From 0f903581602beaadb88288a925ee8eed4ec6bc93 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 14 Jun 2024 23:47:36 +0300 Subject: [PATCH] details for singleplayer records also very small refactoring (I will redo Tetra Stats from the ground one day...) --- lib/utils/numers_formats.dart | 9 +- lib/utils/relative_timestamps.dart | 17 ++ lib/views/main_view.dart | 211 ++--------------------- lib/views/singleplayer_record_view.dart | 45 +++++ lib/views/sprint_and_blitz_averages.dart | 3 +- lib/views/tl_match_view.dart | 2 +- lib/widgets/recent_sp_games.dart | 51 ++++++ lib/widgets/singleplayer_record.dart | 151 ++++++++++++++++ 8 files changed, 292 insertions(+), 197 deletions(-) create mode 100644 lib/views/singleplayer_record_view.dart create mode 100644 lib/widgets/recent_sp_games.dart create mode 100644 lib/widgets/singleplayer_record.dart diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index 7a22180..097b705 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -10,4 +10,11 @@ final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..minimumFractionDigits = 0; final NumberFormat f1 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 1); final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode); -final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2; \ No newline at end of file +final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2; + +/// Readable [a] - [b], without sign +String readableIntDifference(int a, int b){ + int result = a - b; + + return NumberFormat("#,###;#,###", LocaleSettings.currentLocale.languageCode).format(result); +} \ No newline at end of file diff --git a/lib/utils/relative_timestamps.dart b/lib/utils/relative_timestamps.dart index 5e5645f..0e9260f 100644 --- a/lib/utils/relative_timestamps.dart +++ b/lib/utils/relative_timestamps.dart @@ -1,5 +1,10 @@ +import 'package:intl/intl.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; +final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); +final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode); + /// Returns string, that represents time difference between [dateTime] and now String relativeDateTime(DateTime dateTime){ Duration difference = dateTime.difference(DateTime.now()); @@ -56,4 +61,16 @@ String relativeDateTime(DateTime dateTime){ } else { return inPast ? "${f1.format(timeInterval)} seconds ago" : "in ${f1.format(timeInterval)} seconds"; } +} + +/// Takes number of [microseconds] and returns readable 40 lines time +String get40lTime(int microseconds){ + return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); +} + +/// Readable [a] - [b], without sign +String readableTimeDifference(Duration a, Duration b){ + Duration result = a - b; + + return NumberFormat("0.000s;0.000s", LocaleSettings.currentLocale.languageCode).format(result.inMilliseconds/1000); } \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 2371fbe..6d3a71a 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -17,12 +17,16 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show prefs, teto; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/singleplayer_record_view.dart'; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; +import 'package:tetra_stats/widgets/recent_sp_games.dart'; import 'package:tetra_stats/widgets/search_box.dart'; +import 'package:tetra_stats/widgets/singleplayer_record.dart'; import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; @@ -38,9 +42,6 @@ late ZoomPanBehavior _zoomPanBehavior; bool _smooth = false; List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; late ScrollController _scrollController; -final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode); -final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); - class MainView extends StatefulWidget { final String? player; @@ -57,25 +58,6 @@ Future copyToClipboard(String text) async { await Clipboard.setData(ClipboardData(text: text)); } -/// Takes number of [microseconds] and returns readable 40 lines time -String get40lTime(int microseconds){ - return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); - } - -/// Readable [a] - [b], without sign -String readableTimeDifference(Duration a, Duration b){ - Duration result = a - b; - - return NumberFormat("0.000s;0.000s", LocaleSettings.currentLocale.languageCode).format(result.inMilliseconds/1000); -} - -/// Readable [a] - [b], without sign -String readableIntDifference(int a, int b){ - int result = a - b; - - return NumberFormat("#,###;#,###", LocaleSettings.currentLocale.languageCode).format(result); -} - class _MainState extends State with TickerProviderStateMixin { Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up TetrioPlayersLeaderboard? everyone; @@ -103,7 +85,7 @@ class _MainState extends State with TickerProviderStateMixin { void initState() { initDB(); _scrollController = ScrollController(); - _tabController = TabController(length: 6, vsync: this); + _tabController = TabController(length: 7, vsync: this); _wideScreenTabController = TabController(length: 4, vsync: this); _zoomPanBehavior = ZoomPanBehavior( enablePinching: true, @@ -455,6 +437,7 @@ class _MainState extends State with TickerProviderStateMixin { Tab(text: t.history), Tab(text: t.sprint), Tab(text: t.blitz), + Tab(text: "Recent runs"), Tab(text: t.other), ], ), @@ -514,8 +497,9 @@ class _MainState extends State with TickerProviderStateMixin { ), _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), - _RecordThingy(record: snapshot.data![1].sprint, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![9]), - _RecordThingy(record: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![10]), + SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![9]), + SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![10]), + _RecentSingleplayersThingy(snapshot.data![8]), _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6]) ], ), @@ -1106,9 +1090,7 @@ class _TwoRecordsThingy extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ for (int i = 1; i < sprintStream.records.length; i++) ListTile( - onTap: () { - print("lox"); - }, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), title: Text(get40lTime(sprintStream.records[i].endContext.finalTime.inMicroseconds), style: TextStyle(fontSize: 18)), @@ -1185,9 +1167,7 @@ class _TwoRecordsThingy extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ for (int i = 1; i < sprintStream.records.length; i++) ListTile( - onTap: () { - print("lox"); - }, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].endContext.score)} points", style: TextStyle(fontSize: 18)), @@ -1201,180 +1181,25 @@ class _TwoRecordsThingy extends StatelessWidget { ), SizedBox( width: 400, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text("Recent", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - ), - for(RecordSingle record in recent.records) ListTile( - onTap: () { - print("lox"); - }, - leading: Text( - switch (record.endContext.gameType){ - "40l" => "40L", - "blitz" => "BLZ", - "5mblast" => "5MB", - String() => "huh", - }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (record.endContext.gameType){ - "40l" => get40lTime(record.endContext.finalTime.inMicroseconds), - "blitz" => "${NumberFormat.decimalPattern().format(record.endContext.score)} points", - "5mblast" => get40lTime(record.endContext.finalTime.inMicroseconds), - String() => "huh", - }, - style: TextStyle(fontSize: 18)), - subtitle: Text(timestamp(record.timestamp), style: TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(record.endContext) - ) - ], - ), + child: RecentSingleplayerGames(recent: recent), ) ]), )); } } -class _RecordThingy extends StatelessWidget { - final RecordSingle? record; - final SingleplayerStream stream; - final String? rank; +class _RecentSingleplayersThingy extends StatelessWidget { + final SingleplayerStream recent; - /// Widget that displays data from [record] - const _RecordThingy({required this.record, required this.stream, this.rank}); - - Color getColorOfRank(int rank){ - if (rank == 1) return Colors.yellowAccent; - if (rank == 2) return Colors.blueGrey; - if (rank == 3) return Colors.brown[400]!; - if (rank <= 9) return Colors.blueAccent; - if (rank <= 99) return Colors.greenAccent; - return Colors.grey; - } + const _RecentSingleplayersThingy(this.recent); @override Widget build(BuildContext context) { - if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); - late MapEntry closestAverageBlitz; - late bool blitzBetterThanClosestAverage; - bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.score > blitzAverages[rank]! : null; - late MapEntry closestAverageSprint; - late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.finalTime < sprintAverages[rank]! : null; - if (record!.endContext.gameType == "40l") { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.endContext.finalTime).abs() < (b -record!.endContext.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = record!.endContext.finalTime < closestAverageSprint.value; - }else if (record!.endContext.gameType == "blitz"){ - closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-record!.endContext.score).abs() < (b -record!.endContext.score).abs() ? a : b)); - blitzBetterThanClosestAverage = record!.endContext.score > closestAverageBlitz.value; - } - - return LayoutBuilder( - builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - return SingleChildScrollView( - controller: _scrollController, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (record!.endContext.gameType == "40l") Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) - ), - if (record!.endContext.gameType == "blitz") Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (record!.endContext.gameType == "40l") Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - if (record!.endContext.gameType == "blitz") Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: record!.endContext.gameType == "40l" ? get40lTime(record!.endContext.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record!.endContext.score), - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (record!.endContext.gameType == "40l" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if (record!.endContext.gameType == "40l" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )) - else if (record!.endContext.gameType == "blitz" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if (record!.endContext.gameType == "blitz" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )), - if (record!.rank != null) TextSpan(text: "№${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank!))), - if (record!.rank != null) const TextSpan(text: " • "), - TextSpan(text: timestamp(record!.timestamp)), - ] - ), - ) - ],), - ], - ), - if (record!.endContext.gameType == "40l") Wrap( - alignment: WrapAlignment.spaceBetween, - spacing: 20, - children: [ - StatCellNum(playerStat: record!.endContext.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - ], - ), - if (record!.endContext.gameType == "blitz") Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - StatCellNum(playerStat: record!.endContext.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) - ], - ), - FinesseThingy(record?.endContext.finesse, record?.endContext.finessePercentage), - LineclearsThingy(record!.endContext.clears, record!.endContext.lines, record!.endContext.holds, record!.endContext.tSpins), - if (record!.endContext.gameType == "40l") Text("${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kps)} KPS"), - if (record!.endContext.gameType == "blitz") Text("${record!.endContext.piecesPlaced} P • ${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kpp)} KPP • ${f2.format(record!.endContext.kps)} KPS"), - if (stream.records.length > 1) for(int i = 1; i < stream.records.length; i++) ListTile( - onTap: () { - print("lox"); - }, - leading: Text("#${i+1}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (stream.records[i].endContext.gameType){ - "40l" => get40lTime(stream.records[i].endContext.finalTime.inMicroseconds), - "blitz" => "${NumberFormat.decimalPattern().format(stream.records[i].endContext.score)} points", - "5mblast" => get40lTime(stream.records[i].endContext.finalTime.inMicroseconds), - String() => "huh", - }, - style: TextStyle(fontSize: 18)), - subtitle: Text(timestamp(stream.records[i].timestamp), style: TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(stream.records[i].endContext) - ) - ] - ), - ), - ); - } + return SingleChildScrollView( + child: RecentSingleplayerGames(recent: recent, hideTitle: true) ); } + } class _OtherThingy extends StatelessWidget { diff --git a/lib/views/singleplayer_record_view.dart b/lib/views/singleplayer_record_view.dart new file mode 100644 index 0000000..eb23320 --- /dev/null +++ b/lib/views/singleplayer_record_view.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/widgets/singleplayer_record.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class SingleplayerRecordView extends StatelessWidget { + final RecordSingle record; + + const SingleplayerRecordView({super.key, required this.record}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + //bool bigScreen = MediaQuery.of(context).size.width >= 368; + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: Text("${ + switch (record.endContext.gameType){ + "40l" => t.sprint, + "blitz" => t.blitz, + String() => "5000000 Blast", + } + } ${timestamp(record.timestamp)}"), + ), + body: SafeArea( + child: SingleChildScrollView( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SingleplayerRecord(record: record, hideTitle: true), + ] + ) + ], + ) + ) + ), + ); + } + +} \ No newline at end of file diff --git a/lib/views/sprint_and_blitz_averages.dart b/lib/views/sprint_and_blitz_averages.dart index 322de40..9ab7ada 100644 --- a/lib/views/sprint_and_blitz_averages.dart +++ b/lib/views/sprint_and_blitz_averages.dart @@ -1,12 +1,11 @@ import 'dart:io'; -import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/main_view.dart'; import 'package:window_manager/window_manager.dart'; late String oldWindowTitle; diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index e230053..c02eb3b 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/vs_graphs.dart'; -import 'main_view.dart' show secs; import 'package:tetra_stats/main.dart' show teto; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/recent_sp_games.dart b/lib/widgets/recent_sp_games.dart new file mode 100644 index 0000000..8e16cc7 --- /dev/null +++ b/lib/widgets/recent_sp_games.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/singleplayer_record_view.dart'; +import 'package:tetra_stats/widgets/singleplayer_record.dart'; +import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class RecentSingleplayerGames extends StatelessWidget{ + final SingleplayerStream recent; + final bool hideTitle; + + const RecentSingleplayerGames({required this.recent, this.hideTitle = false, super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (!hideTitle) const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text("Recent", style: TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + ), + for(RecordSingle record in recent.records) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: record))), + leading: Text( + switch (record.endContext.gameType){ + "40l" => "40L", + "blitz" => "BLZ", + "5mblast" => "5MB", + String() => "huh", + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (record.endContext.gameType){ + "40l" => get40lTime(record.endContext.finalTime.inMicroseconds), + "blitz" => "${NumberFormat.decimalPattern().format(record.endContext.score)} points", + "5mblast" => get40lTime(record.endContext.finalTime.inMicroseconds), + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(record.timestamp), style: TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(record.endContext) + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart new file mode 100644 index 0000000..594c539 --- /dev/null +++ b/lib/widgets/singleplayer_record.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/singleplayer_record_view.dart'; +import 'package:tetra_stats/widgets/finesse_thingy.dart'; +import 'package:tetra_stats/widgets/lineclears_thingy.dart'; +import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; +import 'package:tetra_stats/widgets/stat_sell_num.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class SingleplayerRecord extends StatelessWidget { + final RecordSingle? record; + final SingleplayerStream? stream; + final String? rank; + final bool hideTitle; + + /// Widget that displays data from [record] + const SingleplayerRecord({super.key, required this.record, this.stream, this.rank, this.hideTitle = false}); + + Color getColorOfRank(int rank){ + if (rank == 1) return Colors.yellowAccent; + if (rank == 2) return Colors.blueGrey; + if (rank == 3) return Colors.brown[400]!; + if (rank <= 9) return Colors.blueAccent; + if (rank <= 99) return Colors.greenAccent; + return Colors.grey; + } + + @override + Widget build(BuildContext context) { + if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); + late MapEntry closestAverageBlitz; + late bool blitzBetterThanClosestAverage; + bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.score > blitzAverages[rank]! : null; + late MapEntry closestAverageSprint; + late bool sprintBetterThanClosestAverage; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.finalTime < sprintAverages[rank]! : null; + if (record!.endContext.gameType == "40l") { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.endContext.finalTime).abs() < (b -record!.endContext.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = record!.endContext.finalTime < closestAverageSprint.value; + }else if (record!.endContext.gameType == "blitz"){ + closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-record!.endContext.score).abs() < (b -record!.endContext.score).abs() ? a : b)); + blitzBetterThanClosestAverage = record!.endContext.score > closestAverageBlitz.value; + } + + return LayoutBuilder( + builder: (context, constraints) { + bool bigScreen = constraints.maxWidth > 768; + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (record!.endContext.gameType == "40l") Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) + ), + if (record!.endContext.gameType == "blitz") Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (record!.endContext.gameType == "40l" && !hideTitle) Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + if (record!.endContext.gameType == "blitz" && !hideTitle) Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: record!.endContext.gameType == "40l" ? get40lTime(record!.endContext.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record!.endContext.score), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (record!.endContext.gameType == "40l" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if (record!.endContext.gameType == "40l" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( + color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent + )) + else if (record!.endContext.gameType == "blitz" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if (record!.endContext.gameType == "blitz" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( + color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent + )), + if (record!.rank != null) TextSpan(text: "№${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank!))), + if (record!.rank != null) const TextSpan(text: " • "), + TextSpan(text: timestamp(record!.timestamp)), + ] + ), + ) + ],), + ], + ), + if (record!.endContext.gameType == "40l") Wrap( + alignment: WrapAlignment.spaceBetween, + spacing: 20, + children: [ + StatCellNum(playerStat: record!.endContext.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + ], + ), + if (record!.endContext.gameType == "blitz") Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 20, + children: [ + StatCellNum(playerStat: record!.endContext.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) + ], + ), + FinesseThingy(record?.endContext.finesse, record?.endContext.finessePercentage), + LineclearsThingy(record!.endContext.clears, record!.endContext.lines, record!.endContext.holds, record!.endContext.tSpins), + if (record!.endContext.gameType == "40l") Text("${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kps)} KPS"), + if (record!.endContext.gameType == "blitz") Text("${record!.endContext.piecesPlaced} P • ${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kpp)} KPP • ${f2.format(record!.endContext.kps)} KPS"), + if (stream != null && stream!.records.length > 1) for(int i = 1; i < stream!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: stream!.records[i]))), + leading: Text("#${i+1}", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (stream!.records[i].endContext.gameType){ + "40l" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds), + "blitz" => "${NumberFormat.decimalPattern().format(stream!.records[i].endContext.score)} points", + "5mblast" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds), + String() => "huh", + }, + style: TextStyle(fontSize: 18)), + subtitle: Text(timestamp(stream!.records[i].timestamp), style: TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(stream!.records[i].endContext) + ) + ] + ), + ), + ); + } + ); + } +} \ No newline at end of file