mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-16 13:10:28 +00:00
feat: init YearlyReportUI
This commit is contained in:
parent
9a28257f4a
commit
6ec973144e
@ -104,6 +104,22 @@ class GameLogAnalyzer {
|
|||||||
// 本地库存请求解析
|
// 本地库存请求解析
|
||||||
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
|
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
|
||||||
|
|
||||||
|
// 载具控制解析
|
||||||
|
static final vehicleControlPattern = RegExp(r"granted control token for '([^']+)'\s+\[(\d+)\]");
|
||||||
|
|
||||||
|
/// 公开的日期时间解析方法,供其他模块使用
|
||||||
|
static DateTime? getLogLineDateTime(String line) => _getLogLineDateTime(line);
|
||||||
|
|
||||||
|
/// 公开的日期时间字符串解析方法
|
||||||
|
static String? getLogLineDateTimeString(String line) => _getLogLineDateTimeString(line);
|
||||||
|
|
||||||
|
/// 从载具名称中移除末尾的ID
|
||||||
|
/// 示例: ANVL_Hornet_F7A_Mk2_3467069517923 -> ANVL_Hornet_F7A_Mk2
|
||||||
|
static String removeVehicleId(String vehicleName) {
|
||||||
|
final regex = RegExp(r'_\d+$');
|
||||||
|
return vehicleName.replaceAll(regex, '');
|
||||||
|
}
|
||||||
|
|
||||||
/// 分析整个日志文件
|
/// 分析整个日志文件
|
||||||
///
|
///
|
||||||
/// [logFile] 日志文件
|
/// [logFile] 日志文件
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:isolate';
|
||||||
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
|
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
|
||||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
|
||||||
|
|
||||||
/// 年度报告数据类
|
/// 年度报告数据类
|
||||||
class YearlyReportData {
|
class YearlyReportData {
|
||||||
@ -18,6 +18,13 @@ class YearlyReportData {
|
|||||||
final DateTime? earliestPlayDate; // 年度游玩最早的一天 (05:00及以后)
|
final DateTime? earliestPlayDate; // 年度游玩最早的一天 (05:00及以后)
|
||||||
final DateTime? latestPlayDate; // 年度游玩最晚的一天 (04:00及以前)
|
final DateTime? latestPlayDate; // 年度游玩最晚的一天 (04:00及以前)
|
||||||
|
|
||||||
|
// 游玩时长统计
|
||||||
|
final Duration? longestSession; // 最长单次游玩时长
|
||||||
|
final DateTime? longestSessionDate; // 最长游玩那一天
|
||||||
|
final Duration? shortestSession; // 最短单次游玩时长 (超过5分钟的)
|
||||||
|
final DateTime? shortestSessionDate; // 最短游玩那一天
|
||||||
|
final Duration? averageSessionTime; // 平均单次游玩时长
|
||||||
|
|
||||||
// 载具统计
|
// 载具统计
|
||||||
final int yearlyVehicleDestructionCount; // 年度炸船次数
|
final int yearlyVehicleDestructionCount; // 年度炸船次数
|
||||||
final String? mostDestroyedVehicle; // 年度炸的最多的船
|
final String? mostDestroyedVehicle; // 年度炸的最多的船
|
||||||
@ -25,19 +32,23 @@ class YearlyReportData {
|
|||||||
final String? mostPilotedVehicle; // 年度最爱驾驶的载具
|
final String? mostPilotedVehicle; // 年度最爱驾驶的载具
|
||||||
final int mostPilotedVehicleCount; // 驾驶次数
|
final int mostPilotedVehicleCount; // 驾驶次数
|
||||||
|
|
||||||
// 战斗统计
|
|
||||||
final int yearlyKillCount; // 年度击杀次数
|
|
||||||
final int yearlyDeathCount; // 年度死亡次数
|
|
||||||
|
|
||||||
// 账号统计
|
// 账号统计
|
||||||
final int accountCount; // 账号数量
|
final int accountCount; // 账号数量
|
||||||
final String? mostPlayedAccount; // 游玩最多的账号
|
final String? mostPlayedAccount; // 游玩最多的账号
|
||||||
final int mostPlayedAccountSessionCount; // 游玩最多的账号的会话次数
|
final int mostPlayedAccountSessionCount; // 游玩最多的账号的会话次数
|
||||||
|
|
||||||
|
// 地点统计
|
||||||
|
final List<MapEntry<String, int>> topLocations; // Top 地点访问统计
|
||||||
|
|
||||||
|
// 击杀统计 (K/D)
|
||||||
|
final int yearlyKillCount; // 年度击杀次数
|
||||||
|
final int yearlyDeathCount; // 年度死亡次数
|
||||||
|
final int yearlySelfKillCount; // 年度自杀次数
|
||||||
|
|
||||||
// 详细数据 (用于展示)
|
// 详细数据 (用于展示)
|
||||||
final Map<String, int> vehicleDestructionDetails; // 载具损毁详情
|
|
||||||
final Map<String, int> vehiclePilotedDetails; // 驾驶载具详情
|
final Map<String, int> vehiclePilotedDetails; // 驾驶载具详情
|
||||||
final Map<String, int> accountSessionDetails; // 账号会话详情
|
final Map<String, int> accountSessionDetails; // 账号会话详情
|
||||||
|
final Map<String, int> locationDetails; // 地点访问详情
|
||||||
|
|
||||||
const YearlyReportData({
|
const YearlyReportData({
|
||||||
required this.totalLaunchCount,
|
required this.totalLaunchCount,
|
||||||
@ -49,49 +60,44 @@ class YearlyReportData {
|
|||||||
this.yearlyFirstLaunchTime,
|
this.yearlyFirstLaunchTime,
|
||||||
this.earliestPlayDate,
|
this.earliestPlayDate,
|
||||||
this.latestPlayDate,
|
this.latestPlayDate,
|
||||||
|
this.longestSession,
|
||||||
|
this.longestSessionDate,
|
||||||
|
this.shortestSession,
|
||||||
|
this.shortestSessionDate,
|
||||||
|
this.averageSessionTime,
|
||||||
required this.yearlyVehicleDestructionCount,
|
required this.yearlyVehicleDestructionCount,
|
||||||
this.mostDestroyedVehicle,
|
this.mostDestroyedVehicle,
|
||||||
required this.mostDestroyedVehicleCount,
|
required this.mostDestroyedVehicleCount,
|
||||||
this.mostPilotedVehicle,
|
this.mostPilotedVehicle,
|
||||||
required this.mostPilotedVehicleCount,
|
required this.mostPilotedVehicleCount,
|
||||||
required this.yearlyKillCount,
|
|
||||||
required this.yearlyDeathCount,
|
|
||||||
required this.accountCount,
|
required this.accountCount,
|
||||||
this.mostPlayedAccount,
|
this.mostPlayedAccount,
|
||||||
required this.mostPlayedAccountSessionCount,
|
required this.mostPlayedAccountSessionCount,
|
||||||
required this.vehicleDestructionDetails,
|
required this.topLocations,
|
||||||
|
required this.yearlyKillCount,
|
||||||
|
required this.yearlyDeathCount,
|
||||||
|
required this.yearlySelfKillCount,
|
||||||
required this.vehiclePilotedDetails,
|
required this.vehiclePilotedDetails,
|
||||||
required this.accountSessionDetails,
|
required this.accountSessionDetails,
|
||||||
|
required this.locationDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 将 DateTime 转换为带时区的 ISO 8601 字符串
|
/// 将 DateTime 转换为 UTC 毫秒时间戳
|
||||||
/// 输出格式: 2025-12-17T10:30:00.000+08:00
|
static int? _toUtcTimestamp(DateTime? dateTime) {
|
||||||
static String? _toIso8601WithTimezone(DateTime? dateTime) {
|
|
||||||
if (dateTime == null) return null;
|
if (dateTime == null) return null;
|
||||||
final local = dateTime.toLocal();
|
return dateTime.toUtc().millisecondsSinceEpoch;
|
||||||
final offset = local.timeZoneOffset;
|
|
||||||
final sign = offset.isNegative ? '-' : '+';
|
|
||||||
final hours = offset.inHours.abs().toString().padLeft(2, '0');
|
|
||||||
final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0');
|
|
||||||
// 使用本地时间的 ISO 字符串,然后附加时区偏移
|
|
||||||
final isoString = local.toIso8601String();
|
|
||||||
// 移除可能的 'Z' 后缀(UTC 标记)
|
|
||||||
final baseString = isoString.endsWith('Z') ? isoString.substring(0, isoString.length - 1) : isoString;
|
|
||||||
return '$baseString$sign$hours:$minutes';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 转换为 JSON Map
|
/// 转换为 JSON Map
|
||||||
|
///
|
||||||
|
/// 时间字段使用 UTC 毫秒时间戳 (int),配合 timezoneOffsetMinutes 可在客户端还原本地时间
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final offset = now.timeZoneOffset;
|
final offset = now.timeZoneOffset;
|
||||||
final sign = offset.isNegative ? '-' : '+';
|
|
||||||
final hours = offset.inHours.abs().toString().padLeft(2, '0');
|
|
||||||
final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 元数据
|
// 元数据
|
||||||
'generatedAt': _toIso8601WithTimezone(now),
|
'generatedAtUtc': _toUtcTimestamp(now),
|
||||||
'timezoneOffset': '$sign$hours:$minutes',
|
|
||||||
'timezoneOffsetMinutes': offset.inMinutes,
|
'timezoneOffsetMinutes': offset.inMinutes,
|
||||||
|
|
||||||
// 基础统计
|
// 基础统计
|
||||||
@ -102,10 +108,17 @@ class YearlyReportData {
|
|||||||
'totalCrashCount': totalCrashCount,
|
'totalCrashCount': totalCrashCount,
|
||||||
'yearlyCrashCount': yearlyCrashCount,
|
'yearlyCrashCount': yearlyCrashCount,
|
||||||
|
|
||||||
// 时间统计 (带时区)
|
// 时间统计 (UTC 毫秒时间戳)
|
||||||
'yearlyFirstLaunchTime': _toIso8601WithTimezone(yearlyFirstLaunchTime),
|
'yearlyFirstLaunchTimeUtc': _toUtcTimestamp(yearlyFirstLaunchTime),
|
||||||
'earliestPlayDate': _toIso8601WithTimezone(earliestPlayDate),
|
'earliestPlayDateUtc': _toUtcTimestamp(earliestPlayDate),
|
||||||
'latestPlayDate': _toIso8601WithTimezone(latestPlayDate),
|
'latestPlayDateUtc': _toUtcTimestamp(latestPlayDate),
|
||||||
|
|
||||||
|
// 游玩时长统计
|
||||||
|
'longestSessionMs': longestSession?.inMilliseconds,
|
||||||
|
'longestSessionDateUtc': _toUtcTimestamp(longestSessionDate),
|
||||||
|
'shortestSessionMs': shortestSession?.inMilliseconds,
|
||||||
|
'shortestSessionDateUtc': _toUtcTimestamp(shortestSessionDate),
|
||||||
|
'averageSessionTimeMs': averageSessionTime?.inMilliseconds,
|
||||||
|
|
||||||
// 载具统计
|
// 载具统计
|
||||||
'yearlyVehicleDestructionCount': yearlyVehicleDestructionCount,
|
'yearlyVehicleDestructionCount': yearlyVehicleDestructionCount,
|
||||||
@ -114,28 +127,26 @@ class YearlyReportData {
|
|||||||
'mostPilotedVehicle': mostPilotedVehicle,
|
'mostPilotedVehicle': mostPilotedVehicle,
|
||||||
'mostPilotedVehicleCount': mostPilotedVehicleCount,
|
'mostPilotedVehicleCount': mostPilotedVehicleCount,
|
||||||
|
|
||||||
// 战斗统计
|
|
||||||
'yearlyKillCount': yearlyKillCount,
|
|
||||||
'yearlyDeathCount': yearlyDeathCount,
|
|
||||||
|
|
||||||
// 账号统计
|
// 账号统计
|
||||||
'accountCount': accountCount,
|
'accountCount': accountCount,
|
||||||
'mostPlayedAccount': mostPlayedAccount,
|
'mostPlayedAccount': mostPlayedAccount,
|
||||||
'mostPlayedAccountSessionCount': mostPlayedAccountSessionCount,
|
'mostPlayedAccountSessionCount': mostPlayedAccountSessionCount,
|
||||||
|
|
||||||
|
// 地点统计
|
||||||
|
'topLocations': topLocations.map((e) => {'location': e.key, 'count': e.value}).toList(),
|
||||||
|
|
||||||
|
// 击杀统计
|
||||||
|
'yearlyKillCount': yearlyKillCount,
|
||||||
|
'yearlyDeathCount': yearlyDeathCount,
|
||||||
|
'yearlySelfKillCount': yearlySelfKillCount,
|
||||||
|
|
||||||
// 详细数据
|
// 详细数据
|
||||||
'vehicleDestructionDetails': vehicleDestructionDetails,
|
|
||||||
'vehiclePilotedDetails': vehiclePilotedDetails,
|
'vehiclePilotedDetails': vehiclePilotedDetails,
|
||||||
'accountSessionDetails': accountSessionDetails,
|
'accountSessionDetails': accountSessionDetails,
|
||||||
|
'locationDetails': locationDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 转换为 JSON 字符串
|
|
||||||
String toJsonString() => jsonEncode(toJson());
|
|
||||||
|
|
||||||
/// 转换为 Base64 编码的 JSON 字符串
|
|
||||||
String toJsonBase64() => base64Encode(utf8.encode(toJsonString()));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''YearlyReportData(
|
return '''YearlyReportData(
|
||||||
@ -148,13 +159,15 @@ class YearlyReportData {
|
|||||||
yearlyFirstLaunchTime: $yearlyFirstLaunchTime,
|
yearlyFirstLaunchTime: $yearlyFirstLaunchTime,
|
||||||
earliestPlayDate: $earliestPlayDate,
|
earliestPlayDate: $earliestPlayDate,
|
||||||
latestPlayDate: $latestPlayDate,
|
latestPlayDate: $latestPlayDate,
|
||||||
|
longestSession: $longestSession (on $longestSessionDate),
|
||||||
|
shortestSession: $shortestSession (on $shortestSessionDate),
|
||||||
|
averageSessionTime: $averageSessionTime,
|
||||||
yearlyVehicleDestructionCount: $yearlyVehicleDestructionCount,
|
yearlyVehicleDestructionCount: $yearlyVehicleDestructionCount,
|
||||||
mostDestroyedVehicle: $mostDestroyedVehicle ($mostDestroyedVehicleCount),
|
mostDestroyedVehicle: $mostDestroyedVehicle ($mostDestroyedVehicleCount),
|
||||||
mostPilotedVehicle: $mostPilotedVehicle ($mostPilotedVehicleCount),
|
mostPilotedVehicle: $mostPilotedVehicle ($mostPilotedVehicleCount),
|
||||||
yearlyKillCount: $yearlyKillCount,
|
|
||||||
yearlyDeathCount: $yearlyDeathCount,
|
|
||||||
accountCount: $accountCount,
|
accountCount: $accountCount,
|
||||||
mostPlayedAccount: $mostPlayedAccount ($mostPlayedAccountSessionCount),
|
mostPlayedAccount: $mostPlayedAccount ($mostPlayedAccountSessionCount),
|
||||||
|
topLocations: ${topLocations.take(5).map((e) => '${e.key}: ${e.value}').join(', ')},
|
||||||
)''';
|
)''';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,6 +179,7 @@ class _LogFileStats {
|
|||||||
bool hasCrash = false;
|
bool hasCrash = false;
|
||||||
int killCount = 0;
|
int killCount = 0;
|
||||||
int deathCount = 0;
|
int deathCount = 0;
|
||||||
|
int selfKillCount = 0;
|
||||||
Set<String> playerNames = {};
|
Set<String> playerNames = {};
|
||||||
String? currentPlayerName;
|
String? currentPlayerName;
|
||||||
String? firstPlayerName; // 第一个检测到的玩家名,用于去重
|
String? firstPlayerName; // 第一个检测到的玩家名,用于去重
|
||||||
@ -176,9 +190,11 @@ class _LogFileStats {
|
|||||||
// 驾驶载具: 载具型号 (去除ID后) -> 次数
|
// 驾驶载具: 载具型号 (去除ID后) -> 次数
|
||||||
Map<String, int> vehiclePiloted = {};
|
Map<String, int> vehiclePiloted = {};
|
||||||
|
|
||||||
// 年度内的时间记录
|
// 地点访问: 地点名 -> 次数
|
||||||
List<DateTime> yearlyStartTimes = [];
|
Map<String, int> locationVisits = {};
|
||||||
List<DateTime> yearlyEndTimes = [];
|
|
||||||
|
// 年度内的会话记录
|
||||||
|
List<_SessionInfo> yearlySessions = [];
|
||||||
|
|
||||||
/// 生成用于去重的唯一标识
|
/// 生成用于去重的唯一标识
|
||||||
/// 基于启动时间和第一个玩家名生成
|
/// 基于启动时间和第一个玩家名生成
|
||||||
@ -190,10 +206,20 @@ class _LogFileStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 单次游玩会话信息
|
||||||
|
class _SessionInfo {
|
||||||
|
final DateTime startTime;
|
||||||
|
final DateTime endTime;
|
||||||
|
|
||||||
|
_SessionInfo({required this.startTime, required this.endTime});
|
||||||
|
|
||||||
|
Duration get duration => endTime.difference(startTime);
|
||||||
|
}
|
||||||
|
|
||||||
/// 年度报告分析器
|
/// 年度报告分析器
|
||||||
class YearlyReportAnalyzer {
|
class YearlyReportAnalyzer {
|
||||||
// 复用 GameLogAnalyzer 中的正则表达式和方法
|
// 新版日志格式的正则表达式
|
||||||
static final _characterNamePattern = RegExp(r"name\s+([^-]+)");
|
static final _characterNamePattern = RegExp(r'name\s+(\w+)\s+signedIn');
|
||||||
static final _vehicleDestructionPattern = RegExp(
|
static final _vehicleDestructionPattern = RegExp(
|
||||||
r"Vehicle\s+'([^']+)'.*?" // 载具型号
|
r"Vehicle\s+'([^']+)'.*?" // 载具型号
|
||||||
r"in zone\s+'([^']+)'.*?" // Zone
|
r"in zone\s+'([^']+)'.*?" // Zone
|
||||||
@ -206,6 +232,16 @@ class YearlyReportAnalyzer {
|
|||||||
r"to zone '([^']+)'", // 目标区域
|
r"to zone '([^']+)'", // 目标区域
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Legacy 格式的正则表达式 (旧版日志)
|
||||||
|
static final _legacyCharacterNamePattern = RegExp(r"name\s+([^-]+)");
|
||||||
|
static final _legacyActorDeathPattern = RegExp(
|
||||||
|
r"CActor::Kill: '([^']+)'.*?" // 受害者ID
|
||||||
|
r"in zone '([^']+)'.*?" // 死亡位置区域
|
||||||
|
r"killed by '([^']+)'.*?" // 击杀者ID
|
||||||
|
r"with damage type '([^']+)'", // 伤害类型
|
||||||
|
);
|
||||||
|
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
|
||||||
|
|
||||||
/// 分析单个日志文件
|
/// 分析单个日志文件
|
||||||
static Future<_LogFileStats> _analyzeLogFile(File logFile, int targetYear) async {
|
static Future<_LogFileStats> _analyzeLogFile(File logFile, int targetYear) async {
|
||||||
final stats = _LogFileStats();
|
final stats = _LogFileStats();
|
||||||
@ -231,21 +267,6 @@ class YearlyReportAnalyzer {
|
|||||||
// 更新结束时间 (最后一个有效时间)
|
// 更新结束时间 (最后一个有效时间)
|
||||||
if (lineTime != null) {
|
if (lineTime != null) {
|
||||||
stats.endTime = lineTime;
|
stats.endTime = lineTime;
|
||||||
|
|
||||||
// 记录年度内的时间
|
|
||||||
if (lineTime.year == targetYear) {
|
|
||||||
if (stats.yearlyStartTimes.isEmpty ||
|
|
||||||
stats.yearlyStartTimes.last.difference(lineTime).abs().inMinutes > 30) {
|
|
||||||
// 新的会话开始
|
|
||||||
stats.yearlyStartTimes.add(lineTime);
|
|
||||||
}
|
|
||||||
// 总是更新最后的结束时间
|
|
||||||
if (stats.yearlyEndTimes.isEmpty) {
|
|
||||||
stats.yearlyEndTimes.add(lineTime);
|
|
||||||
} else {
|
|
||||||
stats.yearlyEndTimes[stats.yearlyEndTimes.length - 1] = lineTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测崩溃
|
// 检测崩溃
|
||||||
@ -253,20 +274,21 @@ class YearlyReportAnalyzer {
|
|||||||
stats.hasCrash = true;
|
stats.hasCrash = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测玩家登录
|
// 检测玩家登录 (尝试新版格式,失败则用旧版)
|
||||||
final nameMatch = _characterNamePattern.firstMatch(line);
|
var nameMatch = _characterNamePattern.firstMatch(line);
|
||||||
|
nameMatch ??= _legacyCharacterNamePattern.firstMatch(line);
|
||||||
if (nameMatch != null) {
|
if (nameMatch != null) {
|
||||||
final playerName = nameMatch.group(1)?.trim();
|
final playerName = nameMatch.group(1)?.trim();
|
||||||
if (playerName != null && playerName.isNotEmpty) {
|
if (playerName != null && playerName.isNotEmpty && !playerName.contains(' ')) {
|
||||||
stats.currentPlayerName = playerName;
|
stats.currentPlayerName = playerName;
|
||||||
stats.playerNames.add(playerName);
|
stats.playerNames.add(playerName);
|
||||||
// 记录第一个玩家名用于去重
|
|
||||||
stats.firstPlayerName ??= playerName;
|
stats.firstPlayerName ??= playerName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测载具损毁 (仅年度内)
|
// 年度内的统计
|
||||||
if (lineTime != null && lineTime.year == targetYear) {
|
if (lineTime != null && lineTime.year == targetYear) {
|
||||||
|
// 检测载具损毁
|
||||||
final destructionMatch = _vehicleDestructionPattern.firstMatch(line);
|
final destructionMatch = _vehicleDestructionPattern.firstMatch(line);
|
||||||
if (destructionMatch != null) {
|
if (destructionMatch != null) {
|
||||||
final vehicleModel = destructionMatch.group(1);
|
final vehicleModel = destructionMatch.group(1);
|
||||||
@ -291,29 +313,86 @@ class YearlyReportAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测死亡
|
// 检测死亡 (新版格式)
|
||||||
final deathMatch = _actorDeathPattern.firstMatch(line);
|
var deathMatch = _actorDeathPattern.firstMatch(line);
|
||||||
if (deathMatch != null) {
|
if (deathMatch != null) {
|
||||||
final victimId = deathMatch.group(1)?.trim();
|
final victimId = deathMatch.group(1)?.trim();
|
||||||
|
|
||||||
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
|
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
|
||||||
stats.deathCount++;
|
stats.deathCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测死亡 (旧版格式 - Legacy)
|
||||||
|
final legacyDeathMatch = _legacyActorDeathPattern.firstMatch(line);
|
||||||
|
if (legacyDeathMatch != null) {
|
||||||
|
final victimId = legacyDeathMatch.group(1)?.trim();
|
||||||
|
final killerId = legacyDeathMatch.group(3)?.trim();
|
||||||
|
|
||||||
|
if (victimId != null && stats.currentPlayerName != null) {
|
||||||
|
// 检测自杀
|
||||||
|
if (victimId == killerId) {
|
||||||
|
if (victimId == stats.currentPlayerName) {
|
||||||
|
stats.selfKillCount++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 检测死亡
|
||||||
|
if (victimId == stats.currentPlayerName) {
|
||||||
|
stats.deathCount++;
|
||||||
|
}
|
||||||
|
// 检测击杀
|
||||||
|
if (killerId == stats.currentPlayerName) {
|
||||||
|
stats.killCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测地点访问 (RequestLocationInventory)
|
||||||
|
final locationMatch = _requestLocationInventoryPattern.firstMatch(line);
|
||||||
|
if (locationMatch != null) {
|
||||||
|
final location = locationMatch.group(2)?.trim();
|
||||||
|
if (location != null && location.isNotEmpty) {
|
||||||
|
// 清理地点名称,移除数字ID后缀
|
||||||
|
final cleanLocation = _cleanLocationName(location);
|
||||||
|
stats.locationVisits[cleanLocation] = (stats.locationVisits[cleanLocation] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录会话信息
|
||||||
|
if (stats.startTime != null && stats.endTime != null && stats.startTime!.year == targetYear) {
|
||||||
|
stats.yearlySessions.add(_SessionInfo(startTime: stats.startTime!, endTime: stats.endTime!));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dPrint('[YearlyReportAnalyzer] Error analyzing log file: $e');
|
// Error handled silently in isolate
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 清理地点名称,移除数字ID后缀
|
||||||
|
static String _cleanLocationName(String location) {
|
||||||
|
// 移除末尾的数字ID (如 "_12345678")
|
||||||
|
final cleanPattern = RegExp(r'_\d{6,}$');
|
||||||
|
return location.replaceAll(cleanPattern, '');
|
||||||
|
}
|
||||||
|
|
||||||
/// 生成年度报告
|
/// 生成年度报告
|
||||||
///
|
///
|
||||||
/// [gameInstallPaths] 游戏安装路径列表 (完整路径,如 ["D:/Games/StarCitizen/LIVE", "D:/Games/StarCitizen/PTU"])
|
/// [gameInstallPaths] 游戏安装路径列表 (完整路径,如 ["D:/Games/StarCitizen/LIVE", "D:/Games/StarCitizen/PTU"])
|
||||||
/// [targetYear] 目标年份
|
/// [targetYear] 目标年份
|
||||||
|
///
|
||||||
|
/// 该方法在独立 Isolate 中运行,避免阻塞 UI
|
||||||
static Future<YearlyReportData> generateReport(List<String> gameInstallPaths, int targetYear) async {
|
static Future<YearlyReportData> generateReport(List<String> gameInstallPaths, int targetYear) async {
|
||||||
|
// 在独立 Isolate 中运行以避免阻塞 UI
|
||||||
|
return await Isolate.run(() async {
|
||||||
|
return await _generateReportInIsolate(gameInstallPaths, targetYear);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部方法:在 Isolate 中执行的报告生成逻辑
|
||||||
|
static Future<YearlyReportData> _generateReportInIsolate(List<String> gameInstallPaths, int targetYear) async {
|
||||||
final List<File> allLogFiles = [];
|
final List<File> allLogFiles = [];
|
||||||
|
|
||||||
// 从所有安装路径收集日志文件
|
// 从所有安装路径收集日志文件
|
||||||
@ -322,7 +401,6 @@ class YearlyReportAnalyzer {
|
|||||||
|
|
||||||
// 检查安装目录是否存在
|
// 检查安装目录是否存在
|
||||||
if (!await installDir.exists()) {
|
if (!await installDir.exists()) {
|
||||||
dPrint('[YearlyReportAnalyzer] Install path does not exist: $installPath');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,10 +422,6 @@ class YearlyReportAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dPrint(
|
|
||||||
'[YearlyReportAnalyzer] Found ${allLogFiles.length} log files from ${gameInstallPaths.length} install paths',
|
|
||||||
);
|
|
||||||
|
|
||||||
// 并发分析所有日志文件
|
// 并发分析所有日志文件
|
||||||
final futures = allLogFiles.map((file) => _analyzeLogFile(file, targetYear));
|
final futures = allLogFiles.map((file) => _analyzeLogFile(file, targetYear));
|
||||||
final allStatsRaw = await Future.wait(futures);
|
final allStatsRaw = await Future.wait(futures);
|
||||||
@ -359,18 +433,13 @@ class YearlyReportAnalyzer {
|
|||||||
for (final stats in allStatsRaw) {
|
for (final stats in allStatsRaw) {
|
||||||
final key = stats.uniqueKey;
|
final key = stats.uniqueKey;
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
// 无法生成唯一标识的日志仍然保留
|
|
||||||
allStats.add(stats);
|
allStats.add(stats);
|
||||||
} else if (!seenKeys.contains(key)) {
|
} else if (!seenKeys.contains(key)) {
|
||||||
seenKeys.add(key);
|
seenKeys.add(key);
|
||||||
allStats.add(stats);
|
allStats.add(stats);
|
||||||
} else {
|
|
||||||
dPrint('[YearlyReportAnalyzer] Skipping duplicate log: $key');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dPrint('[YearlyReportAnalyzer] After deduplication: ${allStats.length} unique logs');
|
|
||||||
|
|
||||||
// 合并统计数据
|
// 合并统计数据
|
||||||
int totalLaunchCount = allStats.length;
|
int totalLaunchCount = allStats.length;
|
||||||
Duration totalPlayTime = Duration.zero;
|
Duration totalPlayTime = Duration.zero;
|
||||||
@ -381,12 +450,23 @@ class YearlyReportAnalyzer {
|
|||||||
DateTime? yearlyFirstLaunchTime;
|
DateTime? yearlyFirstLaunchTime;
|
||||||
DateTime? earliestPlayDate;
|
DateTime? earliestPlayDate;
|
||||||
DateTime? latestPlayDate;
|
DateTime? latestPlayDate;
|
||||||
|
|
||||||
|
// 会话时长统计
|
||||||
|
Duration? longestSession;
|
||||||
|
DateTime? longestSessionDate;
|
||||||
|
Duration? shortestSession;
|
||||||
|
DateTime? shortestSessionDate;
|
||||||
|
List<Duration> allSessionDurations = [];
|
||||||
|
|
||||||
|
// K/D 统计
|
||||||
int yearlyKillCount = 0;
|
int yearlyKillCount = 0;
|
||||||
int yearlyDeathCount = 0;
|
int yearlyDeathCount = 0;
|
||||||
|
int yearlySelfKillCount = 0;
|
||||||
|
|
||||||
final Map<String, int> vehicleDestructionDetails = {};
|
final Map<String, int> vehicleDestructionDetails = {};
|
||||||
final Map<String, int> vehiclePilotedDetails = {};
|
final Map<String, int> vehiclePilotedDetails = {};
|
||||||
final Map<String, int> accountSessionDetails = {};
|
final Map<String, int> accountSessionDetails = {};
|
||||||
|
final Map<String, int> locationDetails = {};
|
||||||
|
|
||||||
for (final stats in allStats) {
|
for (final stats in allStats) {
|
||||||
// 累计游玩时长
|
// 累计游玩时长
|
||||||
@ -397,46 +477,57 @@ class YearlyReportAnalyzer {
|
|||||||
// 崩溃统计
|
// 崩溃统计
|
||||||
if (stats.hasCrash) {
|
if (stats.hasCrash) {
|
||||||
totalCrashCount++;
|
totalCrashCount++;
|
||||||
// 检查是否为年度内的崩溃
|
|
||||||
if (stats.endTime != null && stats.endTime!.year == targetYear) {
|
if (stats.endTime != null && stats.endTime!.year == targetYear) {
|
||||||
yearlyCrashCount++;
|
yearlyCrashCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 年度统计
|
// 年度会话统计
|
||||||
for (int i = 0; i < stats.yearlyStartTimes.length; i++) {
|
for (final session in stats.yearlySessions) {
|
||||||
yearlyLaunchCount++;
|
yearlyLaunchCount++;
|
||||||
final startTime = stats.yearlyStartTimes[i];
|
final sessionDuration = session.duration;
|
||||||
final endTime = i < stats.yearlyEndTimes.length ? stats.yearlyEndTimes[i] : startTime;
|
yearlyPlayTime += sessionDuration;
|
||||||
yearlyPlayTime += endTime.difference(startTime);
|
allSessionDurations.add(sessionDuration);
|
||||||
|
|
||||||
// 年度第一次启动时间
|
// 年度第一次启动时间
|
||||||
if (yearlyFirstLaunchTime == null || startTime.isBefore(yearlyFirstLaunchTime)) {
|
if (yearlyFirstLaunchTime == null || session.startTime.isBefore(yearlyFirstLaunchTime)) {
|
||||||
yearlyFirstLaunchTime = startTime;
|
yearlyFirstLaunchTime = session.startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最早游玩的一天 (05:00及以后开始游戏)
|
// 最早游玩的一天 (05:00及以后开始游戏)
|
||||||
if (startTime.hour >= 5) {
|
if (session.startTime.hour >= 5) {
|
||||||
if (earliestPlayDate == null || _timeOfDayIsEarlier(startTime, earliestPlayDate)) {
|
if (earliestPlayDate == null || _timeOfDayIsEarlier(session.startTime, earliestPlayDate)) {
|
||||||
earliestPlayDate = startTime;
|
earliestPlayDate = session.startTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最晚游玩的一天 (04:00及以前结束游戏)
|
// 最晚游玩的一天 (04:00及以前结束游戏)
|
||||||
if (endTime.hour <= 4) {
|
if (session.endTime.hour <= 4) {
|
||||||
if (latestPlayDate == null || _timeOfDayIsLater(endTime, latestPlayDate)) {
|
if (latestPlayDate == null || _timeOfDayIsLater(session.endTime, latestPlayDate)) {
|
||||||
latestPlayDate = endTime;
|
latestPlayDate = session.endTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最长游玩时长
|
||||||
|
if (longestSession == null || sessionDuration > longestSession) {
|
||||||
|
longestSession = sessionDuration;
|
||||||
|
longestSessionDate = session.startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最短游玩时长 (超过5分钟的)
|
||||||
|
if (sessionDuration.inMinutes >= 5) {
|
||||||
|
if (shortestSession == null || sessionDuration < shortestSession) {
|
||||||
|
shortestSession = sessionDuration;
|
||||||
|
shortestSessionDate = session.startTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 累加战斗统计
|
// 合并载具损毁详情 (过滤包含 PU 的载具)
|
||||||
yearlyKillCount += stats.killCount;
|
|
||||||
yearlyDeathCount += stats.deathCount;
|
|
||||||
|
|
||||||
// 合并载具损毁详情
|
|
||||||
for (final entry in stats.vehicleDestruction.entries) {
|
for (final entry in stats.vehicleDestruction.entries) {
|
||||||
vehicleDestructionDetails[entry.key] = (vehicleDestructionDetails[entry.key] ?? 0) + entry.value;
|
if (!entry.key.contains('PU_')) {
|
||||||
|
vehicleDestructionDetails[entry.key] = (vehicleDestructionDetails[entry.key] ?? 0) + entry.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并驾驶载具详情
|
// 合并驾驶载具详情
|
||||||
@ -444,10 +535,36 @@ class YearlyReportAnalyzer {
|
|||||||
vehiclePilotedDetails[entry.key] = (vehiclePilotedDetails[entry.key] ?? 0) + entry.value;
|
vehiclePilotedDetails[entry.key] = (vehiclePilotedDetails[entry.key] ?? 0) + entry.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 累计 K/D
|
||||||
|
yearlyKillCount += stats.killCount;
|
||||||
|
yearlyDeathCount += stats.deathCount;
|
||||||
|
yearlySelfKillCount += stats.selfKillCount;
|
||||||
|
|
||||||
// 合并账号会话详情
|
// 合并账号会话详情
|
||||||
for (final playerName in stats.playerNames) {
|
for (final playerName in stats.playerNames) {
|
||||||
accountSessionDetails[playerName] = (accountSessionDetails[playerName] ?? 0) + 1;
|
if (playerName.length > 16) continue;
|
||||||
|
String targetKey = playerName;
|
||||||
|
// 查找是否存在忽略大小写的相同 key
|
||||||
|
for (final key in accountSessionDetails.keys) {
|
||||||
|
if (key.toLowerCase() == playerName.toLowerCase()) {
|
||||||
|
targetKey = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accountSessionDetails[targetKey] = (accountSessionDetails[targetKey] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 合并地点访问详情
|
||||||
|
for (final entry in stats.locationVisits.entries) {
|
||||||
|
locationDetails[entry.key] = (locationDetails[entry.key] ?? 0) + entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算平均游玩时长
|
||||||
|
Duration? averageSessionTime;
|
||||||
|
if (allSessionDurations.isNotEmpty) {
|
||||||
|
final totalMs = allSessionDurations.fold<int>(0, (sum, d) => sum + d.inMilliseconds);
|
||||||
|
averageSessionTime = Duration(milliseconds: totalMs ~/ allSessionDurations.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算派生统计
|
// 计算派生统计
|
||||||
@ -480,6 +597,10 @@ class YearlyReportAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算 Top 10 地点
|
||||||
|
final sortedLocations = locationDetails.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
|
||||||
|
final topLocations = sortedLocations.take(10).toList();
|
||||||
|
|
||||||
return YearlyReportData(
|
return YearlyReportData(
|
||||||
totalLaunchCount: totalLaunchCount,
|
totalLaunchCount: totalLaunchCount,
|
||||||
totalPlayTime: totalPlayTime,
|
totalPlayTime: totalPlayTime,
|
||||||
@ -490,19 +611,26 @@ class YearlyReportAnalyzer {
|
|||||||
yearlyFirstLaunchTime: yearlyFirstLaunchTime,
|
yearlyFirstLaunchTime: yearlyFirstLaunchTime,
|
||||||
earliestPlayDate: earliestPlayDate,
|
earliestPlayDate: earliestPlayDate,
|
||||||
latestPlayDate: latestPlayDate,
|
latestPlayDate: latestPlayDate,
|
||||||
|
longestSession: longestSession,
|
||||||
|
longestSessionDate: longestSessionDate,
|
||||||
|
shortestSession: shortestSession,
|
||||||
|
shortestSessionDate: shortestSessionDate,
|
||||||
|
averageSessionTime: averageSessionTime,
|
||||||
yearlyVehicleDestructionCount: yearlyVehicleDestructionCount,
|
yearlyVehicleDestructionCount: yearlyVehicleDestructionCount,
|
||||||
mostDestroyedVehicle: mostDestroyedVehicle,
|
mostDestroyedVehicle: mostDestroyedVehicle,
|
||||||
mostDestroyedVehicleCount: mostDestroyedVehicleCount,
|
mostDestroyedVehicleCount: mostDestroyedVehicleCount,
|
||||||
mostPilotedVehicle: mostPilotedVehicle,
|
mostPilotedVehicle: mostPilotedVehicle,
|
||||||
mostPilotedVehicleCount: mostPilotedVehicleCount,
|
mostPilotedVehicleCount: mostPilotedVehicleCount,
|
||||||
yearlyKillCount: yearlyKillCount,
|
|
||||||
yearlyDeathCount: yearlyDeathCount,
|
|
||||||
accountCount: accountSessionDetails.length,
|
accountCount: accountSessionDetails.length,
|
||||||
mostPlayedAccount: mostPlayedAccount,
|
mostPlayedAccount: mostPlayedAccount,
|
||||||
mostPlayedAccountSessionCount: mostPlayedAccountSessionCount,
|
mostPlayedAccountSessionCount: mostPlayedAccountSessionCount,
|
||||||
vehicleDestructionDetails: vehicleDestructionDetails,
|
topLocations: topLocations,
|
||||||
|
yearlyKillCount: yearlyKillCount,
|
||||||
|
yearlyDeathCount: yearlyDeathCount,
|
||||||
|
yearlySelfKillCount: yearlySelfKillCount,
|
||||||
vehiclePilotedDetails: vehiclePilotedDetails,
|
vehiclePilotedDetails: vehiclePilotedDetails,
|
||||||
accountSessionDetails: accountSessionDetails,
|
accountSessionDetails: accountSessionDetails,
|
||||||
|
locationDetails: locationDetails,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@ final class DcbViewerModelProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$dcbViewerModelHash() => r'f0af2a7b4451f746288e2c9565a418af80f58835';
|
String _$dcbViewerModelHash() => r'dfed59de5291e5a19cc481d0115fe91f5bcaf301';
|
||||||
|
|
||||||
abstract class _$DcbViewerModel extends $Notifier<DcbViewerState> {
|
abstract class _$DcbViewerModel extends $Notifier<DcbViewerState> {
|
||||||
DcbViewerState build();
|
DcbViewerState build();
|
||||||
|
|||||||
@ -42,7 +42,7 @@ final class HomeGameLoginUIModelProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$homeGameLoginUIModelHash() =>
|
String _$homeGameLoginUIModelHash() =>
|
||||||
r'217a57f797b37f3467be2e7711f220610e9e67d8';
|
r'd81831f54c6b1e98ea8a1e94b5e6049fe552996f';
|
||||||
|
|
||||||
abstract class _$HomeGameLoginUIModel extends $Notifier<HomeGameLoginState> {
|
abstract class _$HomeGameLoginUIModel extends $Notifier<HomeGameLoginState> {
|
||||||
HomeGameLoginState build();
|
HomeGameLoginState build();
|
||||||
|
|||||||
@ -42,7 +42,7 @@ final class LocalizationUIModelProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$localizationUIModelHash() =>
|
String _$localizationUIModelHash() =>
|
||||||
r'122f9f85da6e112165f4ff88667b45cf3cf3f43e';
|
r'7b398d3b2ddd306ff8f328be39f28200fe8bf49e';
|
||||||
|
|
||||||
abstract class _$LocalizationUIModel extends $Notifier<LocalizationUIState> {
|
abstract class _$LocalizationUIModel extends $Notifier<LocalizationUIState> {
|
||||||
LocalizationUIState build();
|
LocalizationUIState build();
|
||||||
|
|||||||
@ -42,7 +42,7 @@ final class HomePerformanceUIModelProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$homePerformanceUIModelHash() =>
|
String _$homePerformanceUIModelHash() =>
|
||||||
r'4c5c33fe7d85dc8f6bf0d019c1b870d285d594ff';
|
r'ae4771c7804abb1e5ba89bb1687061612f96b46b';
|
||||||
|
|
||||||
abstract class _$HomePerformanceUIModel
|
abstract class _$HomePerformanceUIModel
|
||||||
extends $Notifier<HomePerformanceUIState> {
|
extends $Notifier<HomePerformanceUIState> {
|
||||||
|
|||||||
@ -21,28 +21,84 @@ final Map<String?, String> logAnalyzeSearchTypeMap = {
|
|||||||
"request_location_inventory": S.current.log_analyzer_filter_local_inventory,
|
"request_location_inventory": S.current.log_analyzer_filter_local_inventory,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 日志文件信息
|
||||||
|
class LogFileInfo {
|
||||||
|
final String path;
|
||||||
|
final String displayName;
|
||||||
|
final bool isCurrentLog;
|
||||||
|
|
||||||
|
const LogFileInfo({required this.path, required this.displayName, required this.isCurrentLog});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取可用的日志文件列表
|
||||||
|
Future<List<LogFileInfo>> getAvailableLogFiles(String gameInstallPath) async {
|
||||||
|
final List<LogFileInfo> logFiles = [];
|
||||||
|
|
||||||
|
if (gameInstallPath.isEmpty) return logFiles;
|
||||||
|
|
||||||
|
// 添加当前 Game.log
|
||||||
|
final currentLogFile = File('$gameInstallPath/Game.log');
|
||||||
|
if (await currentLogFile.exists()) {
|
||||||
|
logFiles.add(LogFileInfo(path: currentLogFile.path, displayName: 'Game.log (当前)', isCurrentLog: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 logbackups 目录中的日志文件
|
||||||
|
final logBackupsDir = Directory('$gameInstallPath/logbackups');
|
||||||
|
if (await logBackupsDir.exists()) {
|
||||||
|
final entities = await logBackupsDir.list().toList();
|
||||||
|
// 按文件名排序(通常包含时间戳,降序排列显示最新的在前)
|
||||||
|
entities.sort((a, b) => b.path.compareTo(a.path));
|
||||||
|
|
||||||
|
for (final entity in entities) {
|
||||||
|
if (entity is File && entity.path.endsWith('.log')) {
|
||||||
|
final fileName = entity.path.split(Platform.pathSeparator).last;
|
||||||
|
logFiles.add(LogFileInfo(path: entity.path, displayName: fileName, isCurrentLog: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return logFiles;
|
||||||
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
||||||
@override
|
@override
|
||||||
Future<List<LogAnalyzeLineData>> build(String gameInstallPath, bool listSortReverse) async {
|
Future<List<LogAnalyzeLineData>> build(
|
||||||
final logFile = File("$gameInstallPath/Game.log");
|
String gameInstallPath,
|
||||||
|
bool listSortReverse, {
|
||||||
|
String? selectedLogFile,
|
||||||
|
}) async {
|
||||||
|
// 确定要分析的日志文件
|
||||||
|
final String logFilePath;
|
||||||
|
if (selectedLogFile != null && selectedLogFile.isNotEmpty) {
|
||||||
|
logFilePath = selectedLogFile;
|
||||||
|
} else {
|
||||||
|
logFilePath = "$gameInstallPath/Game.log";
|
||||||
|
}
|
||||||
|
|
||||||
|
final logFile = File(logFilePath);
|
||||||
debugPrint("[ToolsLogAnalyze] logFile: ${logFile.absolute.path}");
|
debugPrint("[ToolsLogAnalyze] logFile: ${logFile.absolute.path}");
|
||||||
|
|
||||||
if (gameInstallPath.isEmpty || !(await logFile.exists())) {
|
if (gameInstallPath.isEmpty || !(await logFile.exists())) {
|
||||||
return [const LogAnalyzeLineData(type: "error", title: "未找到日志文件")];
|
return [const LogAnalyzeLineData(type: "error", title: "未找到日志文件")];
|
||||||
}
|
}
|
||||||
|
|
||||||
state = const AsyncData([]);
|
state = const AsyncData([]);
|
||||||
_launchLogAnalyze(logFile);
|
_launchLogAnalyze(logFile, selectedLogFile == null);
|
||||||
return state.value ?? [];
|
return state.value ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _launchLogAnalyze(File logFile) async {
|
void _launchLogAnalyze(File logFile, bool enableWatch) async {
|
||||||
// 使用新的 GameLogAnalyzer 工具类
|
// 使用新的 GameLogAnalyzer 工具类
|
||||||
final result = await GameLogAnalyzer.analyzeLogFile(logFile);
|
final result = await GameLogAnalyzer.analyzeLogFile(logFile);
|
||||||
final (results, _) = result;
|
final (results, _) = result;
|
||||||
|
|
||||||
_setResult(results);
|
_setResult(results);
|
||||||
|
|
||||||
_startListenFile(logFile);
|
// 只有当前 Game.log 才需要监听变化
|
||||||
|
if (enableWatch) {
|
||||||
|
_startListenFile(logFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复调用
|
// 避免重复调用
|
||||||
@ -60,7 +116,7 @@ class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
|||||||
debugPrint("[ToolsLogAnalyze] logFile change: ${change.type}");
|
debugPrint("[ToolsLogAnalyze] logFile change: ${change.type}");
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
case ChangeType.MODIFY:
|
case ChangeType.MODIFY:
|
||||||
return _launchLogAnalyze(logFile);
|
return _launchLogAnalyze(logFile, true);
|
||||||
case ChangeType.ADD:
|
case ChangeType.ADD:
|
||||||
case ChangeType.REMOVE:
|
case ChangeType.REMOVE:
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
|
|||||||
@ -16,7 +16,7 @@ final class ToolsLogAnalyzeProvider
|
|||||||
extends $AsyncNotifierProvider<ToolsLogAnalyze, List<LogAnalyzeLineData>> {
|
extends $AsyncNotifierProvider<ToolsLogAnalyze, List<LogAnalyzeLineData>> {
|
||||||
const ToolsLogAnalyzeProvider._({
|
const ToolsLogAnalyzeProvider._({
|
||||||
required ToolsLogAnalyzeFamily super.from,
|
required ToolsLogAnalyzeFamily super.from,
|
||||||
required (String, bool) super.argument,
|
required (String, bool, {String? selectedLogFile}) super.argument,
|
||||||
}) : super(
|
}) : super(
|
||||||
retry: null,
|
retry: null,
|
||||||
name: r'toolsLogAnalyzeProvider',
|
name: r'toolsLogAnalyzeProvider',
|
||||||
@ -50,7 +50,7 @@ final class ToolsLogAnalyzeProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$toolsLogAnalyzeHash() => r'4c1aea03394e5c5641b2eb40a31d37892bb978bf';
|
String _$toolsLogAnalyzeHash() => r'7fa6e068a3ee33fbf1eb0c718035eececd625ece';
|
||||||
|
|
||||||
final class ToolsLogAnalyzeFamily extends $Family
|
final class ToolsLogAnalyzeFamily extends $Family
|
||||||
with
|
with
|
||||||
@ -59,7 +59,7 @@ final class ToolsLogAnalyzeFamily extends $Family
|
|||||||
AsyncValue<List<LogAnalyzeLineData>>,
|
AsyncValue<List<LogAnalyzeLineData>>,
|
||||||
List<LogAnalyzeLineData>,
|
List<LogAnalyzeLineData>,
|
||||||
FutureOr<List<LogAnalyzeLineData>>,
|
FutureOr<List<LogAnalyzeLineData>>,
|
||||||
(String, bool)
|
(String, bool, {String? selectedLogFile})
|
||||||
> {
|
> {
|
||||||
const ToolsLogAnalyzeFamily._()
|
const ToolsLogAnalyzeFamily._()
|
||||||
: super(
|
: super(
|
||||||
@ -70,11 +70,18 @@ final class ToolsLogAnalyzeFamily extends $Family
|
|||||||
isAutoDispose: true,
|
isAutoDispose: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
ToolsLogAnalyzeProvider call(String gameInstallPath, bool listSortReverse) =>
|
ToolsLogAnalyzeProvider call(
|
||||||
ToolsLogAnalyzeProvider._(
|
String gameInstallPath,
|
||||||
argument: (gameInstallPath, listSortReverse),
|
bool listSortReverse, {
|
||||||
from: this,
|
String? selectedLogFile,
|
||||||
);
|
}) => ToolsLogAnalyzeProvider._(
|
||||||
|
argument: (
|
||||||
|
gameInstallPath,
|
||||||
|
listSortReverse,
|
||||||
|
selectedLogFile: selectedLogFile,
|
||||||
|
),
|
||||||
|
from: this,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => r'toolsLogAnalyzeProvider';
|
String toString() => r'toolsLogAnalyzeProvider';
|
||||||
@ -82,18 +89,24 @@ final class ToolsLogAnalyzeFamily extends $Family
|
|||||||
|
|
||||||
abstract class _$ToolsLogAnalyze
|
abstract class _$ToolsLogAnalyze
|
||||||
extends $AsyncNotifier<List<LogAnalyzeLineData>> {
|
extends $AsyncNotifier<List<LogAnalyzeLineData>> {
|
||||||
late final _$args = ref.$arg as (String, bool);
|
late final _$args = ref.$arg as (String, bool, {String? selectedLogFile});
|
||||||
String get gameInstallPath => _$args.$1;
|
String get gameInstallPath => _$args.$1;
|
||||||
bool get listSortReverse => _$args.$2;
|
bool get listSortReverse => _$args.$2;
|
||||||
|
String? get selectedLogFile => _$args.selectedLogFile;
|
||||||
|
|
||||||
FutureOr<List<LogAnalyzeLineData>> build(
|
FutureOr<List<LogAnalyzeLineData>> build(
|
||||||
String gameInstallPath,
|
String gameInstallPath,
|
||||||
bool listSortReverse,
|
bool listSortReverse, {
|
||||||
);
|
String? selectedLogFile,
|
||||||
|
});
|
||||||
@$mustCallSuper
|
@$mustCallSuper
|
||||||
@override
|
@override
|
||||||
void runBuild() {
|
void runBuild() {
|
||||||
final created = build(_$args.$1, _$args.$2);
|
final created = build(
|
||||||
|
_$args.$1,
|
||||||
|
_$args.$2,
|
||||||
|
selectedLogFile: _$args.selectedLogFile,
|
||||||
|
);
|
||||||
final ref =
|
final ref =
|
||||||
this.ref
|
this.ref
|
||||||
as $Ref<
|
as $Ref<
|
||||||
|
|||||||
@ -16,7 +16,26 @@ class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedPath = useState<String?>(appState.gameInstallPaths.firstOrNull);
|
final selectedPath = useState<String?>(appState.gameInstallPaths.firstOrNull);
|
||||||
final listSortReverse = useState<bool>(false);
|
final listSortReverse = useState<bool>(false);
|
||||||
final provider = toolsLogAnalyzeProvider(selectedPath.value ?? "", listSortReverse.value);
|
final selectedLogFile = useState<String?>(null); // null 表示使用当前 Game.log
|
||||||
|
final availableLogFiles = useState<List<LogFileInfo>>([]);
|
||||||
|
|
||||||
|
// 加载可用的日志文件列表
|
||||||
|
useEffect(() {
|
||||||
|
if (selectedPath.value != null) {
|
||||||
|
getAvailableLogFiles(selectedPath.value!).then((files) {
|
||||||
|
availableLogFiles.value = files;
|
||||||
|
// 重置选择为当前日志
|
||||||
|
selectedLogFile.value = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [selectedPath.value]);
|
||||||
|
|
||||||
|
final provider = toolsLogAnalyzeProvider(
|
||||||
|
selectedPath.value ?? "",
|
||||||
|
listSortReverse.value,
|
||||||
|
selectedLogFile: selectedLogFile.value,
|
||||||
|
);
|
||||||
final logResp = ref.watch(provider);
|
final logResp = ref.watch(provider);
|
||||||
final searchText = useState<String>("");
|
final searchText = useState<String>("");
|
||||||
final searchType = useState<String?>(null);
|
final searchType = useState<String?>(null);
|
||||||
@ -38,12 +57,12 @@ class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
|||||||
value: selectedPath.value,
|
value: selectedPath.value,
|
||||||
items: [
|
items: [
|
||||||
for (final path in appState.gameInstallPaths)
|
for (final path in appState.gameInstallPaths)
|
||||||
ComboBoxItem<String>(
|
ComboBoxItem<String>(value: path, child: Text(path)),
|
||||||
value: path,
|
|
||||||
child: Text(path),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
onChanged: (value) => selectedPath.value = value,
|
onChanged: (value) {
|
||||||
|
selectedPath.value = value;
|
||||||
|
selectedLogFile.value = null; // 重置日志文件选择
|
||||||
|
},
|
||||||
placeholder: Text(S.current.log_analyzer_select_game_path),
|
placeholder: Text(S.current.log_analyzer_select_game_path),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -55,13 +74,50 @@ class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
|||||||
child: const Icon(FluentIcons.refresh),
|
child: const Icon(FluentIcons.refresh),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// 重新加载日志文件列表
|
||||||
|
if (selectedPath.value != null) {
|
||||||
|
getAvailableLogFiles(selectedPath.value!).then((files) {
|
||||||
|
availableLogFiles.value = files;
|
||||||
|
});
|
||||||
|
}
|
||||||
ref.invalidate(provider);
|
ref.invalidate(provider);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
// 日志文件选择器
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text("日志文件:"),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: ComboBox<String?>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: selectedLogFile.value,
|
||||||
|
items: [
|
||||||
|
for (final logFile in availableLogFiles.value)
|
||||||
|
ComboBoxItem<String?>(
|
||||||
|
value: logFile.isCurrentLog ? null : logFile.path,
|
||||||
|
child: Text(
|
||||||
|
logFile.displayName,
|
||||||
|
style: logFile.isCurrentLog ? const TextStyle(fontWeight: FontWeight.bold) : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
selectedLogFile.value = value;
|
||||||
|
},
|
||||||
|
placeholder: const Text("选择日志文件"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
// 搜索,筛选
|
// 搜索,筛选
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
@ -70,10 +126,7 @@ class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
|||||||
// 输入框
|
// 输入框
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormBox(
|
child: TextFormBox(
|
||||||
prefix: Padding(
|
prefix: Padding(padding: const EdgeInsets.only(left: 12), child: Icon(FluentIcons.search)),
|
||||||
padding: const EdgeInsets.only(left: 12),
|
|
||||||
child: Icon(FluentIcons.search),
|
|
||||||
),
|
|
||||||
placeholder: S.current.log_analyzer_search_placeholder,
|
placeholder: S.current.log_analyzer_search_placeholder,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
searchText.value = value.trim();
|
searchText.value = value.trim();
|
||||||
@ -88,10 +141,7 @@ class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
|||||||
value: searchType.value,
|
value: searchType.value,
|
||||||
placeholder: Text(S.current.log_analyzer_filter_all),
|
placeholder: Text(S.current.log_analyzer_filter_all),
|
||||||
items: logAnalyzeSearchTypeMap.entries
|
items: logAnalyzeSearchTypeMap.entries
|
||||||
.map((e) => ComboBoxItem<String>(
|
.map((e) => ComboBoxItem<String>(value: e.key, child: Text(e.value)))
|
||||||
value: e.key,
|
|
||||||
child: Text(e.value),
|
|
||||||
))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
searchType.value = value;
|
searchType.value = value;
|
||||||
@ -103,7 +153,9 @@ class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle: listSortReverse.value ? 3.14 : 0, child: const Icon(FluentIcons.sort_lines)),
|
angle: listSortReverse.value ? 3.14 : 0,
|
||||||
|
child: const Icon(FluentIcons.sort_lines),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
listSortReverse.value = !listSortReverse.value;
|
listSortReverse.value = !listSortReverse.value;
|
||||||
@ -116,95 +168,79 @@ class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 14),
|
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(bottom: BorderSide(color: Colors.white.withValues(alpha: 0.1), width: 1)),
|
||||||
bottom: BorderSide(
|
|
||||||
color: Colors.white.withValues(alpha: 0.1),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// log analyze result
|
// log analyze result
|
||||||
if (!logResp.hasValue)
|
if (!logResp.hasValue)
|
||||||
Expanded(
|
Expanded(child: Center(child: ProgressRing()))
|
||||||
child: Center(
|
|
||||||
child: ProgressRing(),
|
|
||||||
))
|
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: listCtrl,
|
controller: listCtrl,
|
||||||
itemCount: logResp.value!.length,
|
itemCount: logResp.value!.length,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final item = logResp.value![index];
|
final item = logResp.value![index];
|
||||||
if (searchText.value.isNotEmpty) {
|
if (searchText.value.isNotEmpty) {
|
||||||
// 搜索
|
// 搜索
|
||||||
if (!item.toString().contains(searchText.value)) {
|
if (!item.toString().contains(searchText.value)) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (searchType.value != null) {
|
||||||
if (searchType.value != null) {
|
if (item.type != searchType.value) {
|
||||||
if (item.type != searchType.value) {
|
return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
}
|
||||||
}
|
}
|
||||||
}
|
return Padding(
|
||||||
return Padding(
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
child: SelectionArea(
|
||||||
child: SelectionArea(
|
child: Container(
|
||||||
child: Container(
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: _getBackgroundColor(item.type),
|
||||||
color: _getBackgroundColor(item.type),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderRadius: BorderRadius.circular(8),
|
),
|
||||||
),
|
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),
|
||||||
padding: EdgeInsets.symmetric(
|
child: Column(
|
||||||
vertical: 8,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
horizontal: 10,
|
children: [
|
||||||
),
|
Row(
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
_getIconWidget(item.type),
|
||||||
children: [
|
const SizedBox(width: 10),
|
||||||
Row(
|
Expanded(
|
||||||
children: [
|
child: Text.rich(
|
||||||
_getIconWidget(item.type),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text.rich(
|
|
||||||
TextSpan(children: [
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: item.title,
|
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.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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
))
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import 'package:xml/xml.dart';
|
|||||||
|
|
||||||
import 'dialogs/hosts_booster_dialog_ui.dart';
|
import 'dialogs/hosts_booster_dialog_ui.dart';
|
||||||
import 'dialogs/rsi_launcher_enhance_dialog_ui.dart';
|
import 'dialogs/rsi_launcher_enhance_dialog_ui.dart';
|
||||||
|
import 'yearly_report_ui/yearly_report_ui.dart';
|
||||||
|
|
||||||
part 'tools_ui_model.g.dart';
|
part 'tools_ui_model.g.dart';
|
||||||
|
|
||||||
@ -80,6 +81,23 @@ class ToolsUIModel extends _$ToolsUIModel {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 2025 年度报告入口 - 2026年1月20日前显示
|
||||||
|
final deadline = DateTime(2026, 1, 20);
|
||||||
|
if (DateTime.now().isBefore(deadline)) {
|
||||||
|
items.insert(
|
||||||
|
0,
|
||||||
|
ToolsItemData(
|
||||||
|
"yearly_report",
|
||||||
|
"2025 年度报告(限时)",
|
||||||
|
"查看您在2025年的星际公民游玩统计,数据来自本地 log ,请确保在常用电脑上查看。",
|
||||||
|
const Icon(FontAwesomeIcons.star, size: 22),
|
||||||
|
onTap: () async {
|
||||||
|
_openYearlyReport(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
items.add(await _addP4kCard(context));
|
items.add(await _addP4kCard(context));
|
||||||
items.addAll([
|
items.addAll([
|
||||||
@ -747,6 +765,17 @@ class ToolsUIModel extends _$ToolsUIModel {
|
|||||||
appGlobalState,
|
appGlobalState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openYearlyReport(BuildContext context) {
|
||||||
|
if (state.scInstallPaths.isEmpty) {
|
||||||
|
showToast(context, S.current.tools_action_info_valid_game_directory_needed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).push(FluentPageRoute(builder: (context) => YearlyReportUI(gameInstallPaths: state.scInstallPaths)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 图形渲染器切换对话框
|
/// 图形渲染器切换对话框
|
||||||
|
|||||||
@ -41,7 +41,7 @@ final class ToolsUIModelProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$toolsUIModelHash() => r'b0fefd36bd8f1e23fdd6123d487f73d78e40ad06';
|
String _$toolsUIModelHash() => r'a801ad7f4ac2a45a2fa6872c1c004b83d09a3dca';
|
||||||
|
|
||||||
abstract class _$ToolsUIModel extends $Notifier<ToolsUIState> {
|
abstract class _$ToolsUIModel extends $Notifier<ToolsUIState> {
|
||||||
ToolsUIState build();
|
ToolsUIState build();
|
||||||
|
|||||||
1415
lib/ui/tools/yearly_report_ui/yearly_report_ui.dart
Normal file
1415
lib/ui/tools/yearly_report_ui/yearly_report_ui.dart
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user