Realtime call

This commit is contained in:
LittleSheep 2025-05-26 01:42:59 +08:00
parent edf4ff1c5b
commit f39a066f71
19 changed files with 1433 additions and 112 deletions

View File

@ -41,5 +41,9 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(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
end end

View File

@ -26,6 +26,16 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
@ -33,11 +43,14 @@
<string>fetch</string> <string>fetch</string>
<string>audio</string> <string>audio</string>
<string>remote-notification</string> <string>remote-notification</string>
<string>voip</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@ -51,17 +64,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>UIStatusBarHidden</key>
<false/>
</dict> </dict>
</plist> </plist>

View File

@ -85,4 +85,5 @@ class DefaultFirebaseOptions {
storageBucket: 'solian-0x001.firebasestorage.app', storageBucket: 'solian-0x001.firebasestorage.app',
measurementId: 'G-JD1YEG9D6F', measurementId: 'G-JD1YEG9D6F',
); );
} }

View File

@ -19,6 +19,7 @@ import 'package:island/pods/websocket.dart';
import 'package:island/route.dart'; import 'package:island/route.dart';
import 'package:island/screens/auth/tabs.dart'; import 'package:island/screens/auth/tabs.dart';
import 'package:island/services/notify.dart'; import 'package:island/services/notify.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -28,11 +29,21 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
void main() async { void main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
log(
"[SplashScreen] Keeping the flash screen to loading other resources...",
);
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
} }
await EasyLocalization.ensureInitialized(); try {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await EasyLocalization.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
log("[SplashScreen] Firebase is ready!");
} catch (err) {
showErrorAlert(err);
}
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -43,6 +54,7 @@ void main() async {
appWindow.size = initialSize; appWindow.size = initialSize;
appWindow.alignment = Alignment.center; appWindow.alignment = Alignment.center;
appWindow.show(); appWindow.show();
log("[SplashScreen] Desktop window is ready!");
}); });
} }
@ -52,10 +64,12 @@ void main() async {
if (imagePickerImplementation is ImagePickerAndroid) { if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true; imagePickerImplementation.useAndroidPhotoPicker = true;
} }
log("[SplashScreen] Android image picker is ready!");
} }
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
log("[SplashScreen] Now hiding the splash screen...");
} }
runApp( runApp(

View File

@ -146,12 +146,27 @@ sealed class ChatRealtimeJoinResponse with _$ChatRealtimeJoinResponse {
required String callId, required String callId,
required String roomName, required String roomName,
required bool isAdmin, required bool isAdmin,
required List<CallParticipant> participants,
}) = _ChatRealtimeJoinResponse; }) = _ChatRealtimeJoinResponse;
factory ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) => factory ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) =>
_$ChatRealtimeJoinResponseFromJson(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<String, dynamic> json) =>
_$CallParticipantFromJson(json);
}
@freezed @freezed
sealed class SnRealtimeCall with _$SnRealtimeCall { sealed class SnRealtimeCall with _$SnRealtimeCall {
const factory SnRealtimeCall({ const factory SnRealtimeCall({

View File

@ -1342,7 +1342,7 @@ as DateTime,
/// @nodoc /// @nodoc
mixin _$ChatRealtimeJoinResponse { 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<CallParticipant> get participants;
/// Create a copy of ChatRealtimeJoinResponse /// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -1355,16 +1355,16 @@ $ChatRealtimeJoinResponseCopyWith<ChatRealtimeJoinResponse> get copyWith => _$Ch
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $ChatRealtimeJoinResponseCopyWith(ChatRealtimeJoinResponse value, $Res Function(ChatRealtimeJoinResponse) _then) = _$ChatRealtimeJoinResponseCopyWithImpl;
@useResult @useResult
$Res call({ $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<CallParticipant> participants
}); });
@ -1392,7 +1392,7 @@ class _$ChatRealtimeJoinResponseCopyWithImpl<$Res>
/// Create a copy of ChatRealtimeJoinResponse /// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? 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( return _then(_self.copyWith(
provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable 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 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,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,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 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<CallParticipant>,
)); ));
} }
@ -1411,7 +1412,7 @@ as bool,
@JsonSerializable() @JsonSerializable()
class _ChatRealtimeJoinResponse implements ChatRealtimeJoinResponse { 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<CallParticipant> participants}): _participants = participants;
factory _ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) => _$ChatRealtimeJoinResponseFromJson(json); factory _ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) => _$ChatRealtimeJoinResponseFromJson(json);
@override final String provider; @override final String provider;
@ -1420,6 +1421,13 @@ class _ChatRealtimeJoinResponse implements ChatRealtimeJoinResponse {
@override final String callId; @override final String callId;
@override final String roomName; @override final String roomName;
@override final bool isAdmin; @override final bool isAdmin;
final List<CallParticipant> _participants;
@override List<CallParticipant> get participants {
if (_participants is EqualUnmodifiableListView) return _participants;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_participants);
}
/// Create a copy of ChatRealtimeJoinResponse /// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -1434,16 +1442,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$ChatRealtimeJoinResponseCopyWith(_ChatRealtimeJoinResponse value, $Res Function(_ChatRealtimeJoinResponse) _then) = __$ChatRealtimeJoinResponseCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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<CallParticipant> participants
}); });
@ -1471,7 +1479,7 @@ class __$ChatRealtimeJoinResponseCopyWithImpl<$Res>
/// Create a copy of ChatRealtimeJoinResponse /// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? 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( return _then(_ChatRealtimeJoinResponse(
provider: null == provider ? _self.provider : provider // ignore: cast_nullable_to_non_nullable 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 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,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,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 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<CallParticipant>,
)); ));
} }
@ -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<CallParticipant> get copyWith => _$CallParticipantCopyWithImpl<CallParticipant>(this as CallParticipant, _$identity);
/// Serializes this CallParticipant to a JSON map.
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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 /// @nodoc
mixin _$SnRealtimeCall { mixin _$SnRealtimeCall {

View File

@ -249,6 +249,10 @@ _ChatRealtimeJoinResponse _$ChatRealtimeJoinResponseFromJson(
callId: json['call_id'] as String, callId: json['call_id'] as String,
roomName: json['room_name'] as String, roomName: json['room_name'] as String,
isAdmin: json['is_admin'] as bool, isAdmin: json['is_admin'] as bool,
participants:
(json['participants'] as List<dynamic>)
.map((e) => CallParticipant.fromJson(e as Map<String, dynamic>))
.toList(),
); );
Map<String, dynamic> _$ChatRealtimeJoinResponseToJson( Map<String, dynamic> _$ChatRealtimeJoinResponseToJson(
@ -260,8 +264,30 @@ Map<String, dynamic> _$ChatRealtimeJoinResponseToJson(
'call_id': instance.callId, 'call_id': instance.callId,
'room_name': instance.roomName, 'room_name': instance.roomName,
'is_admin': instance.isAdmin, 'is_admin': instance.isAdmin,
'participants': instance.participants.map((e) => e.toJson()).toList(),
}; };
_CallParticipant _$CallParticipantFromJson(Map<String, dynamic> 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<String, dynamic>),
);
Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) =>
<String, dynamic>{
'identity': instance.identity,
'name': instance.name,
'joined_at': instance.joinedAt.toIso8601String(),
'account_id': instance.accountId,
'profile': instance.profile?.toJson(),
};
_SnRealtimeCall _$SnRealtimeCallFromJson(Map<String, dynamic> json) => _SnRealtimeCall _$SnRealtimeCallFromJson(Map<String, dynamic> json) =>
_SnRealtimeCall( _SnRealtimeCall(
id: json['id'] as String, id: json['id'] as String,

View File

@ -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:livekit_client/livekit_client.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/pods/network.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.g.dart';
part 'call.freezed.dart'; part 'call.freezed.dart';
@ -9,43 +14,244 @@ part 'call.freezed.dart';
@freezed @freezed
sealed class CallState with _$CallState { sealed class CallState with _$CallState {
const factory CallState({ const factory CallState({
required bool isMuted,
required bool isConnected, required bool isConnected,
required bool isMicrophoneEnabled,
required bool isCameraEnabled,
required bool isScreenSharing,
String? error, String? error,
}) = _CallState; }) = _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 @riverpod
class CallNotifier extends _$CallNotifier { class CallNotifier extends _$CallNotifier {
Room? _room; Room? _room;
LocalParticipant? _localParticipant; LocalParticipant? _localParticipant;
LocalAudioTrack? _localAudioTrack; List<CallParticipantLive> _participants = [];
final Map<String, CallParticipant> _participantInfoByIdentity = {};
StreamSubscription? _wsSubscription;
EventsListener? _roomListener;
List<CallParticipantLive> get participants =>
List.unmodifiable(_participants);
LocalParticipant? get localParticipant => _localParticipant;
@override @override
CallState build() { 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<String, dynamic>.from(e)),
)
.toList();
_updateLiveParticipants(parsed);
}
}
});
}
void _initRoomListeners() {
if (_room == null) return;
_roomListener?.dispose();
_roomListener = _room!.createListener();
_room!.addListener(_onRoomChange);
_roomListener!
..on<ParticipantConnectedEvent>((e) {
_refreshLiveParticipants();
})
..on<RoomDisconnectedEvent>((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<CallParticipant>? 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<CallParticipant> 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<CallParticipantLive>(),
);
state = state.copyWith();
}
String? _roomId;
Future<void> joinRoom(String roomId) async { Future<void> joinRoom(String roomId) async {
_roomId = roomId;
try { try {
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
final response = await apiClient.get('/chat/realtime/$roomId/join'); final response = await apiClient.get('/chat/realtime/$roomId/join');
if (response.statusCode == 200 && response.data != null) { if (response.statusCode == 200 && response.data != null) {
final data = response.data; final data = response.data;
final String endpoint = data['endpoint']; // Parse join response
final String token = data['token']; final joinResponse = ChatRealtimeJoinResponse.fromJson(data);
final participants = joinResponse.participants;
final String endpoint = joinResponse.endpoint;
final String token = joinResponse.token;
// Connect to LiveKit // Connect to LiveKit
_room = Room(); _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; _localParticipant = _room!.localParticipant;
// Create local audio track and publish
_localAudioTrack = await LocalAudioTrack.create(); _initRoomListeners();
await _localParticipant!.publishAudioTrack(_localAudioTrack!); _updateLiveParticipants(participants);
// Listen for connection updates // Listen for connection updates
_room!.addListener(() { _room!.addListener(() {
state = state.copyWith( state = state.copyWith(
isConnected: _room!.connectionState == ConnectionState.connected, isConnected: _room!.connectionState == ConnectionState.connected,
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
isCameraEnabled: _localParticipant!.isCameraEnabled(),
isScreenSharing: _localParticipant!.isScreenShareEnabled(),
); );
}); });
state = state.copyWith(isConnected: true); state = state.copyWith(isConnected: true);
@ -57,27 +263,55 @@ class CallNotifier extends _$CallNotifier {
} }
} }
void toggleMute() { Future<void> toggleMicrophone() async {
final newMuted = !state.isMuted; if (_localParticipant != null) {
state = state.copyWith(isMuted: newMuted); const autostop = true;
if (_localAudioTrack != null) { final target = !_localParticipant!.isMicrophoneEnabled();
if (newMuted) { state = state.copyWith(isMicrophoneEnabled: target);
_localAudioTrack!.mute(); if (target) {
await _localParticipant!.audioTrackPublications.firstOrNull?.unmute(
stopOnMute: autostop,
);
} else { } else {
_localAudioTrack!.unmute(); await _localParticipant!.audioTrackPublications.firstOrNull?.mute(
stopOnMute: autostop,
);
} }
} }
} }
Future<void> toggleCamera() async {
if (_localParticipant != null) {
final target = !_localParticipant!.isCameraEnabled();
state = state.copyWith(isCameraEnabled: target);
await _localParticipant!.setCameraEnabled(target);
}
}
Future<void> toggleScreenShare() async {
if (_localParticipant != null) {
final target = !_localParticipant!.isScreenShareEnabled();
state = state.copyWith(isScreenSharing: target);
await _localParticipant!.setScreenShareEnabled(target);
}
}
Future<void> disconnect() async { Future<void> disconnect() async {
if (_room != null) { if (_room != null) {
await _room!.disconnect(); await _room!.disconnect();
state = state.copyWith(isConnected: false); state = state.copyWith(
isConnected: false,
isMicrophoneEnabled: false,
isCameraEnabled: false,
isScreenSharing: false,
);
} }
} }
void dispose() { void dispose() {
_localAudioTrack?.dispose(); _wsSubscription?.cancel();
_roomListener?.dispose();
_room?.removeListener(_onRoomChange);
_room?.dispose(); _room?.dispose();
} }
} }

View File

@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$CallState { 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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -26,16 +26,16 @@ $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>
@override @override
bool operator ==(Object other) { 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 @override
int get hashCode => Object.hash(runtimeType,isMuted,isConnected,error); int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error);
@override @override
String toString() { 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; factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? 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( return _then(_self.copyWith(
isMuted: null == isMuted ? _self.isMuted : isMuted // ignore: cast_nullable_to_non_nullable isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,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 bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?, as String?,
)); ));
@ -79,11 +81,13 @@ as String?,
class _CallState implements CallState { 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 isConnected;
@override final bool isMicrophoneEnabled;
@override final bool isCameraEnabled;
@override final bool isScreenSharing;
@override final String? error; @override final String? error;
/// Create a copy of CallState /// Create a copy of CallState
@ -96,16 +100,16 @@ _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallSt
@override @override
bool operator ==(Object other) { 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 @override
int get hashCode => Object.hash(runtimeType,isMuted,isConnected,error); int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error);
@override @override
String toString() { 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; factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? 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( return _then(_CallState(
isMuted: null == isMuted ? _self.isMuted : isMuted // ignore: cast_nullable_to_non_nullable isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,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 bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?, 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<CallParticipantLive> get copyWith => _$CallParticipantLiveCopyWithImpl<CallParticipantLive>(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 // dart format on

View File

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

View File

@ -62,8 +62,8 @@ class AccountProfileRoute extends _i27.PageRouteInfo<AccountProfileRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<AccountProfileRouteArgs>( final args = data.argsAs<AccountProfileRouteArgs>(
orElse: () => orElse:
AccountProfileRouteArgs(name: pathParams.getString('name')), () => AccountProfileRouteArgs(name: pathParams.getString('name')),
); );
return _i1.AccountProfileScreen(key: args.key, name: args.name); return _i1.AccountProfileScreen(key: args.key, name: args.name);
}, },
@ -81,6 +81,16 @@ class AccountProfileRouteArgs {
String toString() { String toString() {
return 'AccountProfileRouteArgs{key: $key, name: $name}'; 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 /// generated route for
@ -120,6 +130,16 @@ class AccountRouteArgs {
String toString() { String toString() {
return 'AccountRouteArgs{key: $key, isAside: $isAside}'; 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 /// generated route for
@ -193,6 +213,16 @@ class CallRouteArgs {
String toString() { String toString() {
return 'CallRouteArgs{key: $key, roomId: $roomId}'; 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 /// generated route for
@ -234,6 +264,16 @@ class ChatDetailRouteArgs {
String toString() { String toString() {
return 'ChatDetailRouteArgs{key: $key, id: $id}'; 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 /// generated route for
@ -273,6 +313,16 @@ class ChatListRouteArgs {
String toString() { String toString() {
return 'ChatListRouteArgs{key: $key, isAside: $isAside}'; 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 /// generated route for
@ -314,6 +364,16 @@ class ChatRoomRouteArgs {
String toString() { String toString() {
return 'ChatRoomRouteArgs{key: $key, id: $id}'; 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 /// generated route for
@ -385,6 +445,16 @@ class CreatorHubRouteArgs {
String toString() { String toString() {
return 'CreatorHubRouteArgs{key: $key, isAside: $isAside}'; 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 /// generated route for
@ -439,6 +509,16 @@ class EditChatRouteArgs {
String toString() { String toString() {
return 'EditChatRouteArgs{key: $key, id: $id}'; 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 /// generated route for
@ -480,6 +560,16 @@ class EditPublisherRouteArgs {
String toString() { String toString() {
return 'EditPublisherRouteArgs{key: $key, name: $name}'; 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 /// generated route for
@ -521,6 +611,16 @@ class EditRealmRouteArgs {
String toString() { String toString() {
return 'EditRealmRouteArgs{key: $key, slug: $slug}'; 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 /// generated route for
@ -550,10 +650,11 @@ class EditStickerPacksRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<EditStickerPacksRouteArgs>( final args = data.argsAs<EditStickerPacksRouteArgs>(
orElse: () => EditStickerPacksRouteArgs( orElse:
pubName: pathParams.getString('name'), () => EditStickerPacksRouteArgs(
packId: pathParams.optString('packId'), pubName: pathParams.getString('name'),
), packId: pathParams.optString('packId'),
),
); );
return _i12.EditStickerPacksScreen( return _i12.EditStickerPacksScreen(
key: args.key, key: args.key,
@ -581,6 +682,18 @@ class EditStickerPacksRouteArgs {
String toString() { String toString() {
return 'EditStickerPacksRouteArgs{key: $key, pubName: $pubName, packId: $packId}'; 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 /// generated route for
@ -605,10 +718,11 @@ class EditStickersRoute extends _i27.PageRouteInfo<EditStickersRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<EditStickersRouteArgs>( final args = data.argsAs<EditStickersRouteArgs>(
orElse: () => EditStickersRouteArgs( orElse:
packId: pathParams.getString('packId'), () => EditStickersRouteArgs(
id: pathParams.optString('id'), packId: pathParams.getString('packId'),
), id: pathParams.optString('id'),
),
); );
return _i13.EditStickersScreen( return _i13.EditStickersScreen(
key: args.key, key: args.key,
@ -636,6 +750,16 @@ class EditStickersRouteArgs {
String toString() { String toString() {
return 'EditStickersRouteArgs{key: $key, packId: $packId, id: $id}'; 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 /// generated route for
@ -659,8 +783,8 @@ class EventCalanderRoute extends _i27.PageRouteInfo<EventCalanderRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<EventCalanderRouteArgs>( final args = data.argsAs<EventCalanderRouteArgs>(
orElse: () => orElse:
EventCalanderRouteArgs(name: pathParams.getString('name')), () => EventCalanderRouteArgs(name: pathParams.getString('name')),
); );
return _i14.EventCalanderScreen(key: args.key, name: args.name); return _i14.EventCalanderScreen(key: args.key, name: args.name);
}, },
@ -678,6 +802,16 @@ class EventCalanderRouteArgs {
String toString() { String toString() {
return 'EventCalanderRouteArgs{key: $key, name: $name}'; 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 /// generated route for
@ -717,6 +851,16 @@ class ExploreRouteArgs {
String toString() { String toString() {
return 'ExploreRouteArgs{key: $key, isAside: $isAside}'; 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 /// generated route for
@ -821,8 +965,9 @@ class NewStickerPacksRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<NewStickerPacksRouteArgs>( final args = data.argsAs<NewStickerPacksRouteArgs>(
orElse: () => orElse:
NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), () =>
NewStickerPacksRouteArgs(pubName: pathParams.getString('name')),
); );
return _i12.NewStickerPacksScreen(key: args.key, pubName: args.pubName); return _i12.NewStickerPacksScreen(key: args.key, pubName: args.pubName);
}, },
@ -840,6 +985,16 @@ class NewStickerPacksRouteArgs {
String toString() { String toString() {
return 'NewStickerPacksRouteArgs{key: $key, pubName: $pubName}'; 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 /// generated route for
@ -863,8 +1018,8 @@ class NewStickersRoute extends _i27.PageRouteInfo<NewStickersRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<NewStickersRouteArgs>( final args = data.argsAs<NewStickersRouteArgs>(
orElse: () => orElse:
NewStickersRouteArgs(packId: pathParams.getString('packId')), () => NewStickersRouteArgs(packId: pathParams.getString('packId')),
); );
return _i13.NewStickersScreen(key: args.key, packId: args.packId); return _i13.NewStickersScreen(key: args.key, packId: args.packId);
}, },
@ -882,6 +1037,16 @@ class NewStickersRouteArgs {
String toString() { String toString() {
return 'NewStickersRouteArgs{key: $key, packId: $packId}'; 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 /// generated route for
@ -940,6 +1105,16 @@ class PostComposeRouteArgs {
String toString() { String toString() {
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost}'; 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 /// generated route for
@ -981,6 +1156,16 @@ class PostDetailRouteArgs {
String toString() { String toString() {
return 'PostDetailRouteArgs{key: $key, id: $id}'; 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 /// generated route for
@ -1022,6 +1207,16 @@ class PostEditRouteArgs {
String toString() { String toString() {
return 'PostEditRouteArgs{key: $key, id: $id}'; 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 /// generated route for
@ -1046,8 +1241,8 @@ class PublisherProfileRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<PublisherProfileRouteArgs>( final args = data.argsAs<PublisherProfileRouteArgs>(
orElse: () => orElse:
PublisherProfileRouteArgs(name: pathParams.getString('name')), () => PublisherProfileRouteArgs(name: pathParams.getString('name')),
); );
return _i20.PublisherProfileScreen(key: args.key, name: args.name); return _i20.PublisherProfileScreen(key: args.key, name: args.name);
}, },
@ -1065,6 +1260,16 @@ class PublisherProfileRouteArgs {
String toString() { String toString() {
return 'PublisherProfileRouteArgs{key: $key, name: $name}'; 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 /// generated route for
@ -1106,6 +1311,16 @@ class RealmDetailRouteArgs {
String toString() { String toString() {
return 'RealmDetailRouteArgs{key: $key, slug: $slug}'; 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 /// generated route for
@ -1179,10 +1394,11 @@ class StickerPackDetailRoute
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<StickerPackDetailRouteArgs>( final args = data.argsAs<StickerPackDetailRouteArgs>(
orElse: () => StickerPackDetailRouteArgs( orElse:
pubName: pathParams.getString('name'), () => StickerPackDetailRouteArgs(
id: pathParams.getString('packId'), pubName: pathParams.getString('name'),
), id: pathParams.getString('packId'),
),
); );
return _i13.StickerPackDetailScreen( return _i13.StickerPackDetailScreen(
key: args.key, key: args.key,
@ -1210,6 +1426,16 @@ class StickerPackDetailRouteArgs {
String toString() { String toString() {
return 'StickerPackDetailRouteArgs{key: $key, pubName: $pubName, id: $id}'; 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 /// generated route for
@ -1251,6 +1477,16 @@ class StickersRouteArgs {
String toString() { String toString() {
return 'StickersRouteArgs{key: $key, pubName: $pubName}'; 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 /// generated route for
@ -1300,6 +1536,16 @@ class TabsNavigationWidgetArgs {
String toString() { String toString() {
return 'TabsNavigationWidgetArgs{key: $key, child: $child, router: $router}'; 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 /// generated route for

View File

@ -1,8 +1,18 @@
import 'package:auto_route/annotations.dart'; import 'package:auto_route/annotations.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.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() @RoutePage()
class CallScreen extends HookConsumerWidget { class CallScreen extends HookConsumerWidget {
@ -11,6 +21,9 @@ class CallScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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 callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier); final callNotifier = ref.read(callNotifierProvider.notifier);
@ -19,25 +32,327 @@ class CallScreen extends HookConsumerWidget {
return null; return null;
}, []); }, []);
return Scaffold( final actionButtonStyle = ButtonStyle(
appBar: AppBar(title: const Text('Audio Call')), minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
body: Center( );
child: Column(
mainAxisAlignment: MainAxisAlignment.center, final viewMode = useState<String>('grid');
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(
onWillPop: () {
callNotifier.disconnect().then((_) {
callNotifier.dispose();
});
},
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (callState.error != null) Text(
Text(callState.error!, style: const TextStyle(color: Colors.red)), chatRoom.whenOrNull()?.name ?? 'loading'.tr(),
IconButton( style: const TextStyle(fontSize: 16),
icon: Icon(callState.isMuted ? Icons.mic_off : Icons.mic), ),
onPressed: callNotifier.toggleMute, 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();
},
);
},
),
),
],
),
); );
} }
} }

View File

@ -244,6 +244,7 @@ class ChatListScreen extends HookConsumerWidget {
Tab( Tab(
child: Text( child: Text(
'chatTabAll'.tr(), 'chatTabAll'.tr(),
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!, color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
@ -252,6 +253,7 @@ class ChatListScreen extends HookConsumerWidget {
Tab( Tab(
child: Text( child: Text(
'chatTabDirect'.tr(), 'chatTabDirect'.tr(),
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!, color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
@ -260,6 +262,7 @@ class ChatListScreen extends HookConsumerWidget {
Tab( Tab(
child: Text( child: Text(
'chatTabGroup'.tr(), 'chatTabGroup'.tr(),
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!, color: Theme.of(context).appBarTheme.foregroundColor!,
), ),

View File

@ -179,12 +179,14 @@ class AppScaffold extends StatelessWidget {
class PageBackButton extends StatelessWidget { class PageBackButton extends StatelessWidget {
final List<Shadow>? shadows; final List<Shadow>? shadows;
const PageBackButton({super.key, this.shadows}); final VoidCallback? onWillPop;
const PageBackButton({super.key, this.shadows, this.onWillPop});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
onPressed: () { onPressed: () {
onWillPop?.call();
context.router.maybePop(); context.router.maybePop();
}, },
icon: Icon( icon: Icon(

View File

@ -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<double>(
tween: Tween<double>(
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,
);
}
}
}

View File

@ -15,7 +15,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045" BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Solian.app" BuildableName = "island.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
@ -31,7 +31,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045" BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Solian.app" BuildableName = "island.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
@ -66,7 +66,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045" BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Solian.app" BuildableName = "island.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
@ -83,7 +83,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045" BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Solian.app" BuildableName = "island.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>

View File

@ -8,6 +8,8 @@
<true/> <true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key> <key>com.apple.security.files.downloads.read-write</key>
<true/> <true/>
<key>com.apple.security.files.user-selected.read-only</key> <key>com.apple.security.files.user-selected.read-only</key>

View File

@ -4,5 +4,17 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -77,18 +77,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: auto_route name: auto_route
sha256: "89bc5d17d8c575399891194b8cd02b39f52a8512c730052f17ebe443cdcb9109" sha256: eae18fcd3e3762eb6074a3560c0f411d1e36bd9f8d3eed9c15ed1c577e8d1815
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.1" version: "10.1.0"
auto_route_generator: auto_route_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: auto_route_generator name: auto_route_generator
sha256: "8e622d26dc6be4bf496d47969e3e9ba555c3abcf2290da6abfa43cbd4f57fa52" sha256: "9e3846fcbeacba5c362557328dd8c8fbc953b6a0cbc3395365e8d8f92eea29c4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.1" version: "10.1.0"
avatar_stack: avatar_stack:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1225,10 +1225,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: material_symbols_icons name: material_symbols_icons
sha256: d45b6c36c3effa8cb51b1afb8698107d5ff1f88fa4631428f34a8a01abc295d7 sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2815.0" version: "4.2815.1"
media_kit: media_kit:
dependency: "direct main" dependency: "direct main"
description: description: