🧱 Add basis of tencent rtc

This commit is contained in:
2025-05-07 00:48:34 +08:00
parent 937e249b87
commit 419ed666f6
22 changed files with 13972 additions and 43 deletions

View File

@ -0,0 +1,16 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:island/database/drift_db.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
AppDatabase constructDb() {
final db = LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'solar_network_data.sqlite'));
return NativeDatabase(file);
});
return AppDatabase(db);
}

View File

@ -0,0 +1,20 @@
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
import 'package:island/database/drift_db.dart';
AppDatabase constructDb() {
return AppDatabase(connectOnWeb());
}
DatabaseConnection connectOnWeb() {
return DatabaseConnection.delayed(
Future(() async {
final result = await WasmDatabase.open(
databaseName: 'solar_network_data',
sqlite3Uri: Uri.parse('sqlite3.wasm'),
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
);
return result.resolvedExecutor;
}),
);
}

View File

@ -1,17 +1,13 @@
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:island/database/message.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
part 'drift_db.g.dart';
// Define the database
@DriftDatabase(tables: [ChatMessages])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
AppDatabase(super.e);
@override
int get schemaVersion => 1;
@ -75,12 +71,3 @@ class AppDatabase extends _$AppDatabase {
);
}
}
// Helper to open the database connection
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'solar_network_data.sqlite'));
return NativeDatabase(file);
});
}

View File

@ -124,3 +124,14 @@ abstract class MessageSyncResponse with _$MessageSyncResponse {
factory MessageSyncResponse.fromJson(Map<String, dynamic> json) =>
_$MessageSyncResponseFromJson(json);
}
@freezed
abstract class ChatRealtimeJoinResponse with _$ChatRealtimeJoinResponse {
const factory ChatRealtimeJoinResponse({
required String token,
required Map<String, dynamic> config,
}) = _ChatRealtimeJoinResponse;
factory ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) =>
_$ChatRealtimeJoinResponseFromJson(json);
}

View File

