mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-02-06 15:10:20 +00:00
feat: 增加基础的游戏进程监听
This commit is contained in:
@@ -7,7 +7,7 @@ import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_list_page.da
|
||||
import 'package:starcitizen_doctor/ui/party_room/widgets/detail/party_room_detail_page.dart';
|
||||
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_register_page.dart';
|
||||
|
||||
class PartyRoomUI extends HookConsumerWidget {
|
||||
class PartyRoomUI extends ConsumerWidget {
|
||||
const PartyRoomUI({super.key});
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart';
|
||||
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||
import 'package:starcitizen_doctor/ui/party_room/utils/party_room_utils.dart' show PartyRoomUtils;
|
||||
|
||||
import 'utils/game_log_tracker_provider.dart';
|
||||
|
||||
part 'party_room_ui_model.freezed.dart';
|
||||
|
||||
@@ -37,6 +42,7 @@ sealed class PartyRoomUIState with _$PartyRoomUIState {
|
||||
@riverpod
|
||||
class PartyRoomUIModel extends _$PartyRoomUIModel {
|
||||
Timer? _reconnectTimer;
|
||||
ProviderSubscription? _gameLogSubscription;
|
||||
|
||||
@override
|
||||
PartyRoomUIState build() {
|
||||
@@ -48,18 +54,59 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
|
||||
if (previous?.room.isInRoom == true && !next.room.isInRoom) {
|
||||
state = state.copyWith(isMinimized: false);
|
||||
}
|
||||
|
||||
// 监听房间创建时间变化,设置游戏日志监听
|
||||
if (previous?.room.currentRoom?.createdAt != next.room.currentRoom?.createdAt) {
|
||||
_setupGameLogListener(next.room.currentRoom?.createdAt);
|
||||
}
|
||||
});
|
||||
|
||||
connectToServer();
|
||||
|
||||
// 在 dispose 时清理定时器
|
||||
// 在 dispose 时清理定时器和订阅
|
||||
ref.onDispose(() {
|
||||
_reconnectTimer?.cancel();
|
||||
_gameLogSubscription?.close();
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// 设置游戏日志监听
|
||||
void _setupGameLogListener(Int64? createdAt) {
|
||||
// 清除之前的订阅
|
||||
_gameLogSubscription?.close();
|
||||
_gameLogSubscription = null;
|
||||
|
||||
final dateTime = PartyRoomUtils.getDateTime(createdAt);
|
||||
if (dateTime != null) {
|
||||
_gameLogSubscription = ref.listen<PartyRoomGameLogTrackerProviderState>(
|
||||
partyRoomGameLogTrackerProviderProvider(startTime: dateTime),
|
||||
(previous, next) => _onUpdateGameStatus(previous, next),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理游戏状态更新
|
||||
void _onUpdateGameStatus(PartyRoomGameLogTrackerProviderState? previous, PartyRoomGameLogTrackerProviderState next) {
|
||||
// 防抖
|
||||
final currentGameStartTime = previous?.gameStartTime?.millisecondsSinceEpoch;
|
||||
final gameStartTime = next.gameStartTime?.microsecondsSinceEpoch;
|
||||
if (next.kills != previous?.kills ||
|
||||
next.deaths != previous?.deaths ||
|
||||
next.location != previous?.location ||
|
||||
currentGameStartTime != gameStartTime) {
|
||||
// 更新状态
|
||||
ref
|
||||
.read(partyRoomProvider.notifier)
|
||||
.setStatus(
|
||||
kills: next.kills != previous?.kills ? next.kills : null,
|
||||
deaths: next.deaths != previous?.deaths ? next.deaths : null,
|
||||
currentLocation: next.location != previous?.location ? next.location : null,
|
||||
playTime: currentGameStartTime != gameStartTime ? gameStartTime : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理连接状态变化
|
||||
void _handleConnectionStateChange(PartyRoomFullState? previous, PartyRoomFullState next) {
|
||||
// 检测断线:之前已连接但现在未连接
|
||||
@@ -140,17 +187,16 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
|
||||
|
||||
// 加载标签(游客和登录用户都需要)
|
||||
await partyRoom.loadTags();
|
||||
|
||||
// 非游客模式:尝试登录
|
||||
try {
|
||||
state = state.copyWith(isLoggingIn: true);
|
||||
await partyRoom.login();
|
||||
// 登录成功,加载房间列表
|
||||
await loadRoomList();
|
||||
state = state.copyWith(showRoomList: true);
|
||||
state = state.copyWith(showRoomList: true, isLoggingIn: false, isGuestMode: false);
|
||||
} catch (e) {
|
||||
// 未注册,保持在连接状态
|
||||
dPrint('[PartyRoomUI] Login failed, need register: $e');
|
||||
state = state.copyWith(isGuestMode: true);
|
||||
} finally {
|
||||
state = state.copyWith(isLoggingIn: false);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ final class PartyRoomUIModelProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$partyRoomUIModelHash() => r'48291373cafc9005843478a90970152426b3a666';
|
||||
String _$partyRoomUIModelHash() => r'a0b6c3632ff33f2d58882f9bc1ab58c69c2487f4';
|
||||
|
||||
abstract class _$PartyRoomUIModel extends $Notifier<PartyRoomUIState> {
|
||||
PartyRoomUIState build();
|
||||
|
||||
172
lib/ui/party_room/utils/game_log_tracker_provider.dart
Normal file
172
lib/ui/party_room/utils/game_log_tracker_provider.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
|
||||
import 'package:starcitizen_doctor/common/rust/api/win32_api.dart' as win32;
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
|
||||
part 'game_log_tracker_provider.freezed.dart';
|
||||
|
||||
part 'game_log_tracker_provider.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class PartyRoomGameLogTrackerProviderState with _$PartyRoomGameLogTrackerProviderState {
|
||||
const factory PartyRoomGameLogTrackerProviderState({
|
||||
@Default('') String location,
|
||||
@Default(0) int kills,
|
||||
@Default(0) int deaths,
|
||||
DateTime? gameStartTime,
|
||||
@Default([]) List<String> killedIds, // 本次迭代新增的击杀ID
|
||||
@Default([]) List<String> deathIds, // 本次迭代新增的死亡ID
|
||||
}) = _PartyRoomGameLogTrackerProviderState;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider {
|
||||
var _disposed = false;
|
||||
|
||||
// 记录上次查询的时间点,用于计算增量
|
||||
DateTime? _lastQueryTime;
|
||||
|
||||
@override
|
||||
PartyRoomGameLogTrackerProviderState build({required DateTime startTime}) {
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] init $startTime");
|
||||
ref.onDispose(() {
|
||||
_disposed = true;
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] disposed $startTime");
|
||||
});
|
||||
_lastQueryTime = startTime;
|
||||
_listenToGameLogs(startTime);
|
||||
return const PartyRoomGameLogTrackerProviderState();
|
||||
}
|
||||
|
||||
Future<void> _listenToGameLogs(DateTime startTime) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
while (!_disposed) {
|
||||
try {
|
||||
// 获取正在运行的游戏进程
|
||||
final l = await win32.getProcessListByName(processName: "StarCitizen.exe");
|
||||
final p = l
|
||||
.where((e) => e.path.toLowerCase().contains("starcitizen") && e.path.toLowerCase().contains("bin64"))
|
||||
.firstOrNull;
|
||||
|
||||
if (p == null) throw Exception("process not found");
|
||||
|
||||
final logPath = _getLogPath(p);
|
||||
final logFile = File(logPath);
|
||||
|
||||
if (await logFile.exists()) {
|
||||
// 分析日志文件
|
||||
await _analyzeLog(logFile, startTime);
|
||||
} else {
|
||||
state = state.copyWith(location: '<Unknown>', gameStartTime: null);
|
||||
}
|
||||
} catch (e) {
|
||||
// 游戏未启动或发生错误
|
||||
state = state.copyWith(
|
||||
location: '<游戏未启动>',
|
||||
gameStartTime: null,
|
||||
kills: 0,
|
||||
deaths: 0,
|
||||
killedIds: [],
|
||||
deathIds: [],
|
||||
);
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _analyzeLog(File logFile, DateTime startTime) async {
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
|
||||
// 使用 GameLogAnalyzer 分析日志
|
||||
// startTime 只影响计数统计
|
||||
final result = await GameLogAnalyzer.analyzeLogFile(logFile, startTime: startTime);
|
||||
final (logData, statistics) = result;
|
||||
|
||||
// 从统计数据中直接获取最新位置(全量查找的结果)
|
||||
final location = statistics.latestLocation == null ? '<主菜单>' : '[${statistics.latestLocation}]';
|
||||
|
||||
// 计算基于 _lastQueryTime 之后的增量 ID
|
||||
final newKilledIds = <String>[];
|
||||
final newDeathIds = <String>[];
|
||||
|
||||
if (_lastQueryTime != null) {
|
||||
// 遍历所有 actor_death 事件
|
||||
for (final data in logData) {
|
||||
if (data.type != "actor_death") continue;
|
||||
|
||||
// 解析事件时间
|
||||
if (data.dateTime != null) {
|
||||
try {
|
||||
// 日志时间格式: "yyyy-MM-dd HH:mm:ss:SSS"
|
||||
// 转换为 ISO 8601 格式再解析
|
||||
final parts = data.dateTime!.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
final datePart = parts[0]; // yyyy-MM-dd
|
||||
final timeParts = parts[1].split(':');
|
||||
if (timeParts.length >= 3) {
|
||||
final hour = timeParts[0];
|
||||
final minute = timeParts[1];
|
||||
final secondMillis = timeParts[2]; // ss:SSS 或 ss.SSS
|
||||
final timeStr = '$datePart $hour:$minute:${secondMillis.replaceAll(':', '.')}';
|
||||
final eventTime = DateTime.parse(timeStr);
|
||||
|
||||
// 只处理在 _lastQueryTime 之后的事件
|
||||
if (eventTime.isBefore(_lastQueryTime!)) continue;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] Failed to parse dateTime: ${data.dateTime}, error: $e");
|
||||
// 时间解析失败,继续处理该事件(保守策略)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用格式化字段,不再重新解析
|
||||
final victimId = data.victimId;
|
||||
final killerId = data.killerId;
|
||||
|
||||
if (victimId != null && killerId != null && victimId != killerId) {
|
||||
// 如果玩家是击杀者,记录被击杀的ID
|
||||
if (killerId == statistics.playerName) {
|
||||
newKilledIds.add(victimId);
|
||||
}
|
||||
|
||||
// 如果玩家是受害者,记录击杀者ID
|
||||
if (victimId == statistics.playerName) {
|
||||
newDeathIds.add(killerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态,只存储本次迭代的增量数据
|
||||
state = state.copyWith(
|
||||
location: location,
|
||||
kills: statistics.killCount,
|
||||
// 从 startTime 开始的总计数
|
||||
deaths: statistics.deathCount,
|
||||
// 从 startTime 开始的总计数
|
||||
gameStartTime: statistics.gameStartTime,
|
||||
// 全量查找的游戏开始时间
|
||||
killedIds: newKilledIds,
|
||||
// 只存储本次迭代的增量
|
||||
deathIds: newDeathIds, // 只存储本次迭代的增量
|
||||
);
|
||||
|
||||
// 更新查询时间为本次查询的时刻
|
||||
_lastQueryTime = now;
|
||||
} catch (e, stackTrace) {
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] Error analyzing log: $e");
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] Stack trace: $stackTrace");
|
||||
}
|
||||
}
|
||||
|
||||
String _getLogPath(win32.ProcessInfo p) {
|
||||
var path = p.path;
|
||||
return path.replaceAll(r"Bin64\StarCitizen.exe", "Game.log");
|
||||
}
|
||||
}
|
||||
295
lib/ui/party_room/utils/game_log_tracker_provider.freezed.dart
Normal file
295
lib/ui/party_room/utils/game_log_tracker_provider.freezed.dart
Normal file
@@ -0,0 +1,295 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'game_log_tracker_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$PartyRoomGameLogTrackerProviderState {
|
||||
|
||||
String get location; int get kills; int get deaths; DateTime? get gameStartTime; List<String> get killedIds;// 本次迭代新增的击杀ID
|
||||
List<String> get deathIds;
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PartyRoomGameLogTrackerProviderStateCopyWith<PartyRoomGameLogTrackerProviderState> get copyWith => _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<PartyRoomGameLogTrackerProviderState>(this as PartyRoomGameLogTrackerProviderState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PartyRoomGameLogTrackerProviderState&&(identical(other.location, location) || other.location == location)&&(identical(other.kills, kills) || other.kills == kills)&&(identical(other.deaths, deaths) || other.deaths == deaths)&&(identical(other.gameStartTime, gameStartTime) || other.gameStartTime == gameStartTime)&&const DeepCollectionEquality().equals(other.killedIds, killedIds)&&const DeepCollectionEquality().equals(other.deathIds, deathIds));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(killedIds),const DeepCollectionEquality().hash(deathIds));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
factory $PartyRoomGameLogTrackerProviderStateCopyWith(PartyRoomGameLogTrackerProviderState value, $Res Function(PartyRoomGameLogTrackerProviderState) _then) = _$PartyRoomGameLogTrackerProviderStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
|
||||
implements $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
_$PartyRoomGameLogTrackerProviderStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PartyRoomGameLogTrackerProviderState _self;
|
||||
final $Res Function(PartyRoomGameLogTrackerProviderState) _then;
|
||||
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? killedIds = null,Object? deathIds = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as String,kills: null == kills ? _self.kills : kills // ignore: cast_nullable_to_non_nullable
|
||||
as int,deaths: null == deaths ? _self.deaths : deaths // ignore: cast_nullable_to_non_nullable
|
||||
as int,gameStartTime: freezed == gameStartTime ? _self.gameStartTime : gameStartTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,killedIds: null == killedIds ? _self.killedIds : killedIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,deathIds: null == deathIds ? _self.deathIds : deathIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PartyRoomGameLogTrackerProviderState].
|
||||
extension PartyRoomGameLogTrackerProviderStatePatterns on PartyRoomGameLogTrackerProviderState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PartyRoomGameLogTrackerProviderState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PartyRoomGameLogTrackerProviderState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PartyRoomGameLogTrackerProviderState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState():
|
||||
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _PartyRoomGameLogTrackerProviderState implements PartyRoomGameLogTrackerProviderState {
|
||||
const _PartyRoomGameLogTrackerProviderState({this.location = '', this.kills = 0, this.deaths = 0, this.gameStartTime, final List<String> killedIds = const [], final List<String> deathIds = const []}): _killedIds = killedIds,_deathIds = deathIds;
|
||||
|
||||
|
||||
@override@JsonKey() final String location;
|
||||
@override@JsonKey() final int kills;
|
||||
@override@JsonKey() final int deaths;
|
||||
@override final DateTime? gameStartTime;
|
||||
final List<String> _killedIds;
|
||||
@override@JsonKey() List<String> get killedIds {
|
||||
if (_killedIds is EqualUnmodifiableListView) return _killedIds;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_killedIds);
|
||||
}
|
||||
|
||||
// 本次迭代新增的击杀ID
|
||||
final List<String> _deathIds;
|
||||
// 本次迭代新增的击杀ID
|
||||
@override@JsonKey() List<String> get deathIds {
|
||||
if (_deathIds is EqualUnmodifiableListView) return _deathIds;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_deathIds);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PartyRoomGameLogTrackerProviderStateCopyWith<_PartyRoomGameLogTrackerProviderState> get copyWith => __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<_PartyRoomGameLogTrackerProviderState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PartyRoomGameLogTrackerProviderState&&(identical(other.location, location) || other.location == location)&&(identical(other.kills, kills) || other.kills == kills)&&(identical(other.deaths, deaths) || other.deaths == deaths)&&(identical(other.gameStartTime, gameStartTime) || other.gameStartTime == gameStartTime)&&const DeepCollectionEquality().equals(other._killedIds, _killedIds)&&const DeepCollectionEquality().equals(other._deathIds, _deathIds));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(_killedIds),const DeepCollectionEquality().hash(_deathIds));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PartyRoomGameLogTrackerProviderStateCopyWith<$Res> implements $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
factory _$PartyRoomGameLogTrackerProviderStateCopyWith(_PartyRoomGameLogTrackerProviderState value, $Res Function(_PartyRoomGameLogTrackerProviderState) _then) = __$PartyRoomGameLogTrackerProviderStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
|
||||
implements _$PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
__$PartyRoomGameLogTrackerProviderStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PartyRoomGameLogTrackerProviderState _self;
|
||||
final $Res Function(_PartyRoomGameLogTrackerProviderState) _then;
|
||||
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? killedIds = null,Object? deathIds = null,}) {
|
||||
return _then(_PartyRoomGameLogTrackerProviderState(
|
||||
location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as String,kills: null == kills ? _self.kills : kills // ignore: cast_nullable_to_non_nullable
|
||||
as int,deaths: null == deaths ? _self.deaths : deaths // ignore: cast_nullable_to_non_nullable
|
||||
as int,gameStartTime: freezed == gameStartTime ? _self.gameStartTime : gameStartTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,killedIds: null == killedIds ? _self._killedIds : killedIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,deathIds: null == deathIds ? _self._deathIds : deathIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
128
lib/ui/party_room/utils/game_log_tracker_provider.g.dart
Normal file
128
lib/ui/party_room/utils/game_log_tracker_provider.g.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'game_log_tracker_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PartyRoomGameLogTrackerProvider)
|
||||
const partyRoomGameLogTrackerProviderProvider =
|
||||
PartyRoomGameLogTrackerProviderFamily._();
|
||||
|
||||
final class PartyRoomGameLogTrackerProviderProvider
|
||||
extends
|
||||
$NotifierProvider<
|
||||
PartyRoomGameLogTrackerProvider,
|
||||
PartyRoomGameLogTrackerProviderState
|
||||
> {
|
||||
const PartyRoomGameLogTrackerProviderProvider._({
|
||||
required PartyRoomGameLogTrackerProviderFamily super.from,
|
||||
required DateTime super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'partyRoomGameLogTrackerProviderProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$partyRoomGameLogTrackerProviderHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'partyRoomGameLogTrackerProviderProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PartyRoomGameLogTrackerProvider create() => PartyRoomGameLogTrackerProvider();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PartyRoomGameLogTrackerProviderState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride:
|
||||
$SyncValueProvider<PartyRoomGameLogTrackerProviderState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PartyRoomGameLogTrackerProviderProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$partyRoomGameLogTrackerProviderHash() =>
|
||||
r'ecb015eb46d25bfe11bbb153242fd5c4f20ef367';
|
||||
|
||||
final class PartyRoomGameLogTrackerProviderFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
PartyRoomGameLogTrackerProvider,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
DateTime
|
||||
> {
|
||||
const PartyRoomGameLogTrackerProviderFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'partyRoomGameLogTrackerProviderProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
PartyRoomGameLogTrackerProviderProvider call({required DateTime startTime}) =>
|
||||
PartyRoomGameLogTrackerProviderProvider._(
|
||||
argument: startTime,
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'partyRoomGameLogTrackerProviderProvider';
|
||||
}
|
||||
|
||||
abstract class _$PartyRoomGameLogTrackerProvider
|
||||
extends $Notifier<PartyRoomGameLogTrackerProviderState> {
|
||||
late final _$args = ref.$arg as DateTime;
|
||||
DateTime get startTime => _$args;
|
||||
|
||||
PartyRoomGameLogTrackerProviderState build({required DateTime startTime});
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(startTime: _$args);
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState
|
||||
>,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class PartyRoomHeader extends ConsumerWidget {
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.back, size: 16, color: Color(0xFFB5BAC1)),
|
||||
icon: const Icon(FluentIcons.back, size: 16, color: Colors.white),
|
||||
onPressed: () {
|
||||
ref.read(partyRoomUIModelProvider.notifier).setMinimized(true);
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@ class PartyRoomMemberItem extends ConsumerWidget {
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
child: GestureDetector(
|
||||
onTapUp: (details) => _showMemberContextMenu(context, member, partyRoom, isOwner, isSelf, flyoutController),
|
||||
onSecondaryTapUp: (details) =>
|
||||
_showMemberContextMenu(context, member, partyRoom, isOwner, isSelf, flyoutController),
|
||||
child: HoverButton(
|
||||
@@ -94,24 +95,23 @@ class PartyRoomMemberItem extends ConsumerWidget {
|
||||
],
|
||||
],
|
||||
),
|
||||
if (member.status.currentLocation.isNotEmpty)
|
||||
Text(
|
||||
member.status.currentLocation,
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFF80848E)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
member.status.currentLocation.isNotEmpty ? member.status.currentLocation : '...',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .9)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
"K: ${member.status.kills} D: ${member.status.deaths}",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 状态指示器
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF23A559), // 在线绿色
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -134,7 +134,7 @@ class PartyRoomMemberItem extends ConsumerWidget {
|
||||
// 复制ID - 所有用户可用
|
||||
MenuFlyoutItem(
|
||||
leading: const Icon(FluentIcons.copy, size: 16),
|
||||
text: const Text('复制用户ID'),
|
||||
text: const Text('复制游戏ID'),
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: member.gameUserId));
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart' as
|
||||
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||
import 'package:starcitizen_doctor/widgets/src/cache_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// 消息列表组件
|
||||
class PartyRoomMessageList extends ConsumerWidget {
|
||||
@@ -20,19 +21,17 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
final room = partyRoomState.room.currentRoom;
|
||||
final hasSocialLinks = room != null && room.socialLinks.isNotEmpty;
|
||||
|
||||
// 计算总项数:社交链接消息(如果有)+ 事件消息
|
||||
final totalItems = (hasSocialLinks ? 1 : 0) + events.length;
|
||||
// 计算总项数:社交链接消息(如果有)+ 复制 ID 消息 + 事件消息
|
||||
final totalItems = (hasSocialLinks ? 1 : 0) + 1 + events.length;
|
||||
|
||||
if (totalItems == 0) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(FluentIcons.chat, size: 64, color: Color(0xFF404249)),
|
||||
Icon(FluentIcons.chat, size: 64, color: Colors.white.withValues(alpha: .6)),
|
||||
const SizedBox(height: 16),
|
||||
Text('暂无消息', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14)),
|
||||
const SizedBox(height: 4),
|
||||
Text('发送一条信号开始对话吧!', style: TextStyle(color: Colors.white.withValues(alpha: 0.3), fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -47,9 +46,13 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
if (hasSocialLinks && index == 0) {
|
||||
return _buildSocialLinksMessage(room);
|
||||
}
|
||||
|
||||
// 第二条消息显示复制 ID
|
||||
final copyIdIndex = hasSocialLinks ? 1 : 0;
|
||||
if (index == copyIdIndex) {
|
||||
return _buildCopyIdMessage(room);
|
||||
}
|
||||
// 其他消息显示事件
|
||||
final eventIndex = hasSocialLinks ? index - 1 : index;
|
||||
final eventIndex = index - (hasSocialLinks ? 2 : 1);
|
||||
final event = events[eventIndex];
|
||||
return _MessageItem(event: event);
|
||||
},
|
||||
@@ -78,7 +81,7 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'该房间包含第三方社交连接,点击加入一起开黑吧~',
|
||||
'该房间包含第三方社交连接,点击加入自由交流吧~',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFFDBDEE1), fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
@@ -117,6 +120,68 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCopyIdMessage(dynamic room) {
|
||||
final ownerGameId = room?.ownerGameId ?? '';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: const Color(0xFF2B2D31), borderRadius: BorderRadius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(color: Colors.purple, shape: BoxShape.circle),
|
||||
child: const Icon(FluentIcons.copy, size: 14, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'复制房主的游戏ID,可在游戏首页添加好友,快速组队',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFFDBDEE1), fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(color: const Color(0xFF1E1F22), borderRadius: BorderRadius.circular(4)),
|
||||
child: Text(ownerGameId, style: const TextStyle(fontSize: 13, color: Color(0xFFDBDEE1))),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Button(
|
||||
onPressed: ownerGameId.isNotEmpty
|
||||
? () async {
|
||||
await Clipboard.setData(ClipboardData(text: ownerGameId));
|
||||
}
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)),
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.isPressed) return const Color(0xFF3A40A0);
|
||||
if (states.isHovered) return const Color(0xFF4752C4);
|
||||
return const Color(0xFF5865F2);
|
||||
}),
|
||||
),
|
||||
child: const Text(
|
||||
'复制',
|
||||
style: TextStyle(fontSize: 13, color: Colors.white, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getSocialIcon(String link) {
|
||||
if (link.contains('qq.com')) return FontAwesomeIcons.qq;
|
||||
if (link.contains('discord')) return FontAwesomeIcons.discord;
|
||||
@@ -145,6 +210,9 @@ class _MessageItem extends ConsumerWidget {
|
||||
final userName = _getEventUserName(roomEvent);
|
||||
final avatarUrl = _getEventAvatarUrl(roomEvent);
|
||||
|
||||
final text = _getEventText(roomEvent, ref);
|
||||
if (text == null) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
@@ -176,7 +244,7 @@ class _MessageItem extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getEventText(roomEvent, ref),
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isSignal ? const Color(0xFFDBDEE1) : const Color(0xFF949BA4),
|
||||
@@ -223,7 +291,7 @@ class _MessageItem extends ConsumerWidget {
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getEventText(partroom.RoomEvent event, WidgetRef ref) {
|
||||
String? _getEventText(partroom.RoomEvent event, WidgetRef ref) {
|
||||
final partyRoomState = ref.read(partyRoomProvider);
|
||||
final signalTypes = partyRoomState.room.signalTypes;
|
||||
switch (event.type) {
|
||||
@@ -245,21 +313,13 @@ class _MessageItem extends ConsumerWidget {
|
||||
case partroom.RoomEventType.ROOM_UPDATED:
|
||||
return '房间信息已更新';
|
||||
case partroom.RoomEventType.MEMBER_STATUS_UPDATED:
|
||||
if (event.hasMember()) {
|
||||
final member = event.member;
|
||||
final name = member.handleName.isNotEmpty ? member.handleName : member.gameUserId;
|
||||
if (member.hasStatus() && member.status.currentLocation.isNotEmpty) {
|
||||
return '$name 更新了状态: ${member.status.currentLocation}';
|
||||
}
|
||||
return '$name 更新了状态';
|
||||
}
|
||||
return '成员状态已更新';
|
||||
return null;
|
||||
case partroom.RoomEventType.ROOM_DISMISSED:
|
||||
return '房间已解散';
|
||||
case partroom.RoomEventType.MEMBER_KICKED:
|
||||
return '被踢出房间';
|
||||
default:
|
||||
return '未知事件';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user