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:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hexcolor/hexcolor.dart';
|
import 'package:hexcolor/hexcolor.dart';
|
||||||
import 'package:starcitizen_doctor/app.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 'package:starcitizen_doctor/ui/tools/log_analyze_ui/log_analyze_ui.dart';
|
||||||
|
|
||||||
import 'base_utils.dart';
|
import 'base_utils.dart';
|
||||||
@ -21,6 +23,7 @@ class MultiWindowAppState with _$MultiWindowAppState {
|
|||||||
required String backgroundColor,
|
required String backgroundColor,
|
||||||
required String menuColor,
|
required String menuColor,
|
||||||
required String micaColor,
|
required String micaColor,
|
||||||
|
required List<String> gameInstallPaths,
|
||||||
String? languageCode,
|
String? languageCode,
|
||||||
String? countryCode,
|
String? countryCode,
|
||||||
}) = _MultiWindowAppState;
|
}) = _MultiWindowAppState;
|
||||||
@ -29,12 +32,17 @@ class MultiWindowAppState with _$MultiWindowAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MultiWindowManager {
|
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({
|
final window = await DesktopMultiWindow.createWindow(jsonEncode({
|
||||||
'window_type': type,
|
'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.center();
|
||||||
await window.show();
|
await window.show();
|
||||||
// sendAppStateBroadcast(appGlobalState);
|
// sendAppStateBroadcast(appGlobalState);
|
||||||
@ -48,29 +56,28 @@ class MultiWindowManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static MultiWindowAppState _appStateToWindowState(AppGlobalState appGlobalState) {
|
static MultiWindowAppState _appStateToWindowState(AppGlobalState appGlobalState, {List<String>? gameInstallPaths}) {
|
||||||
return MultiWindowAppState(
|
return MultiWindowAppState(
|
||||||
backgroundColor: colorToHexCode(appGlobalState.themeConf.backgroundColor),
|
backgroundColor: colorToHexCode(appGlobalState.themeConf.backgroundColor),
|
||||||
menuColor: colorToHexCode(appGlobalState.themeConf.menuColor),
|
menuColor: colorToHexCode(appGlobalState.themeConf.menuColor),
|
||||||
micaColor: colorToHexCode(appGlobalState.themeConf.micaColor),
|
micaColor: colorToHexCode(appGlobalState.themeConf.micaColor),
|
||||||
languageCode: appGlobalState.appLocale?.languageCode,
|
languageCode: appGlobalState.appLocale?.languageCode,
|
||||||
countryCode: appGlobalState.appLocale?.countryCode,
|
countryCode: appGlobalState.appLocale?.countryCode,
|
||||||
|
gameInstallPaths: gameInstallPaths ?? [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void runSubWindowApp(List<String> args) {
|
static void runSubWindowApp(List<String> args) {
|
||||||
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;
|
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;
|
||||||
|
final windowAppState = MultiWindowAppState.fromJson(argument['app_state'] ?? {});
|
||||||
Widget? windowWidget;
|
Widget? windowWidget;
|
||||||
switch (argument["window_type"]) {
|
switch (argument["window_type"]) {
|
||||||
case "log_analyze":
|
case "log_analyze":
|
||||||
windowWidget = const ToolsLogAnalyzeDialogUI();
|
windowWidget = ToolsLogAnalyzeDialogUI(appState: windowAppState);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Exception('Unknown window type');
|
throw Exception('Unknown window type');
|
||||||
}
|
}
|
||||||
|
|
||||||
final windowAppState = MultiWindowAppState.fromJson(argument['app_state'] ?? {});
|
|
||||||
|
|
||||||
return runApp(ProviderScope(
|
return runApp(ProviderScope(
|
||||||
child: FluentApp(
|
child: FluentApp(
|
||||||
title: "StarCitizenToolBox",
|
title: "StarCitizenToolBox",
|
||||||
@ -81,8 +88,9 @@ class MultiWindowManager {
|
|||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
FluentLocalizations.delegate,
|
FluentLocalizations.delegate,
|
||||||
|
S.delegate,
|
||||||
],
|
],
|
||||||
supportedLocales: const [Locale('en', 'US')],
|
supportedLocales: S.delegate.supportedLocales,
|
||||||
home: windowWidget,
|
home: windowWidget,
|
||||||
theme: FluentThemeData(
|
theme: FluentThemeData(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
@ -94,10 +102,10 @@ class MultiWindowManager {
|
|||||||
micaBackgroundColor: HexColor(windowAppState.micaColor),
|
micaBackgroundColor: HexColor(windowAppState.micaColor),
|
||||||
buttonTheme: ButtonThemeData(
|
buttonTheme: ButtonThemeData(
|
||||||
defaultButtonStyle: ButtonStyle(
|
defaultButtonStyle: ButtonStyle(
|
||||||
shape: WidgetStateProperty.all(RoundedRectangleBorder(
|
shape: WidgetStateProperty.all(RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
side: BorderSide(color: Colors.white.withValues(alpha: .01)))),
|
side: BorderSide(color: Colors.white.withValues(alpha: .01)))),
|
||||||
))),
|
))),
|
||||||
locale: windowAppState.languageCode != null
|
locale: windowAppState.languageCode != null
|
||||||
? Locale(windowAppState.languageCode!, windowAppState.countryCode)
|
? Locale(windowAppState.languageCode!, windowAppState.countryCode)
|
||||||
: null,
|
: null,
|
||||||
|
@ -23,6 +23,7 @@ mixin _$MultiWindowAppState {
|
|||||||
String get backgroundColor => throw _privateConstructorUsedError;
|
String get backgroundColor => throw _privateConstructorUsedError;
|
||||||
String get menuColor => throw _privateConstructorUsedError;
|
String get menuColor => throw _privateConstructorUsedError;
|
||||||
String get micaColor => throw _privateConstructorUsedError;
|
String get micaColor => throw _privateConstructorUsedError;
|
||||||
|
List<String> get gameInstallPaths => throw _privateConstructorUsedError;
|
||||||
String? get languageCode => throw _privateConstructorUsedError;
|
String? get languageCode => throw _privateConstructorUsedError;
|
||||||
String? get countryCode => throw _privateConstructorUsedError;
|
String? get countryCode => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ abstract class $MultiWindowAppStateCopyWith<$Res> {
|
|||||||
{String backgroundColor,
|
{String backgroundColor,
|
||||||
String menuColor,
|
String menuColor,
|
||||||
String micaColor,
|
String micaColor,
|
||||||
|
List<String> gameInstallPaths,
|
||||||
String? languageCode,
|
String? languageCode,
|
||||||
String? countryCode});
|
String? countryCode});
|
||||||
}
|
}
|
||||||
@ -68,6 +70,7 @@ class _$MultiWindowAppStateCopyWithImpl<$Res, $Val extends MultiWindowAppState>
|
|||||||
Object? backgroundColor = null,
|
Object? backgroundColor = null,
|
||||||
Object? menuColor = null,
|
Object? menuColor = null,
|
||||||
Object? micaColor = null,
|
Object? micaColor = null,
|
||||||
|
Object? gameInstallPaths = null,
|
||||||
Object? languageCode = freezed,
|
Object? languageCode = freezed,
|
||||||
Object? countryCode = freezed,
|
Object? countryCode = freezed,
|
||||||
}) {
|
}) {
|
||||||
@ -84,6 +87,10 @@ class _$MultiWindowAppStateCopyWithImpl<$Res, $Val extends MultiWindowAppState>
|
|||||||
? _value.micaColor
|
? _value.micaColor
|
||||||
: micaColor // ignore: cast_nullable_to_non_nullable
|
: micaColor // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
gameInstallPaths: null == gameInstallPaths
|
||||||
|
? _value.gameInstallPaths
|
||||||
|
: gameInstallPaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
languageCode: freezed == languageCode
|
languageCode: freezed == languageCode
|
||||||
? _value.languageCode
|
? _value.languageCode
|
||||||
: languageCode // ignore: cast_nullable_to_non_nullable
|
: languageCode // ignore: cast_nullable_to_non_nullable
|
||||||
@ -108,6 +115,7 @@ abstract class _$$MultiWindowAppStateImplCopyWith<$Res>
|
|||||||
{String backgroundColor,
|
{String backgroundColor,
|
||||||
String menuColor,
|
String menuColor,
|
||||||
String micaColor,
|
String micaColor,
|
||||||
|
List<String> gameInstallPaths,
|
||||||
String? languageCode,
|
String? languageCode,
|
||||||
String? countryCode});
|
String? countryCode});
|
||||||
}
|
}
|
||||||
@ -128,6 +136,7 @@ class __$$MultiWindowAppStateImplCopyWithImpl<$Res>
|
|||||||
Object? backgroundColor = null,
|
Object? backgroundColor = null,
|
||||||
Object? menuColor = null,
|
Object? menuColor = null,
|
||||||
Object? micaColor = null,
|
Object? micaColor = null,
|
||||||
|
Object? gameInstallPaths = null,
|
||||||
Object? languageCode = freezed,
|
Object? languageCode = freezed,
|
||||||
Object? countryCode = freezed,
|
Object? countryCode = freezed,
|
||||||
}) {
|
}) {
|
||||||
@ -144,6 +153,10 @@ class __$$MultiWindowAppStateImplCopyWithImpl<$Res>
|
|||||||
? _value.micaColor
|
? _value.micaColor
|
||||||
: micaColor // ignore: cast_nullable_to_non_nullable
|
: micaColor // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
gameInstallPaths: null == gameInstallPaths
|
||||||
|
? _value._gameInstallPaths
|
||||||
|
: gameInstallPaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
languageCode: freezed == languageCode
|
languageCode: freezed == languageCode
|
||||||
? _value.languageCode
|
? _value.languageCode
|
||||||
: languageCode // ignore: cast_nullable_to_non_nullable
|
: languageCode // ignore: cast_nullable_to_non_nullable
|
||||||
@ -163,8 +176,10 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
|||||||
{required this.backgroundColor,
|
{required this.backgroundColor,
|
||||||
required this.menuColor,
|
required this.menuColor,
|
||||||
required this.micaColor,
|
required this.micaColor,
|
||||||
|
required final List<String> gameInstallPaths,
|
||||||
this.languageCode,
|
this.languageCode,
|
||||||
this.countryCode});
|
this.countryCode})
|
||||||
|
: _gameInstallPaths = gameInstallPaths;
|
||||||
|
|
||||||
factory _$MultiWindowAppStateImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$MultiWindowAppStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$MultiWindowAppStateImplFromJson(json);
|
_$$MultiWindowAppStateImplFromJson(json);
|
||||||
@ -175,6 +190,15 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
|||||||
final String menuColor;
|
final String menuColor;
|
||||||
@override
|
@override
|
||||||
final String micaColor;
|
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
|
@override
|
||||||
final String? languageCode;
|
final String? languageCode;
|
||||||
@override
|
@override
|
||||||
@ -182,7 +206,7 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
@override
|
||||||
@ -196,6 +220,8 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
|||||||
other.menuColor == menuColor) &&
|
other.menuColor == menuColor) &&
|
||||||
(identical(other.micaColor, micaColor) ||
|
(identical(other.micaColor, micaColor) ||
|
||||||
other.micaColor == micaColor) &&
|
other.micaColor == micaColor) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._gameInstallPaths, _gameInstallPaths) &&
|
||||||
(identical(other.languageCode, languageCode) ||
|
(identical(other.languageCode, languageCode) ||
|
||||||
other.languageCode == languageCode) &&
|
other.languageCode == languageCode) &&
|
||||||
(identical(other.countryCode, countryCode) ||
|
(identical(other.countryCode, countryCode) ||
|
||||||
@ -204,8 +230,14 @@ class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
|
|||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, backgroundColor, menuColor,
|
int get hashCode => Object.hash(
|
||||||
micaColor, languageCode, countryCode);
|
runtimeType,
|
||||||
|
backgroundColor,
|
||||||
|
menuColor,
|
||||||
|
micaColor,
|
||||||
|
const DeepCollectionEquality().hash(_gameInstallPaths),
|
||||||
|
languageCode,
|
||||||
|
countryCode);
|
||||||
|
|
||||||
/// Create a copy of MultiWindowAppState
|
/// Create a copy of MultiWindowAppState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// 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 backgroundColor,
|
||||||
required final String menuColor,
|
required final String menuColor,
|
||||||
required final String micaColor,
|
required final String micaColor,
|
||||||
|
required final List<String> gameInstallPaths,
|
||||||
final String? languageCode,
|
final String? languageCode,
|
||||||
final String? countryCode}) = _$MultiWindowAppStateImpl;
|
final String? countryCode}) = _$MultiWindowAppStateImpl;
|
||||||
|
|
||||||
@ -242,6 +275,8 @@ abstract class _MultiWindowAppState implements MultiWindowAppState {
|
|||||||
@override
|
@override
|
||||||
String get micaColor;
|
String get micaColor;
|
||||||
@override
|
@override
|
||||||
|
List<String> get gameInstallPaths;
|
||||||
|
@override
|
||||||
String? get languageCode;
|
String? get languageCode;
|
||||||
@override
|
@override
|
||||||
String? get countryCode;
|
String? get countryCode;
|
||||||
|
@ -12,6 +12,9 @@ _$MultiWindowAppStateImpl _$$MultiWindowAppStateImplFromJson(
|
|||||||
backgroundColor: json['backgroundColor'] as String,
|
backgroundColor: json['backgroundColor'] as String,
|
||||||
menuColor: json['menuColor'] as String,
|
menuColor: json['menuColor'] as String,
|
||||||
micaColor: json['micaColor'] as String,
|
micaColor: json['micaColor'] as String,
|
||||||
|
gameInstallPaths: (json['gameInstallPaths'] as List<dynamic>)
|
||||||
|
.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
languageCode: json['languageCode'] as String?,
|
languageCode: json['languageCode'] as String?,
|
||||||
countryCode: json['countryCode'] as String?,
|
countryCode: json['countryCode'] as String?,
|
||||||
);
|
);
|
||||||
@ -22,6 +25,7 @@ Map<String, dynamic> _$$MultiWindowAppStateImplToJson(
|
|||||||
'backgroundColor': instance.backgroundColor,
|
'backgroundColor': instance.backgroundColor,
|
||||||
'menuColor': instance.menuColor,
|
'menuColor': instance.menuColor,
|
||||||
'micaColor': instance.micaColor,
|
'micaColor': instance.micaColor,
|
||||||
|
'gameInstallPaths': instance.gameInstallPaths,
|
||||||
'languageCode': instance.languageCode,
|
'languageCode': instance.languageCode,
|
||||||
'countryCode': instance.countryCode,
|
'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: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.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
|
@riverpod
|
||||||
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
||||||
@override
|
@override
|
||||||
void build() async {
|
Future<List<LogAnalyzeLineData>> build(String gameInstallPath) async {
|
||||||
return;
|
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
|
// 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].
|
/// See also [ToolsLogAnalyze].
|
||||||
@ProviderFor(ToolsLogAnalyze)
|
@ProviderFor(ToolsLogAnalyze)
|
||||||
final toolsLogAnalyzeProvider =
|
const toolsLogAnalyzeProvider = ToolsLogAnalyzeFamily();
|
||||||
AutoDisposeNotifierProvider<ToolsLogAnalyze, void>.internal(
|
|
||||||
ToolsLogAnalyze.new,
|
|
||||||
name: r'toolsLogAnalyzeProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$toolsLogAnalyzeHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
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: 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
|
// 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: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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:starcitizen_doctor/common/utils/multi_window_manager.dart';
|
||||||
|
|
||||||
|
import 'log_analyze_provider.dart';
|
||||||
|
|
||||||
class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
||||||
const ToolsLogAnalyzeDialogUI({super.key});
|
final MultiWindowAppState appState;
|
||||||
|
|
||||||
|
const ToolsLogAnalyzeDialogUI({super.key, required this.appState});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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(
|
return ScaffoldPage(
|
||||||
header: const PageHeader(
|
|
||||||
title: Text("Log 分析器"),
|
|
||||||
),
|
|
||||||
content: Column(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!context.mounted) 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
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$toolsUIModelHash() => r'af6e6de30c191a7c9a4e2a1c96a688fba6ee086c';
|
String _$toolsUIModelHash() => r'cd72f7833fa5696baf9022d16d10d7951387df7e';
|
||||||
|
|
||||||
/// See also [ToolsUIModel].
|
/// See also [ToolsUIModel].
|
||||||
@ProviderFor(ToolsUIModel)
|
@ProviderFor(ToolsUIModel)
|
||||||
|
@ -17,7 +17,7 @@ dependencies:
|
|||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
flutter_hooks: ^0.21.2
|
flutter_hooks: ^0.21.2
|
||||||
hooks_riverpod: ^2.6.1
|
hooks_riverpod: ^2.6.1
|
||||||
json_annotation: ^4.8.1
|
json_annotation: ^4.9.0
|
||||||
go_router: ^14.0.1
|
go_router: ^14.0.1
|
||||||
window_manager: ^0.4.0
|
window_manager: ^0.4.0
|
||||||
fluent_ui: ^4.8.6
|
fluent_ui: ^4.8.6
|
||||||
@ -67,6 +67,7 @@ dependencies:
|
|||||||
shelf: ^1.4.1
|
shelf: ^1.4.1
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
desktop_multi_window: ^0.2.1
|
desktop_multi_window: ^0.2.1
|
||||||
|
watcher: ^1.1.1
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
http: ^1.1.2
|
http: ^1.1.2
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user