@ -1179,6 +1179,148 @@ as DateTime,
}
}
/// @nodoc
mixin _$ChatRealtimeJoinResponse {
String get token; Map<String, dynamic> get config;
/// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ChatRealtimeJoinResponseCopyWith<ChatRealtimeJoinResponse> get copyWith => _$ChatRealtimeJoinResponseCopyWithImpl<ChatRealtimeJoinResponse>(this as ChatRealtimeJoinResponse, _$identity);
/// Serializes this ChatRealtimeJoinResponse to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatRealtimeJoinResponse&&(identical(other.token, token) || other.token == token)&&const DeepCollectionEquality().equals(other.config, config));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,token,const DeepCollectionEquality().hash(config));
@override
String toString() {
return 'ChatRealtimeJoinResponse(token: $token, config: $config)';
}
}
/// @nodoc
abstract mixin class $ChatRealtimeJoinResponseCopyWith<$Res> {
factory $ChatRealtimeJoinResponseCopyWith(ChatRealtimeJoinResponse value, $Res Function(ChatRealtimeJoinResponse) _then) = _$ChatRealtimeJoinResponseCopyWithImpl;
@useResult
$Res call({
String token, Map<String, dynamic> config
});
}
/// @nodoc
class _$ChatRealtimeJoinResponseCopyWithImpl<$Res>
implements $ChatRealtimeJoinResponseCopyWith<$Res> {
_$ChatRealtimeJoinResponseCopyWithImpl(this._self, this._then);
final ChatRealtimeJoinResponse _self;
final $Res Function(ChatRealtimeJoinResponse) _then;
/// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? token = null,Object? config = null,}) {
return _then(_self.copyWith(
token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _ChatRealtimeJoinResponse implements ChatRealtimeJoinResponse {
const _ChatRealtimeJoinResponse({required this.token, required final Map<String, dynamic> config}): _config = config;
factory _ChatRealtimeJoinResponse.fromJson(Map<String, dynamic> json) => _$ChatRealtimeJoinResponseFromJson(json);
@override final String token;
final Map<String, dynamic> _config;
@override Map<String, dynamic> get config {
if (_config is EqualUnmodifiableMapView) return _config;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_config);
}
/// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ChatRealtimeJoinResponseCopyWith<_ChatRealtimeJoinResponse> get copyWith => __$ChatRealtimeJoinResponseCopyWithImpl<_ChatRealtimeJoinResponse>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ChatRealtimeJoinResponseToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatRealtimeJoinResponse&&(identical(other.token, token) || other.token == token)&&const DeepCollectionEquality().equals(other._config, _config));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,token,const DeepCollectionEquality().hash(_config));
@override
String toString() {
return 'ChatRealtimeJoinResponse(token: $token, config: $config)';
}
}
/// @nodoc
abstract mixin class _$ChatRealtimeJoinResponseCopyWith<$Res> implements $ChatRealtimeJoinResponseCopyWith<$Res> {
factory _$ChatRealtimeJoinResponseCopyWith(_ChatRealtimeJoinResponse value, $Res Function(_ChatRealtimeJoinResponse) _then) = __$ChatRealtimeJoinResponseCopyWithImpl;
@override @useResult
$Res call({
String token, Map<String, dynamic> config
});
}
/// @nodoc
class __$ChatRealtimeJoinResponseCopyWithImpl<$Res>
implements _$ChatRealtimeJoinResponseCopyWith<$Res> {
__$ChatRealtimeJoinResponseCopyWithImpl(this._self, this._then);
final _ChatRealtimeJoinResponse _self;
final $Res Function(_ChatRealtimeJoinResponse) _then;
/// Create a copy of ChatRealtimeJoinResponse
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? token = null,Object? config = null,}) {
return _then(_ChatRealtimeJoinResponse(
token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,config: null == config ? _self._config : config // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
// dart format on

View File

@ -223,3 +223,14 @@ Map<String, dynamic> _$MessageSyncResponseToJson(
'changes': instance.changes.map((e) => e.toJson()).toList(),
'current_timestamp': instance.currentTimestamp.toIso8601String(),
};
_ChatRealtimeJoinResponse _$ChatRealtimeJoinResponseFromJson(
Map<String, dynamic> json,
) => _ChatRealtimeJoinResponse(
token: json['token'] as String,
config: json['config'] as Map<String, dynamic>,
);
Map<String, dynamic> _$ChatRealtimeJoinResponseToJson(
_ChatRealtimeJoinResponse instance,
) => <String, dynamic>{'token': instance.token, 'config': instance.config};

11
lib/pods/database.dart Normal file
View File

@ -0,0 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/drift_db.dart';
import 'package:island/database/database.native.dart'
if (dart.library.html) 'package:island/database/database.web.dart';
final databaseProvider = Provider<AppDatabase>((ref) {
final db = constructDb();
ref.onDispose(() => db.close());
return db;
});

View File

@ -2,16 +2,10 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/drift_db.dart';
import 'package:island/pods/database.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
final databaseProvider = Provider<AppDatabase>((ref) {
final db = AppDatabase();
ref.onDispose(() => db.close());
return db;
});
Future<void> resetDatabase(WidgetRef ref) async {
if (kIsWeb) return;

View File

@ -10,11 +10,13 @@ import 'package:island/database/message_repository.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/message.dart';
import 'package:island/pods/database.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/rtc.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
@ -471,6 +473,21 @@ class ChatRoomScreen extends HookConsumerWidget {
error: (_, __) => const Text('Error'),
),
actions: [
IconButton(
onPressed: () {
final user = ref.watch(userInfoProvider);
if (currentlyJoined) {
leaveRealtimeChat(chatRoom.value!);
} else {
joinRealtimeChat(
ref.watch(apiClientProvider),
chatRoom.value!,
user.value!,
);
}
},
icon: const Icon(Symbols.video_call),
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {

51
lib/services/rtc.dart Normal file
View File

@ -0,0 +1,51 @@
import 'package:dio/dio.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/user.dart';
import 'package:tencent_rtc_sdk/trtc_cloud.dart';
import 'package:tencent_rtc_sdk/trtc_cloud_def.dart';
import 'package:tencent_rtc_sdk/trtc_cloud_listener.dart';
Future<TRTCCloud> _getTRTCCloud() async {
return TRTCCloud.sharedInstance();
}
bool currentlyJoined = false;
Future<void> joinRealtimeChat(
Dio apiClient,
SnChatRoom chat,
SnAccount user,
) async {
final resp = await apiClient.get('/chat/realtime/${chat.id}');
final data = ChatRealtimeJoinResponse.fromJson(resp.data);
final config = data.config;
final cloud = await _getTRTCCloud();
cloud.setConsoleEnabled(true);
cloud.registerListener(
TRTCCloudListener(
onRemoteUserEnterRoom: (userId) {
print('onRemoteUserEnterRoom: $userId');
},
onRemoteUserLeaveRoom: (userId, reason) {
print('onRemoteUserLeaveRoom: $userId, $reason');
},
),
);
cloud.enterRoom(
TRTCParams(
sdkAppId: config['app_id'],
userId: user.name,
userSig: data.token,
roomId: chat.id,
role: TRTCRoleType.anchor,
),
TRTCAppScene.voiceChatRoom,
);
cloud.startLocalAudio(TRTCAudioQuality.speech);
currentlyJoined = true;
}
Future<void> leaveRealtimeChat(SnChatRoom chat) async {
final cloud = await _getTRTCCloud();
cloud.exitRoom();
}

View File

@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
@ -258,6 +259,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
final websocketState = ref.watch(websocketStateProvider);
final indicatorHeight = MediaQuery.of(context).padding.top + 60;
@ -277,7 +279,10 @@ class _WebSocketIndicator extends HookConsumerWidget {
return AnimatedPositioned(
duration: Duration(milliseconds: 1850),
top: websocketState == WebSocketState.connected() ? -indicatorHeight : 0,
top:
!user.hasValue || websocketState == WebSocketState.connected()
? -indicatorHeight
: 0,
curve: Curves.fastLinearToSlowEaseIn,
left: 0,
right: 0,