feat: YearlyReportUI Update

This commit is contained in:
xkeyC 2025-12-17 16:49:58 +08:00
parent 6ec973144e
commit 70c47a8b85
2 changed files with 842 additions and 327 deletions

View File

@ -45,6 +45,20 @@ class YearlyReportData {
final int yearlyDeathCount; //
final int yearlySelfKillCount; //
//
final int? mostPlayedMonth; // (1-12)
final int mostPlayedMonthCount; //
final int? leastPlayedMonth; // (1-12, )
final int leastPlayedMonthCount; //
// /线
final int longestPlayStreak; //
final DateTime? playStreakStartDate; //
final DateTime? playStreakEndDate; //
final int longestOfflineStreak; // 线
final DateTime? offlineStreakStartDate; // 线
final DateTime? offlineStreakEndDate; // 线
// ()
final Map<String, int> vehiclePilotedDetails; //
final Map<String, int> accountSessionDetails; //
@ -77,6 +91,16 @@ class YearlyReportData {
required this.yearlyKillCount,
required this.yearlyDeathCount,
required this.yearlySelfKillCount,
this.mostPlayedMonth,
required this.mostPlayedMonthCount,
this.leastPlayedMonth,
required this.leastPlayedMonthCount,
required this.longestPlayStreak,
this.playStreakStartDate,
this.playStreakEndDate,
required this.longestOfflineStreak,
this.offlineStreakStartDate,
this.offlineStreakEndDate,
required this.vehiclePilotedDetails,
required this.accountSessionDetails,
required this.locationDetails,
@ -140,6 +164,20 @@ class YearlyReportData {
'yearlyDeathCount': yearlyDeathCount,
'yearlySelfKillCount': yearlySelfKillCount,
//
'mostPlayedMonth': mostPlayedMonth,
'mostPlayedMonthCount': mostPlayedMonthCount,
'leastPlayedMonth': leastPlayedMonth,
'leastPlayedMonthCount': leastPlayedMonthCount,
// /线
'longestPlayStreak': longestPlayStreak,
'playStreakStartDateUtc': _toUtcTimestamp(playStreakStartDate),
'playStreakEndDateUtc': _toUtcTimestamp(playStreakEndDate),
'longestOfflineStreak': longestOfflineStreak,
'offlineStreakStartDateUtc': _toUtcTimestamp(offlineStreakStartDate),
'offlineStreakEndDateUtc': _toUtcTimestamp(offlineStreakEndDate),
//
'vehiclePilotedDetails': vehiclePilotedDetails,
'accountSessionDetails': accountSessionDetails,
@ -193,6 +231,9 @@ class _LogFileStats {
// 访: ->
Map<String, int> locationVisits = {};
// ( 2s )
DateTime? _lastDeathTime;
//
List<_SessionInfo> yearlySessions = [];
@ -279,9 +320,17 @@ class YearlyReportAnalyzer {
nameMatch ??= _legacyCharacterNamePattern.firstMatch(line);
if (nameMatch != null) {
final playerName = nameMatch.group(1)?.trim();
if (playerName != null && playerName.isNotEmpty && !playerName.contains(' ')) {
if (playerName != null &&
playerName.isNotEmpty &&
!playerName.contains(' ') &&
!playerName.contains('/') &&
!playerName.contains(r'\') &&
!playerName.contains('.')) {
stats.currentPlayerName = playerName;
// ()
if (!stats.playerNames.any((n) => n.toLowerCase() == playerName.toLowerCase())) {
stats.playerNames.add(playerName);
}
stats.firstPlayerName ??= playerName;
}
}
@ -318,7 +367,11 @@ class YearlyReportAnalyzer {
if (deathMatch != null) {
final victimId = deathMatch.group(1)?.trim();
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
// (2)
if (stats._lastDeathTime == null || lineTime.difference(stats._lastDeathTime!).abs().inSeconds > 2) {
stats.deathCount++;
stats._lastDeathTime = lineTime;
}
}
}
@ -329,17 +382,34 @@ class YearlyReportAnalyzer {
final killerId = legacyDeathMatch.group(3)?.trim();
if (victimId != null && stats.currentPlayerName != null) {
bool isRecent =
stats._lastDeathTime != null && lineTime.difference(stats._lastDeathTime!).abs().inSeconds <= 2;
//
if (victimId == killerId) {
if (victimId == stats.currentPlayerName) {
if (isRecent) {
// ()
// deathCount++退 selfKillCount
if (stats.deathCount > 0) stats.deathCount--;
stats.selfKillCount++;
//
stats._lastDeathTime = lineTime;
} else {
stats.selfKillCount++;
stats._lastDeathTime = lineTime;
}
}
} else {
//
// ()
if (victimId == stats.currentPlayerName) {
// ()
if (!isRecent) {
stats.deathCount++;
stats._lastDeathTime = lineTime;
}
//
}
// ()
if (killerId == stats.currentPlayerName) {
stats.killCount++;
}
@ -601,6 +671,90 @@ class YearlyReportAnalyzer {
final sortedLocations = locationDetails.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
final topLocations = sortedLocations.take(10).toList();
//
final Map<int, int> monthlyPlayCount = {};
final Set<DateTime> playDates = {}; // ()
for (final stats in allStats) {
for (final session in stats.yearlySessions) {
final month = session.startTime.month;
monthlyPlayCount[month] = (monthlyPlayCount[month] ?? 0) + 1;
// ()
playDates.add(DateTime(session.startTime.year, session.startTime.month, session.startTime.day));
}
}
int? mostPlayedMonth;
int mostPlayedMonthCount = 0;
int? leastPlayedMonth;
int leastPlayedMonthCount = 0;
if (monthlyPlayCount.isNotEmpty) {
//
for (final entry in monthlyPlayCount.entries) {
if (entry.value > mostPlayedMonthCount) {
mostPlayedMonth = entry.key;
mostPlayedMonthCount = entry.value;
}
}
// ()
leastPlayedMonthCount = monthlyPlayCount.values.first;
for (final entry in monthlyPlayCount.entries) {
if (entry.value <= leastPlayedMonthCount) {
leastPlayedMonth = entry.key;
leastPlayedMonthCount = entry.value;
}
}
}
// 线
int longestPlayStreak = 0;
DateTime? playStreakStartDate;
DateTime? playStreakEndDate;
int longestOfflineStreak = 0;
DateTime? offlineStreakStartDate;
DateTime? offlineStreakEndDate;
if (playDates.isNotEmpty) {
//
final sortedDates = playDates.toList()..sort();
//
int currentStreak = 1;
DateTime streakStart = sortedDates.first;
for (int i = 1; i < sortedDates.length; i++) {
final diff = sortedDates[i].difference(sortedDates[i - 1]).inDays;
if (diff == 1) {
currentStreak++;
} else {
if (currentStreak > longestPlayStreak) {
longestPlayStreak = currentStreak;
playStreakStartDate = streakStart;
playStreakEndDate = sortedDates[i - 1];
}
currentStreak = 1;
streakStart = sortedDates[i];
}
}
//
if (currentStreak > longestPlayStreak) {
longestPlayStreak = currentStreak;
playStreakStartDate = streakStart;
playStreakEndDate = sortedDates.last;
}
// 线 ()
for (int i = 1; i < sortedDates.length; i++) {
final gapDays = sortedDates[i].difference(sortedDates[i - 1]).inDays - 1;
if (gapDays > longestOfflineStreak) {
longestOfflineStreak = gapDays;
offlineStreakStartDate = sortedDates[i - 1].add(const Duration(days: 1));
offlineStreakEndDate = sortedDates[i].subtract(const Duration(days: 1));
}
}
}
return YearlyReportData(
totalLaunchCount: totalLaunchCount,
totalPlayTime: totalPlayTime,
@ -628,6 +782,16 @@ class YearlyReportAnalyzer {
yearlyKillCount: yearlyKillCount,
yearlyDeathCount: yearlyDeathCount,
yearlySelfKillCount: yearlySelfKillCount,
mostPlayedMonth: mostPlayedMonth,
mostPlayedMonthCount: mostPlayedMonthCount,
leastPlayedMonth: leastPlayedMonth,
leastPlayedMonthCount: leastPlayedMonthCount,
longestPlayStreak: longestPlayStreak,
playStreakStartDate: playStreakStartDate,
playStreakEndDate: playStreakEndDate,
longestOfflineStreak: longestOfflineStreak,
offlineStreakStartDate: offlineStreakStartDate,
offlineStreakEndDate: offlineStreakEndDate,
vehiclePilotedDetails: vehiclePilotedDetails,
accountSessionDetails: accountSessionDetails,
locationDetails: locationDetails,

View File

@ -29,7 +29,7 @@ class YearlyReportUI extends HookConsumerWidget {
return makeDefaultPage(
context,
title: "2025 年度报告",
title: "星际公民 2025 年度报告",
useBodyContainer: true,
content: Column(
children: [
@ -233,6 +233,10 @@ class YearlyReportUI extends HookConsumerWidget {
_buildPlayTimePage(context, data),
//
_buildSessionStatsPage(context, data),
//
_buildMonthlyStatsPage(context, data),
// /线
_buildStreakStatsPage(context, data),
//
_buildCrashCountPage(context, data),
// (K/D)
@ -375,13 +379,15 @@ class YearlyReportUI extends HookConsumerWidget {
),
),
const SizedBox(height: 32),
//
// -
FadeInUp(
delay: const Duration(milliseconds: 600),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
SizedBox(
width: 100,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
@ -400,8 +406,11 @@ class YearlyReportUI extends HookConsumerWidget {
],
),
),
),
const SizedBox(width: 16),
Container(
SizedBox(
width: 100,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
@ -420,8 +429,11 @@ class YearlyReportUI extends HookConsumerWidget {
],
),
),
),
const SizedBox(width: 16),
Container(
SizedBox(
width: 100,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
@ -440,6 +452,7 @@ class YearlyReportUI extends HookConsumerWidget {
],
),
),
),
],
),
),
@ -653,15 +666,22 @@ class YearlyReportUI extends HookConsumerWidget {
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
final newOffset = scrollController.offset + event.scrollDelta.dy;
if (newOffset >= scrollController.position.minScrollExtent &&
newOffset <= scrollController.position.maxScrollExtent) {
final canScroll =
newOffset >= scrollController.position.minScrollExtent &&
newOffset <= scrollController.position.maxScrollExtent;
if (canScroll) {
scrollController.jumpTo(newOffset);
}
//
}
},
behavior: HitTestBehavior.opaque,
child: NotificationListener<ScrollNotification>(
onNotification: (_) => true, //
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: sortedVehicles.map((vehicle) {
@ -705,6 +725,7 @@ class YearlyReportUI extends HookConsumerWidget {
),
),
),
),
],
),
),
@ -873,102 +894,111 @@ class YearlyReportUI extends HookConsumerWidget {
child: Text("游玩时长详情", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
),
const SizedBox(height: 32),
//
//
FadeInUp(
delay: const Duration(milliseconds: 400),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//
Flexible(
child: Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.chartLine, size: 20, color: Colors.blue),
const SizedBox(width: 12),
Text("平均每次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
Icon(FontAwesomeIcons.chartLine, size: 16, color: Colors.blue),
const SizedBox(width: 8),
Text("平均", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
],
),
const SizedBox(height: 8),
Text(
_formatDuration(data.averageSessionTime),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
],
),
),
),
const SizedBox(height: 16),
const SizedBox(width: 12),
//
FadeInUp(
delay: const Duration(milliseconds: 600),
Flexible(
child: Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.arrowUp, size: 20, color: Colors.green),
const SizedBox(width: 12),
Text("最长一次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
Icon(FontAwesomeIcons.arrowUp, size: 16, color: Colors.green),
const SizedBox(width: 8),
Text("最长", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
],
),
const SizedBox(height: 8),
Text(
_formatDuration(data.longestSession),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.green),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green),
textAlign: TextAlign.center,
),
if (data.longestSessionDate != null)
Text(
"${data.longestSessionDate!.month} ${data.longestSessionDate!.day} ",
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
"${data.longestSessionDate!.month}${data.longestSessionDate!.day}",
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
),
],
),
),
),
const SizedBox(height: 16),
const SizedBox(width: 12),
//
FadeInUp(
delay: const Duration(milliseconds: 800),
Flexible(
child: Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.arrowDown, size: 20, color: Colors.orange),
const SizedBox(width: 12),
Text("最短一次游玩", style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .9))),
Icon(FontAwesomeIcons.arrowDown, size: 16, color: Colors.orange),
const SizedBox(width: 8),
Text("最短", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
],
),
const SizedBox(height: 8),
Text(
_formatDuration(data.shortestSession),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.orange),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.orange),
textAlign: TextAlign.center,
),
if (data.shortestSessionDate != null)
Text(
"${data.shortestSessionDate!.month} ${data.shortestSessionDate!.day} ",
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
"${data.shortestSessionDate!.month}${data.shortestSessionDate!.day}",
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
),
Text("(仅统计超过 5 分钟的游戏)", style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6))),
],
),
),
@ -976,10 +1006,233 @@ class YearlyReportUI extends HookConsumerWidget {
],
),
),
),
),
const SizedBox(height: 16),
FadeInUp(
delay: const Duration(milliseconds: 600),
child: Text(
"(最短仅统计超过 5 分钟的游戏)",
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .5)),
),
),
],
),
),
);
}
///
String _getMonthName(int month) {
return "$month月";
}
Widget _buildMonthlyStatsPage(BuildContext context, YearlyReportData data) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ZoomIn(child: Icon(FontAwesomeIcons.calendarDays, size: 64, color: Colors.blue)),
const SizedBox(height: 32),
FadeInUp(
delay: const Duration(milliseconds: 200),
child: Text("月份统计", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
),
const SizedBox(height: 32),
//
FadeInUp(
delay: const Duration(milliseconds: 400),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//
if (data.mostPlayedMonth != null)
Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.fire, size: 18, color: Colors.orange),
const SizedBox(width: 10),
Text("游玩最多", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
],
),
const SizedBox(height: 12),
Text(
_getMonthName(data.mostPlayedMonth!),
style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.orange),
),
Text(
"启动了 ${data.mostPlayedMonthCount}",
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
),
],
),
),
//
if (data.leastPlayedMonth != null && data.leastPlayedMonth != data.mostPlayedMonth)
Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.snowflake, size: 18, color: Colors.teal),
const SizedBox(width: 10),
Text("游玩最少", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
],
),
const SizedBox(height: 12),
Text(
_getMonthName(data.leastPlayedMonth!),
style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.teal),
),
Text(
"仅启动 ${data.leastPlayedMonthCount}",
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)),
),
],
),
),
],
),
),
],
),
);
}
Widget _buildStreakStatsPage(BuildContext context, YearlyReportData data) {
String formatDateRange(DateTime? start, DateTime? end) {
if (start == null || end == null) return "";
return "${start.month}${start.day}日 - ${end.month}${end.day}";
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ZoomIn(child: Icon(FontAwesomeIcons.fire, size: 64, color: Colors.red)),
const SizedBox(height: 32),
FadeInUp(
delay: const Duration(milliseconds: 200),
child: Text("连续记录", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
),
const SizedBox(height: 32),
//
FadeInUp(
delay: const Duration(milliseconds: 400),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//
if (data.longestPlayStreak > 0)
Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.gamepad, size: 18, color: Colors.green),
const SizedBox(width: 10),
Text("连续游玩", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${data.longestPlayStreak}",
style: TextStyle(fontSize: 42, fontWeight: FontWeight.bold, color: Colors.green),
),
Padding(
padding: const EdgeInsets.only(bottom: 6, left: 4),
child: Text("", style: TextStyle(fontSize: 18, color: Colors.green)),
),
],
),
if (data.playStreakStartDate != null)
Text(
formatDateRange(data.playStreakStartDate, data.playStreakEndDate),
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
),
],
),
),
// 线
if (data.longestOfflineStreak > 0)
Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.bed, size: 18, color: Colors.grey),
const SizedBox(width: 10),
Text("连续离线", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .9))),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${data.longestOfflineStreak}",
style: TextStyle(fontSize: 42, fontWeight: FontWeight.bold, color: Colors.grey),
),
Padding(
padding: const EdgeInsets.only(bottom: 6, left: 4),
child: Text("", style: TextStyle(fontSize: 18, color: Colors.grey)),
),
],
),
if (data.offlineStreakStartDate != null)
Text(
formatDateRange(data.offlineStreakStartDate, data.offlineStreakEndDate),
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
),
],
),
),
],
),
),
],
),
);
}
Widget _buildLocationStatsPage(BuildContext context, YearlyReportData data) {
final scrollController = ScrollController();
if (data.topLocations.isEmpty) {
return Center(
child: Column(
@ -1001,9 +1254,15 @@ class YearlyReportUI extends HookConsumerWidget {
);
}
// 3
final locations = data.topLocations;
final rowCount = 3;
final List<List<MapEntry<String, int>>> rows = List.generate(rowCount, (_) => []);
for (int i = 0; i < locations.length; i++) {
rows[i % rowCount].add(locations[i]);
}
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 48),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -1019,59 +1278,113 @@ class YearlyReportUI extends HookConsumerWidget {
child: Text("基于库存查看记录统计", style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6))),
),
const SizedBox(height: 24),
// Top
...data.topLocations.asMap().entries.map((entry) {
final index = entry.key;
final location = entry.value;
final isTop3 = index < 3;
return FadeInUp(
delay: Duration(milliseconds: 400 + index * 100),
child: Container(
width: 350,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: const EdgeInsets.symmetric(vertical: 4),
//
FadeInUp(
delay: const Duration(milliseconds: 400),
child: SizedBox(
height: 180,
width: double.infinity,
child: GestureDetector(
onVerticalDragUpdate: (_) {}, //
child: Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
//
GestureBinding.instance.pointerSignalResolver.register(event, (event) {
final scrollEvent = event as PointerScrollEvent;
final newOffset = scrollController.offset + scrollEvent.scrollDelta.dy;
final clampedOffset = newOffset.clamp(
scrollController.position.minScrollExtent,
scrollController.position.maxScrollExtent,
);
scrollController.jumpTo(clampedOffset);
});
}
},
child: Center(
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: rows.asMap().entries.map((rowEntry) {
final rowIndex = rowEntry.key;
final rowLocations = rowEntry.value;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: rowLocations.asMap().entries.map((locEntry) {
final actualIndex = locEntry.key * rowCount + rowIndex;
final location = locEntry.value;
final isTop3 = actualIndex < 3;
return Container(
width: 200,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: isTop3
? FluentTheme.of(context).accentColor.withValues(alpha: .2 - index * 0.05)
? FluentTheme.of(context).accentColor.withValues(alpha: .2 - actualIndex * 0.05)
: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 28,
height: 28,
width: 22,
height: 22,
decoration: BoxDecoration(
color: isTop3 ? Colors.yellow.withValues(alpha: 1 - index * 0.3) : Colors.grey,
color: isTop3
? Colors.yellow.withValues(alpha: 1 - actualIndex * 0.3)
: Colors.grey,
shape: BoxShape.circle,
),
child: Center(
child: Text(
"${index + 1}",
"${actualIndex + 1}",
style: TextStyle(
fontSize: 14,
fontSize: 11,
fontWeight: FontWeight.bold,
color: isTop3 ? Colors.black : Colors.white,
),
),
),
),
const SizedBox(width: 12),
const SizedBox(width: 8),
Expanded(
child: Text(location.key, style: TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
location.key,
style: TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
Text(
"${location.value}",
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .5)),
),
],
),
),
],
),
);
}),
],
}).toList(),
),
);
}).toList(),
),
),
),
),
),
),
),
],
),
);
}
@ -1108,21 +1421,36 @@ class YearlyReportUI extends HookConsumerWidget {
Widget _buildDataSummaryPage(BuildContext context, YearlyReportData data) {
final yearlyHours = data.yearlyPlayTime.inMinutes / 60;
final kdRatio = data.yearlyDeathCount > 0
? (data.yearlyKillCount / data.yearlyDeathCount).toStringAsFixed(2)
: data.yearlyKillCount > 0
? ""
: "-";
//
final dataItems = <_SummaryGridItem>[
_SummaryGridItem("启动游戏", "${data.yearlyLaunchCount}", "", FontAwesomeIcons.play, Colors.green),
_SummaryGridItem("游玩时长", yearlyHours.toStringAsFixed(1), "小时", FontAwesomeIcons.clock, Colors.blue),
_SummaryGridItem("游戏崩溃", "${data.yearlyCrashCount}", "", FontAwesomeIcons.bug, Colors.orange),
_SummaryGridItem("击杀玩家", "${data.yearlyKillCount}", "", FontAwesomeIcons.crosshairs, Colors.green),
_SummaryGridItem("意外死亡", "${data.yearlyDeathCount}", "", FontAwesomeIcons.skull, Colors.red),
_SummaryGridItem("KD 比率", kdRatio, "", FontAwesomeIcons.chartLine, Colors.teal),
_SummaryGridItem("载具损毁", "${data.yearlyVehicleDestructionCount}", "", FontAwesomeIcons.explosion, Colors.red),
_SummaryGridItem("启动游戏", "${data.yearlyLaunchCount}", "", FontAwesomeIcons.play, Colors.green, isWide: false),
_SummaryGridItem(
"游玩时长",
yearlyHours.toStringAsFixed(1),
"小时",
FontAwesomeIcons.clock,
Colors.blue,
isWide: false,
),
_SummaryGridItem("游戏崩溃", "${data.yearlyCrashCount}", "", FontAwesomeIcons.bug, Colors.orange, isWide: false),
_SummaryGridItem(
"击杀",
"${data.yearlyKillCount}",
"",
FontAwesomeIcons.crosshairs,
Colors.green,
isWide: false,
),
_SummaryGridItem("死亡", "${data.yearlyDeathCount}", "", FontAwesomeIcons.skull, Colors.red, isWide: false),
_SummaryGridItem(
"载具损毁",
"${data.yearlyVehicleDestructionCount}",
"",
FontAwesomeIcons.explosion,
Colors.red,
isWide: false,
),
if (data.longestSession != null)
_SummaryGridItem(
"最长在线",
@ -1130,17 +1458,9 @@ class YearlyReportUI extends HookConsumerWidget {
"小时",
FontAwesomeIcons.hourglassHalf,
Colors.purple,
isWide: false,
),
if (data.topLocations.isNotEmpty)
_SummaryGridItem(
"最常去",
data.topLocations.first.key.length > 6
? "${data.topLocations.first.key.substring(0, 5)}..."
: data.topLocations.first.key,
"",
FontAwesomeIcons.locationDot,
Colors.grey,
),
//
if (data.earliestPlayDate != null)
_SummaryGridItem(
"最早时刻",
@ -1148,6 +1468,7 @@ class YearlyReportUI extends HookConsumerWidget {
"",
FontAwesomeIcons.sun,
Colors.orange,
isWide: false,
),
if (data.latestPlayDate != null)
_SummaryGridItem(
@ -1156,8 +1477,57 @@ class YearlyReportUI extends HookConsumerWidget {
"",
FontAwesomeIcons.moon,
Colors.purple,
isWide: false,
),
_SummaryGridItem(
"重开次数",
"${data.yearlySelfKillCount}",
"",
FontAwesomeIcons.personFalling,
Colors.grey,
isWide: false,
),
//
if (data.mostPlayedMonth != null)
_SummaryGridItem(
"最热月",
_getMonthName(data.mostPlayedMonth!),
"",
FontAwesomeIcons.fire,
Colors.orange,
isWide: false,
),
// /线
if (data.longestPlayStreak > 0)
_SummaryGridItem(
"连续游玩",
"${data.longestPlayStreak}",
"",
FontAwesomeIcons.gamepad,
Colors.green,
isWide: false,
),
if (data.longestOfflineStreak > 0)
_SummaryGridItem("连续离线", "${data.longestOfflineStreak}", "", FontAwesomeIcons.bed, Colors.grey, isWide: false),
//
if (data.topLocations.isNotEmpty)
_SummaryGridItem(
"常去位置",
data.topLocations.first.key,
"",
FontAwesomeIcons.locationDot,
Colors.red,
isWide: true, // 使
),
if (data.mostPilotedVehicle != null)
_SummaryGridItem(
"最爱载具",
data.mostPilotedVehicle!,
"",
FontAwesomeIcons.shuttleSpace,
Colors.teal,
isWide: true, // 使
),
_SummaryGridItem("重开次数", "${data.yearlySelfKillCount}", "", FontAwesomeIcons.personFalling, Colors.grey),
];
return Center(
@ -1173,7 +1543,7 @@ class YearlyReportUI extends HookConsumerWidget {
Icon(FontAwesomeIcons.star, size: 20, color: Colors.yellow),
const SizedBox(width: 12),
Text(
"2025 年度报告",
"星际公民 2025 年度报告",
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(width: 12),
@ -1195,19 +1565,21 @@ class YearlyReportUI extends HookConsumerWidget {
//
FadeInUp(
delay: const Duration(milliseconds: 200),
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: MasonryGridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
crossAxisCount: 5,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: dataItems.length,
itemBuilder: (context, index) {
final item = dataItems[index];
final isSmallFont = item.isWide; // 使
return Container(
padding: const EdgeInsets.all(12),
height: 150,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
@ -1216,8 +1588,8 @@ class YearlyReportUI extends HookConsumerWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(item.icon, size: 14, color: item.color.withValues(alpha: .8)),
const SizedBox(height: 8),
Icon(item.icon, size: 20, color: item.color.withValues(alpha: .8)),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
@ -1226,30 +1598,32 @@ class YearlyReportUI extends HookConsumerWidget {
child: Text(
item.value,
style: TextStyle(
fontSize: 34,
fontSize: isSmallFont ? 16 : 42,
fontWeight: FontWeight.bold,
color: Colors.white,
height: 1.0,
),
textAlign: TextAlign.center,
maxLines: isSmallFont ? 2 : null,
overflow: isSmallFont ? TextOverflow.ellipsis : null,
),
),
if (item.unit.isNotEmpty) ...[
const SizedBox(width: 2),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2),
padding: const EdgeInsets.only(bottom: 4),
child: Text(
item.unit,
style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .6)),
style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: .6)),
),
),
],
],
),
const SizedBox(height: 4),
const SizedBox(height: 6),
Text(
item.label,
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .5)),
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .5)),
textAlign: TextAlign.center,
),
],
@ -1259,37 +1633,13 @@ class YearlyReportUI extends HookConsumerWidget {
),
),
),
const SizedBox(height: 16),
//
if (data.mostPilotedVehicle != null)
FadeInUp(
delay: const Duration(milliseconds: 400),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(FontAwesomeIcons.shuttleSpace, size: 14, color: Colors.teal.withValues(alpha: .7)),
const SizedBox(width: 10),
Text(
"最爱: ${data.mostPilotedVehicle}",
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .8)),
),
],
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 20),
//
FadeInUp(
delay: const Duration(milliseconds: 600),
delay: const Duration(milliseconds: 400),
child: Text(
"SC汉化盒子 · 2025年度报告",
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .3)),
"由 SC 汉化盒子为您呈现",
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .3)),
),
),
],
@ -1302,11 +1652,11 @@ class YearlyReportUI extends HookConsumerWidget {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .3)),
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .15)),
child: Text(
"数据使用您的本地日志生成,不会发送到任何第三方。因跨版本 Log 改动较大,数据可能不完整,仅供娱乐。",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .5)),
style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: .7)),
),
);
}
@ -1319,8 +1669,9 @@ class _SummaryGridItem {
final String unit;
final IconData icon;
final Color color;
final bool isWide;
const _SummaryGridItem(this.label, this.value, this.unit, this.icon, this.color);
const _SummaryGridItem(this.label, this.value, this.unit, this.icon, this.color, {this.isWide = false});
}
///