mirror of
https://ghfast.top/https://github.com/StarCitizenToolBox/app.git
synced 2025-05-10 02:41:23 +08:00
feat: log 分析器
This commit is contained in:
parent
8dd7ef53a1
commit
fdc4060ac0
@ -7,6 +7,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hexcolor/hexcolor.dart';
|
||||
import 'package:starcitizen_doctor/app.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||||
import 'package:starcitizen_doctor/generated/l10n.dart';
|
||||
import 'package:starcitizen_doctor/ui/tools/log_analyze_ui/log_analyze_ui.dart';
|
||||
|
||||
import 'base_utils.dart';
|
||||
@ -21,6 +23,7 @@ class MultiWindowAppState with _$MultiWindowAppState {
|
||||
required String backgroundColor,
|
||||
required String menuColor,
|
||||
required String micaColor,
|
||||
required List<String> gameInstallPaths,
|
||||
String? languageCode,
|
||||
String? countryCode,
|
||||
}) = _MultiWindowAppState;
|
||||
@ -29,12 +32,17 @@ class MultiWindowAppState with _$MultiWindowAppState {
|
||||
}
|
||||
|
||||
class MultiWindowManager {
|
||||
static Future<void> launchSubWindow(String type, AppGlobalState appGlobalState) async {
|
||||
static Future<void> launchSubWindow(String type, String title, AppGlobalState appGlobalState) async {
|
||||
final gameInstallPaths = await SCLoggerHelper.getGameInstallPath(await SCLoggerHelper.getLauncherLogList() ?? []);
|
||||
final window = await DesktopMultiWindow.createWindow(jsonEncode({
|
||||
'window_type': type,
|
||||
'app_state': _appStateToWindowState(appGlobalState).toJson(),
|
||||
'app_state': _appStateToWindowState(
|
||||
appGlobalState,
|
||||
gameInstallPaths: gameInstallPaths,
|
||||
).toJson(),
|
||||
}));
|
||||
window.setTitle("Log 分析器");
|
||||
window.setFrame(const Rect.fromLTWH(0, 0, 900, 1200));
|
||||
window.setTitle(title);
|
||||
await window.center();
|
||||
await window.show();
|
||||
// sendAppStateBroadcast(appGlobalState);
|
||||
@ -48,29 +56,28 @@ class MultiWindowManager {
|
||||
);
|
||||
}
|
||||
|
||||
static MultiWindowAppState _appStateToWindowState(AppGlobalState appGlobalState) {
|
||||
static MultiWindowAppState _appStateToWindowState(AppGlobalState appGlobalState, {List<String>? gameInstallPaths}) {
|
||||
return MultiWindowAppState(
|
||||
backgroundColor: colorToHexCode(appGlobalState.themeConf.backgroundColor),
|
||||
menuColor: colorToHexCode(appGlobalState.themeConf.menuColor),
|
||||
micaColor: colorToHexCode(appGlobalState.themeConf.micaColor),
|
||||
languageCode: appGlobalState.appLocale?.languageCode,
|
||||
countryCode: appGlobalState.appLocale?.countryCode,
|
||||
gameInstallPaths: gameInstallPaths ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
static void runSubWindowApp(List<String> args) {
|
||||
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;
|
||||
final windowAppState = MultiWindowAppState.fromJson(argument['app_state'] ?? {});
|
||||
Widget? windowWidget;
|
||||
switch (argument["window_type"]) {
|
||||
case "log_analyze":
|
||||
windowWidget = const ToolsLogAnalyzeDialogUI();
|
||||
windowWidget = ToolsLogAnalyzeDialogUI(appState: windowAppState);
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unknown window type');
|
||||
}
|
||||
|
||||
final windowAppState = MultiWindowAppState.fromJson(argument['app_state'] ?? {});
|
||||
|
||||
return runApp(ProviderScope(
|
||||
child: FluentApp(
|
||||
title: "StarCitizenToolBox",
|
||||
@ -81,8 +88,9 @@ class MultiWindowManager {
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
FluentLocalizations.delegate,
|
||||
S.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('en', 'US')],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
home: windowWidget,
|
||||
theme: FluentThemeData(
|
||||
brightness: Brightness.dark,
|
||||
@ -94,10 +102,10 @@ class MultiWindowManager {
|
||||
micaBackgroundColor: HexColor(windowAppState.micaColor),
|
||||
buttonTheme: ButtonThemeData(
|
||||
defaultButtonStyle: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: BorderSide(color: Colors.white.withValues(alpha: .01)))),
|
||||
))),
|
||||
shape: WidgetStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: BorderSide(color: Colors.white.withValues(alpha: .01)))),
|
||||
))),
|
||||
locale: windowAppState.languageCode != null
|
||||
? Locale(windowAppState.languageCode!, windowAppState.countryCode)
|
||||
: null,
|
||||
|
@ -23,6 +23,7 @@ mixin _$MultiWindowAppState {
|
||||
String get backgroundColor => throw _privateConstructorUsedError;
|
||||
String get menuColor => throw _privateConstructorUsedError;
|
||||
String get micaColor => throw _privateConstructorUsedError;
|
||||
List<String> get gameInstallPaths => throw _privateConstructorUsedError;
|
||||
String? get languageCode => throw _privateConstructorUsedError;
|
||||
String? get countryCode => throw _privateConstructorUsedError;
|
||||
|
||||
@ -46,6 +47,7 @@ abstract class $MultiWindowAppStateCopyWith<$Res> {
|
||||
{String backgroundColor,
|
||||
String menuColor,
|
||||
String micaColor,
|
||||
List<String> gameInstallPaths,
|
||||
String? languageCode,
|
||||
String? countryCode});
|
||||
}
|
||||
@ -68,6 +70,7 @@ class _$MultiWindowAppStateCopyWithImpl<$Res, $Val extends MultiWindowAppState>
|
||||
Object? backgroundColor = null,
|
||||
Object? menuColor = null,
|
||||
Object? micaColor = null,
|
||||
Object? gameInstallPaths = null,
|
||||
Object? languageCode = freezed,
|
||||
Object? countryCode = freezed,
|
||||
}) {
|
||||
@ -84,6 +87,10 @@ class _$MultiWindowAppStateCopyWithImpl<$Res, $Val extends MultiWindowAppState>
|
||||
? _value.micaColor
|
||||
: micaColor // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
gameInstallPaths: null == gameInstallPaths
|
||||
? _value.gameInstallPaths
|
||||
: gameInstallPaths // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
languageCode: freezed == languageCode
|
||||
? _value.languageCode
|
||||
: languageCode // ignore: cast_nullable_to_non_nullable
|
||||
@ -108,6 +115,7 @@ abstract class _$$MultiWindowAppStateImplCopyWith<$Res>
|
||||
{String backgroundColor,
|
||||
String menuColor,
|
||||
String micaColor,
|
||||
List<String> gameInstallPaths,
|
||||
String? languageCode,
|
||||
String? countryCode});
|
||||
}
|
||||
@ -128,6 +136,7 @@ class __$$MultiWindowAppStateImplCopyWithImpl<$Res>
|
||||
Object? backgroundColor = null,
|
||||
Object? menuColor = null,
|
||||
Object? micaColor = null,
|
||||
Object? gameInstallPaths = null,
|
||||
Object? languageCode = freezed,
|
||||
Object? countryCode = freezed,
|
||||
}) {
|
||||
@ -144,6 +153,10 @@ class __$$MultiWindowAppStateImplCopyWithImpl<$Res>
|
||||
? _value.micaColor
|
||||
: micaColor // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
gameInstallPaths: null == gameInstallPaths
|
||||
? _value._gameInstallPaths
|
||||
: gameInstallPaths // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
languageCode: freezed == languageCode
|
||||
? _value.languageCode
|
||||
: languageCode // ignore: cast_nullable_to_non_nullable
|
||||
@ -163,8 +176,10 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
||||
{required this.backgroundColor,
|
||||
required this.menuColor,
|
||||
required this.micaColor,
|
||||
required final List<String> gameInstallPaths,
|
||||
this.languageCode,
|
||||
this.countryCode});
|
||||
this.countryCode})
|
||||
: _gameInstallPaths = gameInstallPaths;
|
||||
|
||||
factory _$MultiWindowAppStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$MultiWindowAppStateImplFromJson(json);
|
||||
@ -175,6 +190,15 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
||||
final String menuColor;
|
||||
@override
|
||||
final String micaColor;
|
||||
final List<String> _gameInstallPaths;
|
||||
@override
|
||||
List<String> get gameInstallPaths {
|
||||
if (_gameInstallPaths is EqualUnmodifiableListView)
|
||||
return _gameInstallPaths;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_gameInstallPaths);
|
||||
}
|
||||
|
||||
@override
|
||||
final String? languageCode;
|
||||
@override
|
||||
@ -182,7 +206,7 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, languageCode: $languageCode, countryCode: $countryCode)';
|
||||
return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -196,6 +220,8 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
||||
other.menuColor == menuColor) &&
|
||||
(identical(other.micaColor, micaColor) ||
|
||||
other.micaColor == micaColor) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._gameInstallPaths, _gameInstallPaths) &&
|
||||
(identical(other.languageCode, languageCode) ||
|
||||
other.languageCode == languageCode) &&
|
||||
(identical(other.countryCode, countryCode) ||
|
||||
@ -204,8 +230,14 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, backgroundColor, menuColor,
|
||||
micaColor, languageCode, countryCode);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
backgroundColor,
|
||||
menuColor,
|
||||
micaColor,
|
||||
const DeepCollectionEquality().hash(_gameInstallPaths),
|
||||
languageCode,
|
||||
countryCode);
|
||||
|
||||
/// Create a copy of MultiWindowAppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -229,6 +261,7 @@ abstract class _MultiWindowAppState implements MultiWindowAppState {
|
||||
{required final String backgroundColor,
|
||||
required final String menuColor,
|
||||
required final String micaColor,
|
||||
required final List<String> gameInstallPaths,
|
||||
final String? languageCode,
|
||||
final String? countryCode}) = _$MultiWindowAppStateImpl;
|
||||
|
||||
@ -242,6 +275,8 @@ abstract class _MultiWindowAppState implements MultiWindowAppState {
|
||||
@override
|
||||
String get micaColor;
|
||||
@override
|
||||
List<String> get gameInstallPaths;
|
||||
@override
|
||||
String? get languageCode;
|
||||
@override
|
||||
String? get countryCode;
|
||||
|
@ -12,6 +12,9 @@ _$MultiWindowAppStateImpl _$$MultiWindowAppStateImplFromJson(
|
||||
backgroundColor: json['backgroundColor'] as String,
|
||||
menuColor: json['menuColor'] as String,
|
||||
micaColor: json['micaColor'] as String,
|
||||
gameInstallPaths: (json['gameInstallPaths'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
languageCode: json['languageCode'] as String?,
|
||||
countryCode: json['countryCode'] as String?,
|
||||
);
|
||||
@ -22,6 +25,7 @@ Map<String, dynamic> _$$MultiWindowAppStateImplToJson(
|
||||
'backgroundColor': instance.backgroundColor,
|
||||
'menuColor': instance.menuColor,
|
||||
'micaColor': instance.micaColor,
|
||||
'gameInstallPaths': instance.gameInstallPaths,
|
||||
'languageCode': instance.languageCode,
|
||||
'countryCode': instance.countryCode,
|
||||
};
|
||||
|
@ -1,12 +1,408 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' show debugPrint;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
part 'log_analyze_provider.g.dart';
|
||||
|
||||
part 'log_analyze_provider.freezed.dart';
|
||||
|
||||
const Map<String?, String> logAnalyzeSearchTypeMap = {
|
||||
null: "全部",
|
||||
"info": "基础信息",
|
||||
"player_login": "账户相关",
|
||||
"fatal_collision": "致命碰撞",
|
||||
"vehicle_destruction": "载具损毁",
|
||||
"actor_death": "角色死亡",
|
||||
"statistics": "统计信息",
|
||||
"game_crash": "游戏崩溃",
|
||||
"request_location_inventory": "本地库存",
|
||||
};
|
||||
|
||||
@freezed
|
||||
class LogAnalyzeLineData with _$LogAnalyzeLineData {
|
||||
const factory LogAnalyzeLineData({
|
||||
required String type,
|
||||
required String title,
|
||||
String? data,
|
||||
String? dateTime,
|
||||
}) = _LogAnalyzeLineData;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
||||
@override
|
||||
void build() async {
|
||||
return;
|
||||
Future<List<LogAnalyzeLineData>> build(String gameInstallPath) async {
|
||||
final logFile = File("$gameInstallPath/Game.log");
|
||||
debugPrint("[ToolsLogAnalyze] logFile: ${logFile.absolute.path}");
|
||||
if (gameInstallPath.isEmpty || !(await logFile.exists())) {
|
||||
return [
|
||||
LogAnalyzeLineData(
|
||||
type: "error",
|
||||
title: "未找到 log 文件",
|
||||
)
|
||||
];
|
||||
}
|
||||
state = AsyncData([]);
|
||||
_launchLogAnalyze(logFile);
|
||||
return state.value ?? [];
|
||||
}
|
||||
|
||||
String _playerName = ""; // 记录玩家名称
|
||||
int _killCount = 0; // 记录击杀其他实体次数
|
||||
int _deathCount = 0; // 记录被击杀次数
|
||||
int _selfKillCount = 0; // 记录自杀次数
|
||||
DateTime? _gameStartTime; // 记录游戏开始时间
|
||||
int _gameCrashLineNumber = -1; // 记录游戏崩溃行号
|
||||
int _currentLineNumber = 0; // 当前行号
|
||||
|
||||
void _launchLogAnalyze(File logFile, {int startLine = 0}) async {
|
||||
final logLines = utf8.decode((await logFile.readAsBytes()), allowMalformed: true).split("\n");
|
||||
debugPrint("[ToolsLogAnalyze] logLines: ${logLines.length}");
|
||||
if (startLine == 0) {
|
||||
_killCount = 0;
|
||||
_deathCount = 0;
|
||||
_selfKillCount = 0;
|
||||
_gameStartTime = null;
|
||||
_gameCrashLineNumber = -1;
|
||||
} else if (startLine > logLines.length) {
|
||||
// 考虑文件重新写入的情况
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
_currentLineNumber = logLines.length;
|
||||
// for i in logLines
|
||||
for (var i = 0; i < logLines.length; i++) {
|
||||
// 支持追加模式
|
||||
if (i < startLine) continue;
|
||||
final line = logLines[i];
|
||||
if (line.isEmpty) continue;
|
||||
final data = _handleLogLine(line, i);
|
||||
if (data != null) {
|
||||
_appendResult(data);
|
||||
// wait for ui update
|
||||
await Future.delayed(Duration(seconds: 0));
|
||||
}
|
||||
}
|
||||
|
||||
final lastLineDateTime =
|
||||
_gameStartTime != null ? _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20"))) : null;
|
||||
|
||||
// 检查游戏崩溃行号
|
||||
if (_gameCrashLineNumber > 0) {
|
||||
// crashInfo 从 logLines _gameCrashLineNumber 开始到最后一行
|
||||
final crashInfo = logLines.sublist(_gameCrashLineNumber);
|
||||
// 运行一键诊断
|
||||
final info = SCLoggerHelper.getGameRunningLogInfo(crashInfo);
|
||||
crashInfo.add("----- 汉化盒子一键诊断 -----");
|
||||
if (info != null) {
|
||||
crashInfo.add(info.key);
|
||||
if (info.value.isNotEmpty) {
|
||||
crashInfo.add("详细信息:${info.value}");
|
||||
}
|
||||
} else {
|
||||
crashInfo.add("未检测到游戏崩溃信息");
|
||||
}
|
||||
_appendResult(LogAnalyzeLineData(
|
||||
type: "game_crash",
|
||||
title: "游戏崩溃 ",
|
||||
data: crashInfo.join("\n"),
|
||||
dateTime: lastLineDateTime != null ? _dateTimeFormatter.format(lastLineDateTime) : null,
|
||||
));
|
||||
}
|
||||
|
||||
// 击杀总结
|
||||
if (_killCount > 0 || _deathCount > 0) {
|
||||
_appendResult(LogAnalyzeLineData(
|
||||
type: "statistics",
|
||||
title: "击杀总结",
|
||||
data: "击杀次数:$_killCount 死亡次数:$_deathCount 自杀次数:$_selfKillCount",
|
||||
));
|
||||
}
|
||||
|
||||
// 统计游玩时长,_gameStartTime 减去 最后一行的时间
|
||||
if (_gameStartTime != null) {
|
||||
if (lastLineDateTime != null) {
|
||||
final duration = lastLineDateTime.difference(_gameStartTime!);
|
||||
_appendResult(LogAnalyzeLineData(
|
||||
type: "statistics",
|
||||
title: "游玩时长",
|
||||
data: "${duration.inHours} 小时 ${duration.inMinutes.remainder(60)} 分钟 ${duration.inSeconds.remainder(60)} 秒",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
_startListenFile(logFile);
|
||||
}
|
||||
|
||||
// 避免重复调用
|
||||
bool _isListenEnabled = false;
|
||||
|
||||
Future<void> _startListenFile(File logFile) async {
|
||||
_isListenEnabled = true;
|
||||
debugPrint("[ToolsLogAnalyze] startListenFile: ${logFile.absolute.path}");
|
||||
// 监听文件
|
||||
late final StreamSubscription sub;
|
||||
sub = FileWatcher(logFile.absolute.path, pollingDelay: Duration(seconds: 1)).events.listen((change) {
|
||||
sub.cancel();
|
||||
if (!_isListenEnabled) return;
|
||||
_isListenEnabled = false;
|
||||
debugPrint("[ToolsLogAnalyze] logFile change: ${change.type}");
|
||||
switch (change.type) {
|
||||
case ChangeType.MODIFY:
|
||||
// 移除统计信息
|
||||
final newList = state.value?.where((e) => e.type != "statistics").toList();
|
||||
state = AsyncData(newList ?? []);
|
||||
return _launchLogAnalyze(logFile, startLine: _currentLineNumber);
|
||||
case ChangeType.ADD:
|
||||
case ChangeType.REMOVE:
|
||||
ref.invalidateSelf();
|
||||
return;
|
||||
}
|
||||
});
|
||||
ref.onDispose(() {
|
||||
sub.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _handleLogLine(String line, int index) {
|
||||
// 处理 log 行,检测可以提取的内容
|
||||
if (_gameStartTime == null) {
|
||||
_gameStartTime = _getLogLineDateTime(line);
|
||||
return LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: "游戏启动",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
// 读取游戏加载时间
|
||||
final gameLoading = _logGetGameLoading(line);
|
||||
if (gameLoading != null) {
|
||||
return LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: "游戏加载",
|
||||
data: "模式:${gameLoading.$1} 用时:${gameLoading.$2} 秒",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
// 运行基础时间解析器
|
||||
final baseEvent = _baseEventDecoder(line);
|
||||
if (baseEvent != null) {
|
||||
switch (baseEvent) {
|
||||
case "AccountLoginCharacterStatus_Character":
|
||||
// 角色登录
|
||||
return _logGetCharacterName(line);
|
||||
case "FatalCollision":
|
||||
// 载具致命碰撞
|
||||
return _logGetFatalCollision(line);
|
||||
case "Vehicle Destruction":
|
||||
// 载具损毁
|
||||
return _logGetVehicleDestruction(line);
|
||||
case "Actor Death":
|
||||
// 角色死亡
|
||||
return _logGetActorDeath(line);
|
||||
case "RequestLocationInventory":
|
||||
// 请求本地库存
|
||||
return _logGetRequestLocationInventory(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (line.contains("[CIG] CCIGBroker::FastShutdown")) {
|
||||
return LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: "游戏关闭",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
if (line.contains("Cloud Imperium Games public crash handler")) {
|
||||
_gameCrashLineNumber = index;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void _appendResult(LogAnalyzeLineData data) {
|
||||
// 追加结果到 state
|
||||
final currentState = state.value;
|
||||
if (currentState != null) {
|
||||
state = AsyncData([...currentState, data]);
|
||||
} else {
|
||||
state = AsyncData([data]);
|
||||
}
|
||||
}
|
||||
|
||||
final _baseRegExp = RegExp(r'\[Notice\]\s+<([^>]+)>');
|
||||
|
||||
String? _baseEventDecoder(String line) {
|
||||
// 解析 log 行的基本信息
|
||||
final match = _baseRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
final type = match.group(1);
|
||||
return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final _gameLoadingRegExp =
|
||||
RegExp(r'<[^>]+>\s+Loading screen for\s+(\w+)\s+:\s+SC_Frontend closed after\s+(\d+\.\d+)\s+seconds');
|
||||
|
||||
(String, String)? _logGetGameLoading(String line) {
|
||||
final match = _gameLoadingRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
return (match.group(1) ?? "-", match.group(2) ?? "-");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final DateFormat _dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss:SSS');
|
||||
final _logDateTimeRegExp = RegExp(r'<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)>');
|
||||
|
||||
DateTime? _getLogLineDateTime(String line) {
|
||||
// 提取 log 行的时间
|
||||
final match = _logDateTimeRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
final dateTimeString = match.group(1);
|
||||
if (dateTimeString != null) {
|
||||
return DateTime.parse(dateTimeString);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _getLogLineDateTimeString(String line) {
|
||||
// 提取 log 行的时间
|
||||
final dateTime = _getLogLineDateTime(line);
|
||||
if (dateTime != null) {
|
||||
return _dateTimeFormatter.format(dateTime);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 安全提取函数
|
||||
String? safeExtract(RegExp pattern, String line) => pattern.firstMatch(line)?.group(1)?.trim();
|
||||
|
||||
LogAnalyzeLineData? _logGetFatalCollision(String line) {
|
||||
final patterns = {
|
||||
'zone': RegExp(r'\[Part:[^\]]*?Zone:\s*([^,\]]+)'),
|
||||
'player_pilot': RegExp(r'PlayerPilot:\s*(\d)'),
|
||||
'hit_entity': RegExp(r'hitting entity:\s*(\w+)'),
|
||||
'hit_entity_vehicle': RegExp(r'hitting entity:[^\[]*\[Zone:\s*([^\s-]+)'),
|
||||
'distance': RegExp(r'Distance:\s*([\d.]+)')
|
||||
};
|
||||
|
||||
final zone = safeExtract(patterns['zone']!, line) ?? 'Unknown';
|
||||
final playerPilot = (safeExtract(patterns['player_pilot']!, line) ?? '0') == '1';
|
||||
final hitEntity = safeExtract(patterns['hit_entity']!, line) ?? 'Unknown';
|
||||
final hitEntityVehicle = safeExtract(patterns['hit_entity_vehicle']!, line) ?? 'Unknown Vehicle';
|
||||
final distance = double.tryParse(safeExtract(patterns['distance']!, line) ?? '') ?? 0.0;
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "fatal_collision",
|
||||
title: "致命碰撞",
|
||||
data: "区域:$zone 玩家驾驶:${playerPilot ? '✅' : '❌'} 碰撞实体:$hitEntity \n碰撞载具: $hitEntityVehicle 碰撞距离:$distance ",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetVehicleDestruction(String line) {
|
||||
final pattern = RegExp(r"Vehicle\s+'([^']+)'.*?" // 载具型号
|
||||
r"in zone\s+'([^']+)'.*?" // Zone
|
||||
r"destroy level \d+ to (\d+).*?" // 损毁等级
|
||||
r"caused by\s+'([^']+)'" // 责任方
|
||||
);
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final vehicleModel = match.group(1) ?? 'Unknown';
|
||||
final zone = match.group(2) ?? 'Unknown';
|
||||
final destructionLevel = int.tryParse(match.group(3) ?? '') ?? 0;
|
||||
final causedBy = match.group(4) ?? 'Unknown';
|
||||
|
||||
const destructionLevelMap = {1: "软死亡", 2: "解体"};
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "vehicle_destruction",
|
||||
title: "载具损毁",
|
||||
data:
|
||||
"载具型号:$vehicleModel \n区域:$zone \n损毁等级:$destructionLevel (${destructionLevelMap[destructionLevel]}) 责任方:$causedBy",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetActorDeath(String line) {
|
||||
final pattern = RegExp(r"CActor::Kill: '([^']+)'.*?" // 受害者ID
|
||||
r"in zone '([^']+)'.*?" // 死亡位置区域
|
||||
r"killed by '([^']+)'.*?" // 击杀者ID
|
||||
r"with damage type '([^']+)'" // 伤害类型
|
||||
);
|
||||
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final victimId = match.group(1) ?? 'Unknown';
|
||||
final zone = match.group(2) ?? 'Unknown';
|
||||
final killerId = match.group(3) ?? 'Unknown';
|
||||
final damageType = match.group(4) ?? 'Unknown';
|
||||
|
||||
if (victimId.trim() == killerId.trim()) {
|
||||
// 自杀
|
||||
_selfKillCount++;
|
||||
} else {
|
||||
if (victimId.trim() == _playerName) {
|
||||
_deathCount++;
|
||||
}
|
||||
if (killerId.trim() == _playerName) {
|
||||
_killCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "actor_death",
|
||||
title: "角色死亡",
|
||||
data: "受害者ID:$victimId 死因:$damageType \n击杀者ID:$killerId \n区域:$zone",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetCharacterName(String line) {
|
||||
final pattern = RegExp(r"name\s+([^-]+)");
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final characterName = match.group(1)?.trim() ?? 'Unknown';
|
||||
_playerName = characterName.trim(); // 更新玩家名称
|
||||
return LogAnalyzeLineData(
|
||||
type: "player_login",
|
||||
title: "玩家 $characterName 登录 ...",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetRequestLocationInventory(String line) {
|
||||
final pattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final playerId = match.group(1) ?? 'Unknown';
|
||||
final location = match.group(2) ?? 'Unknown';
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "request_location_inventory",
|
||||
title: "查看本地库存",
|
||||
data: "玩家ID:$playerId 位置:$location",
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
198
lib/ui/tools/log_analyze_ui/log_analyze_provider.freezed.dart
Normal file
198
lib/ui/tools/log_analyze_ui/log_analyze_provider.freezed.dart
Normal file
@ -0,0 +1,198 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// 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 'log_analyze_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LogAnalyzeLineData {
|
||||
String get type => throw _privateConstructorUsedError;
|
||||
String get title => throw _privateConstructorUsedError;
|
||||
String? get data => throw _privateConstructorUsedError;
|
||||
String? get dateTime => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$LogAnalyzeLineDataCopyWith<LogAnalyzeLineData> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LogAnalyzeLineDataCopyWith<$Res> {
|
||||
factory $LogAnalyzeLineDataCopyWith(
|
||||
LogAnalyzeLineData value, $Res Function(LogAnalyzeLineData) then) =
|
||||
_$LogAnalyzeLineDataCopyWithImpl<$Res, LogAnalyzeLineData>;
|
||||
@useResult
|
||||
$Res call({String type, String title, String? data, String? dateTime});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LogAnalyzeLineDataCopyWithImpl<$Res, $Val extends LogAnalyzeLineData>
|
||||
implements $LogAnalyzeLineDataCopyWith<$Res> {
|
||||
_$LogAnalyzeLineDataCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? type = null,
|
||||
Object? title = null,
|
||||
Object? data = freezed,
|
||||
Object? dateTime = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
data: freezed == data
|
||||
? _value.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
dateTime: freezed == dateTime
|
||||
? _value.dateTime
|
||||
: dateTime // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LogAnalyzeLineDataImplCopyWith<$Res>
|
||||
implements $LogAnalyzeLineDataCopyWith<$Res> {
|
||||
factory _$$LogAnalyzeLineDataImplCopyWith(_$LogAnalyzeLineDataImpl value,
|
||||
$Res Function(_$LogAnalyzeLineDataImpl) then) =
|
||||
__$$LogAnalyzeLineDataImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String type, String title, String? data, String? dateTime});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LogAnalyzeLineDataImplCopyWithImpl<$Res>
|
||||
extends _$LogAnalyzeLineDataCopyWithImpl<$Res, _$LogAnalyzeLineDataImpl>
|
||||
implements _$$LogAnalyzeLineDataImplCopyWith<$Res> {
|
||||
__$$LogAnalyzeLineDataImplCopyWithImpl(_$LogAnalyzeLineDataImpl _value,
|
||||
$Res Function(_$LogAnalyzeLineDataImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? type = null,
|
||||
Object? title = null,
|
||||
Object? data = freezed,
|
||||
Object? dateTime = freezed,
|
||||
}) {
|
||||
return _then(_$LogAnalyzeLineDataImpl(
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
data: freezed == data
|
||||
? _value.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
dateTime: freezed == dateTime
|
||||
? _value.dateTime
|
||||
: dateTime // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$LogAnalyzeLineDataImpl implements _LogAnalyzeLineData {
|
||||
const _$LogAnalyzeLineDataImpl(
|
||||
{required this.type, required this.title, this.data, this.dateTime});
|
||||
|
||||
@override
|
||||
final String type;
|
||||
@override
|
||||
final String title;
|
||||
@override
|
||||
final String? data;
|
||||
@override
|
||||
final String? dateTime;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LogAnalyzeLineDataImpl &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.data, data) || other.data == data) &&
|
||||
(identical(other.dateTime, dateTime) ||
|
||||
other.dateTime == dateTime));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, type, title, data, dateTime);
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LogAnalyzeLineDataImplCopyWith<_$LogAnalyzeLineDataImpl> get copyWith =>
|
||||
__$$LogAnalyzeLineDataImplCopyWithImpl<_$LogAnalyzeLineDataImpl>(
|
||||
this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _LogAnalyzeLineData implements LogAnalyzeLineData {
|
||||
const factory _LogAnalyzeLineData(
|
||||
{required final String type,
|
||||
required final String title,
|
||||
final String? data,
|
||||
final String? dateTime}) = _$LogAnalyzeLineDataImpl;
|
||||
|
||||
@override
|
||||
String get type;
|
||||
@override
|
||||
String get title;
|
||||
@override
|
||||
String? get data;
|
||||
@override
|
||||
String? get dateTime;
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$LogAnalyzeLineDataImplCopyWith<_$LogAnalyzeLineDataImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
@ -6,21 +6,175 @@ part of 'log_analyze_provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$toolsLogAnalyzeHash() => r'a31922fe5ee020b06e8d494486c39bdd261af34c';
|
||||
String _$toolsLogAnalyzeHash() => r'cc8aed5b4eeb6c8feb35c59ef484dc61c92a5549';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$ToolsLogAnalyze
|
||||
extends BuildlessAutoDisposeAsyncNotifier<List<LogAnalyzeLineData>> {
|
||||
late final String gameInstallPath;
|
||||
|
||||
FutureOr<List<LogAnalyzeLineData>> build(
|
||||
String gameInstallPath,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [ToolsLogAnalyze].
|
||||
@ProviderFor(ToolsLogAnalyze)
|
||||
final toolsLogAnalyzeProvider =
|
||||
AutoDisposeNotifierProvider<ToolsLogAnalyze, void>.internal(
|
||||
ToolsLogAnalyze.new,
|
||||
name: r'toolsLogAnalyzeProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$toolsLogAnalyzeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
const toolsLogAnalyzeProvider = ToolsLogAnalyzeFamily();
|
||||
|
||||
typedef _$ToolsLogAnalyze = AutoDisposeNotifier<void>;
|
||||
/// See also [ToolsLogAnalyze].
|
||||
class ToolsLogAnalyzeFamily
|
||||
extends Family<AsyncValue<List<LogAnalyzeLineData>>> {
|
||||
/// See also [ToolsLogAnalyze].
|
||||
const ToolsLogAnalyzeFamily();
|
||||
|
||||
/// See also [ToolsLogAnalyze].
|
||||
ToolsLogAnalyzeProvider call(
|
||||
String gameInstallPath,
|
||||
) {
|
||||
return ToolsLogAnalyzeProvider(
|
||||
gameInstallPath,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ToolsLogAnalyzeProvider getProviderOverride(
|
||||
covariant ToolsLogAnalyzeProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.gameInstallPath,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'toolsLogAnalyzeProvider';
|
||||
}
|
||||
|
||||
/// See also [ToolsLogAnalyze].
|
||||
class ToolsLogAnalyzeProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
||||
ToolsLogAnalyze, List<LogAnalyzeLineData>> {
|
||||
/// See also [ToolsLogAnalyze].
|
||||
ToolsLogAnalyzeProvider(
|
||||
String gameInstallPath,
|
||||
) : this._internal(
|
||||
() => ToolsLogAnalyze()..gameInstallPath = gameInstallPath,
|
||||
from: toolsLogAnalyzeProvider,
|
||||
name: r'toolsLogAnalyzeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$toolsLogAnalyzeHash,
|
||||
dependencies: ToolsLogAnalyzeFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ToolsLogAnalyzeFamily._allTransitiveDependencies,
|
||||
gameInstallPath: gameInstallPath,
|
||||
);
|
||||
|
||||
ToolsLogAnalyzeProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.gameInstallPath,
|
||||
}) : super.internal();
|
||||
|
||||
final String gameInstallPath;
|
||||
|
||||
@override
|
||||
FutureOr<List<LogAnalyzeLineData>> runNotifierBuild(
|
||||
covariant ToolsLogAnalyze notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
gameInstallPath,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ToolsLogAnalyze Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ToolsLogAnalyzeProvider._internal(
|
||||
() => create()..gameInstallPath = gameInstallPath,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
gameInstallPath: gameInstallPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<ToolsLogAnalyze,
|
||||
List<LogAnalyzeLineData>> createElement() {
|
||||
return _ToolsLogAnalyzeProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ToolsLogAnalyzeProvider &&
|
||||
other.gameInstallPath == gameInstallPath;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, gameInstallPath.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ToolsLogAnalyzeRef
|
||||
on AutoDisposeAsyncNotifierProviderRef<List<LogAnalyzeLineData>> {
|
||||
/// The parameter `gameInstallPath` of this provider.
|
||||
String get gameInstallPath;
|
||||
}
|
||||
|
||||
class _ToolsLogAnalyzeProviderElement
|
||||
extends AutoDisposeAsyncNotifierProviderElement<ToolsLogAnalyze,
|
||||
List<LogAnalyzeLineData>> with ToolsLogAnalyzeRef {
|
||||
_ToolsLogAnalyzeProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get gameInstallPath =>
|
||||
(origin as ToolsLogAnalyzeProvider).gameInstallPath;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
@ -1,18 +1,253 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/multi_window_manager.dart';
|
||||
|
||||
import 'log_analyze_provider.dart';
|
||||
|
||||
class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
||||
const ToolsLogAnalyzeDialogUI({super.key});
|
||||
final MultiWindowAppState appState;
|
||||
|
||||
const ToolsLogAnalyzeDialogUI({super.key, required this.appState});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedPath = useState<String?>(appState.gameInstallPaths.firstOrNull);
|
||||
final logResp = ref.watch(toolsLogAnalyzeProvider(selectedPath.value ?? ""));
|
||||
final searchText = useState<String>("");
|
||||
final searchType = useState<String?>(null);
|
||||
final lastListSize = useState<int>(0);
|
||||
|
||||
final listCtrl = useScrollController();
|
||||
|
||||
_diffData(logResp, lastListSize, listCtrl);
|
||||
return ScaffoldPage(
|
||||
header: const PageHeader(
|
||||
title: Text("Log 分析器"),
|
||||
),
|
||||
content: Column(
|
||||
children: [],
|
||||
children: [
|
||||
// game Path selector
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text("游戏安装路径"),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: ComboBox<String>(
|
||||
isExpanded: true,
|
||||
value: selectedPath.value,
|
||||
items: [
|
||||
for (final path in appState.gameInstallPaths)
|
||||
ComboBoxItem<String>(
|
||||
value: path,
|
||||
child: Text(path),
|
||||
),
|
||||
],
|
||||
onChanged: (value) => selectedPath.value = value,
|
||||
placeholder: const Text("请选择游戏安装路径"),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
// 刷新 IconButton
|
||||
Button(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
child: const Icon(FluentIcons.refresh),
|
||||
),
|
||||
onPressed: () {
|
||||
ref.invalidate(toolsLogAnalyzeProvider(selectedPath.value ?? ""));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
// 搜索,筛选
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
// 输入框
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
prefix: Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Icon(FluentIcons.search),
|
||||
),
|
||||
placeholder: "输入关键字搜索内容",
|
||||
onChanged: (value) {
|
||||
searchText.value = value.trim();
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
// 筛选 ComboBox
|
||||
ComboBox<String>(
|
||||
isExpanded: false,
|
||||
value: searchType.value,
|
||||
placeholder: const Text("全部"),
|
||||
items: logAnalyzeSearchTypeMap.entries
|
||||
.map((e) => ComboBoxItem<String>(
|
||||
value: e.key,
|
||||
child: Text(e.value),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
searchType.value = value;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// log analyze result
|
||||
if (!logResp.hasValue)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: ProgressRing(),
|
||||
))
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: listCtrl,
|
||||
itemCount: logResp.value!.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = logResp.value![index];
|
||||
if (searchText.value.isNotEmpty) {
|
||||
// 搜索
|
||||
if (!item.toString().contains(searchText.value)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
if (searchType.value != null) {
|
||||
if (item.type != searchType.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: SelectionArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(item.type),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_getIconWidget(item.type),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(
|
||||
text: item.title,
|
||||
),
|
||||
if (item.dateTime != null)
|
||||
TextSpan(
|
||||
text: " (${item.dateTime})",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (item.data != null)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
item.data!,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getIconWidget(String key) {
|
||||
const iconMap = {
|
||||
"info": Icon(FluentIcons.info),
|
||||
"account_login": Icon(FluentIcons.accounts),
|
||||
"player_login": Icon(FontAwesomeIcons.solidIdCard),
|
||||
"fatal_collision": Icon(FontAwesomeIcons.personFallingBurst),
|
||||
"vehicle_destruction": Icon(FontAwesomeIcons.carBurst),
|
||||
"actor_death": Icon(FontAwesomeIcons.skull),
|
||||
"statistics": Icon(FontAwesomeIcons.chartSimple),
|
||||
"game_crash": Icon(FontAwesomeIcons.bug),
|
||||
"request_location_inventory": Icon(FontAwesomeIcons.box),
|
||||
};
|
||||
return iconMap[key] ?? const Icon(FluentIcons.info);
|
||||
}
|
||||
|
||||
Color _getBackgroundColor(String type) {
|
||||
switch (type) {
|
||||
case "actor_death":
|
||||
case "fatal_collision":
|
||||
return Colors.red.withValues(alpha: .3);
|
||||
case "game_crash":
|
||||
return Color.fromRGBO(0, 0, 128, 1);
|
||||
case "vehicle_destruction":
|
||||
return Colors.yellow.withValues(alpha: .1);
|
||||
default:
|
||||
return Colors.white.withValues(alpha: .06);
|
||||
}
|
||||
}
|
||||
|
||||
void _diffData(
|
||||
AsyncValue<List<LogAnalyzeLineData>> logResp, ValueNotifier<int> lastListSize, ScrollController listCtrl) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (lastListSize.value == 0) {
|
||||
lastListSize.value = logResp.value?.length ?? 0;
|
||||
} else {
|
||||
// 判断当前列表是否在底部
|
||||
if (listCtrl.position.pixels >= listCtrl.position.maxScrollExtent) {
|
||||
// 如果在底部,判断数据是否有变化
|
||||
if ((logResp.value?.length ?? 0) > lastListSize.value) {
|
||||
Future.delayed(Duration(milliseconds: 100)).then((_) {
|
||||
listCtrl.jumpTo(listCtrl.position.maxScrollExtent);
|
||||
});
|
||||
lastListSize.value = logResp.value?.length ?? 0;
|
||||
} else {
|
||||
// 回顶部
|
||||
if (listCtrl.position.pixels > 0) {
|
||||
listCtrl.jumpTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -568,6 +568,6 @@ class ToolsUIModel extends _$ToolsUIModel {
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
await MultiWindowManager.launchSubWindow("log_analyze", appGlobalState);
|
||||
await MultiWindowManager.launchSubWindow("log_analyze", "SC汉化盒子: log 分析器", appGlobalState);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ part of 'tools_ui_model.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$toolsUIModelHash() => r'af6e6de30c191a7c9a4e2a1c96a688fba6ee086c';
|
||||
String _$toolsUIModelHash() => r'cd72f7833fa5696baf9022d16d10d7951387df7e';
|
||||
|
||||
/// See also [ToolsUIModel].
|
||||
@ProviderFor(ToolsUIModel)
|
||||
|
@ -17,7 +17,7 @@ dependencies:
|
||||
riverpod_annotation: ^2.6.1
|
||||
flutter_hooks: ^0.21.2
|
||||
hooks_riverpod: ^2.6.1
|
||||
json_annotation: ^4.8.1
|
||||
json_annotation: ^4.9.0
|
||||
go_router: ^14.0.1
|
||||
window_manager: ^0.4.0
|
||||
fluent_ui: ^4.8.6
|
||||
@ -67,6 +67,7 @@ dependencies:
|
||||
shelf: ^1.4.1
|
||||
qr_flutter: ^4.1.0
|
||||
desktop_multi_window: ^0.2.1
|
||||
watcher: ^1.1.1
|
||||
dependency_overrides:
|
||||
http: ^1.1.2
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user