From ffbe76e5cc72cc86b5f58b53a01af307ec1eeed5 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 21 Jun 2023 22:17:39 +0300 Subject: [PATCH] teto service now caching data + TL match view Also added weights constants for nerd stats --- lib/data_objects/tetrio.dart | 83 ++-- lib/services/tetrio_crud.dart | 44 +- lib/views/compare_view.dart | 20 +- lib/views/main_view.dart | 39 +- lib/views/tl_match_view.dart | 887 ++++++++++++++++++++++++++++++++++ lib/widgets/tl_thingy.dart | 20 +- 6 files changed, 1006 insertions(+), 87 deletions(-) create mode 100644 lib/views/tl_match_view.dart diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index d1ccb78..4d7970d 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -5,6 +5,18 @@ import 'dart:developer' as developer; import 'package:http/http.dart' as http; import 'dart:convert'; +const double noTrRd = 60.9; +const double apmWeight = 1; +const double ppsWeight = 45; +const double vsWeight = 0.444; +const double appWeight = 185; +const double dssWeight = 175; +const double dspWeight = 450; +const double appdspWeight = 140; +const double vsapmWeight = 60; +const double cheeseWeight = 1.25; +const double gbeWeight = 315; + Duration doubleSecondsToDuration(double value) { value = value * 1000000; return Duration(microseconds: value.floor()); @@ -578,42 +590,46 @@ class TetraLeagueAlphaRecord{ } class EndContextMulti { - String? userId; - String? username; - int? naturalOrder; - int? inputs; - int? piecesPlaced; - Handling? handling; - int? points; - int? wins; - double? secondary; - List? secondaryTracking; - double? tertiary; - List? tertiaryTracking; - double? extra; - List? extraTracking; - bool? success; + late String userId; + late String username; + late int naturalOrder; + late int inputs; + late int piecesPlaced; + late Handling handling; + late int points; + late int wins; + late double secondary; + late List secondaryTracking; + late double tertiary; + late List tertiaryTracking; + late double extra; + late List extraTracking; + late bool success; + late NerdStats nerdStats; + late EstTr estTr; + late Playstyle playstyle; EndContextMulti( - {this.userId, - this.naturalOrder, - this.inputs, - this.piecesPlaced, - this.handling, - this.points, - this.wins, - this.secondary, - this.secondaryTracking, - this.tertiary, - this.tertiaryTracking, - this.extra, - this.extraTracking, - this.success}); + {required this.userId, + required this.username, + required this.naturalOrder, + required this.inputs, + required this.piecesPlaced, + required this.handling, + required this.points, + required this.wins, + required this.secondary, + required this.secondaryTracking, + required this.tertiary, + required this.tertiaryTracking, + required this.extra, + required this.extraTracking, + required this.success}); EndContextMulti.fromJson(Map json) { userId = json['user']['_id']; username = json['user']['username']; - handling = json['handling'] != null ? Handling.fromJson(json['handling']) : null; + handling = Handling.fromJson(json['handling']); success = json['success']; inputs = json['inputs']; piecesPlaced = json['piecesplaced']; @@ -626,15 +642,16 @@ class EndContextMulti { tertiaryTracking = json['points']['tertiaryAvgTracking'].cast(); extra = json['points']['extra']['vs']; extraTracking = json['points']['extraAvgTracking']['aggregatestats___vsscore'].cast(); + nerdStats = NerdStats(secondary, tertiary, extra); + estTr = EstTr(secondary, tertiary, extra, noTrRd, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); + playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); } Map toJson() { final Map data = {}; data['user']['_id'] = userId; data['user']['username'] = username; - if (handling != null) { - data['handling'] = handling!.toJson(); - } + data['handling'] = handling.toJson(); data['success'] = success; data['inputs'] = inputs; data['piecesplaced'] = piecesPlaced; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index e7401c5..48c07c6 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -29,6 +29,8 @@ const String createTetrioUsersToTrack = ''' class TetrioService extends DB { Map> _players = {}; + final Map _playersCache = {}; + final Map _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; late final StreamController>> _tetrioStreamController; @@ -41,16 +43,16 @@ class TetrioService extends DB { @override Future open() async { await super.open(); - await _cachePlayers(); + await _loadPlayers(); } Stream>> get allPlayers => _tetrioStreamController.stream; - Future _cachePlayers() async { + Future _loadPlayers() async { final allPlayers = await getAllPlayers(); _players = allPlayers.toList().first; // ??? _tetrioStreamController.add(_players); - developer.log("_cachePlayers: $_players", name: "services/tetrio_crud"); + developer.log("_loadPlayers: $_players", name: "services/tetrio_crud"); } Future deletePlayer(String id) async { @@ -72,6 +74,19 @@ class TetrioService extends DB { } Future getTLStream(String userID) async { + try{ + var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID); + if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ + developer.log("getTLStream: Stream $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); + return cached.value; + }else{ + _tlStreamsCache.remove(cached.key); + developer.log("getTLStream: Cached stream $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); + } + }catch(e){ + developer.log("getTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud"); + } + var url = Uri.https('ch.tetr.io', 'api/streams/league_userrecent_${userID.toLowerCase().trim()}'); final response = await http.get(url); @@ -83,13 +98,15 @@ class TetrioService extends DB { // await ensureDbIsOpen(); // storeState(player); // } + developer.log("getTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); + _tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream; return stream; } else { developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body); throw Exception("User doesn't exist"); } } else { - developer.log("getTLStream Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); + developer.log("getTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode); throw Exception('Failed to fetch player'); } } @@ -208,6 +225,19 @@ class TetrioService extends DB { } Future fetchPlayer(String user, bool addToDB) async { + try{ + var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user); + if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ + developer.log("fetchPlayer: User $user retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); + return cached.value; + }else{ + _playersCache.remove(cached.key); + developer.log("fetchPlayer: Cached user $user expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); + } + }catch(e){ + developer.log("fetchPlayer: Trying to retrieve $user", name: "services/tetrio_crud"); + } + var url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}'); final response = await http.get(url); @@ -219,13 +249,15 @@ class TetrioService extends DB { await ensureDbIsOpen(); storeState(player); } + developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); + _playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player; return player; } else { - developer.log("fetchTetrioPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); + developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); throw Exception("User doesn't exist"); } } else { - developer.log("fetchTetrioPlayer Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); + developer.log("fetchPlayer Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); throw Exception('Failed to fetch player'); } } diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 8939e0d..312ceb5 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -547,43 +547,43 @@ class CompareState extends State { RadarEntry( value: theGreenSide! .tlSeason1.apm! * - 1), + apmWeight), RadarEntry( value: theGreenSide! .tlSeason1.pps! * - 45), + ppsWeight), RadarEntry( value: theGreenSide! .tlSeason1.vs! * - 0.444), + vsWeight), RadarEntry( value: theGreenSide!.tlSeason1 .nerdStats!.app * - 185), + appWeight), RadarEntry( value: theGreenSide!.tlSeason1 .nerdStats!.dss * - 175), + dssWeight), RadarEntry( value: theGreenSide!.tlSeason1 .nerdStats!.dsp * - 450), + dspWeight), RadarEntry( value: theGreenSide!.tlSeason1 .nerdStats!.appdsp * - 140), + appdspWeight), RadarEntry( value: theGreenSide!.tlSeason1 .nerdStats!.vsapm * - 60), + vsapmWeight), RadarEntry( value: theGreenSide!.tlSeason1 .nerdStats!.cheese * - 1.25), + cheeseWeight), RadarEntry( value: theGreenSide!.tlSeason1 .nerdStats!.gbe * - 315), + gbeWeight), ], ), RadarDataSet( diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index c0c10a9..1444813 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:developer' as developer; import 'package:flutter/services.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; @@ -81,7 +81,6 @@ class _MainState extends State with SingleTickerProviderStateMixin { _getPreferences() .then((value) => changePlayer(prefs.getString("player") ?? "dan63047")); super.initState(); - developer.log("Main view initialized", name: "main_view"); } @override @@ -89,7 +88,6 @@ class _MainState extends State with SingleTickerProviderStateMixin { _tabController.dispose(); _scrollController.dispose(); super.dispose(); - developer.log("Main view disposed", name: "main_view"); } Future _getPreferences() async { @@ -155,10 +153,6 @@ class _MainState extends State with SingleTickerProviderStateMixin { ), PopupMenuButton( itemBuilder: (BuildContext context) => [ - // const PopupMenuItem( - // value: "/compare", - // child: Text('Compare'), - // ), const PopupMenuItem( value: "/states", child: Text('Show stored data'), @@ -182,7 +176,6 @@ class _MainState extends State with SingleTickerProviderStateMixin { child: FutureBuilder( future: me, builder: (context, snapshot) { - developer.log("builder ($context): $snapshot", name: "main_view"); switch (snapshot.connectionState) { case ConnectionState.none: return const Center( @@ -225,10 +218,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { isScrollable: true, tabs: myTabs, onTap: (int tabId) { - setState(() { - developer.log("Tab changed to $tabId", - name: "main_view"); - }); + setState(() {}); }, ), ), @@ -350,8 +340,6 @@ class _NavDrawerState extends State { leading: const Icon(Icons.home), title: Text(homePlayerNickname), onTap: () { - developer.log("Navigator changed player", - name: "main_view"); widget.changePlayer( prefs.getString("player") ?? "dan63047"); Navigator.of(context).pop(); @@ -367,8 +355,6 @@ class _NavDrawerState extends State { title: Text( allPlayers[keys[index]]?.last.username as String), onTap: () { - developer.log("Navigator changed player", - name: "main_view"); widget.changePlayer(keys[index]); Navigator.of(context).pop(); }, @@ -401,10 +387,7 @@ class _TLRecords extends StatelessWidget { child: CircularProgressIndicator(color: Colors.white)); case ConnectionState.done: if (snapshot.hasError) { - return Text(snapshot.error.toString(), - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28)); + return Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)); } else { return ListView( physics: const ClampingScrollPhysics(), @@ -414,7 +397,7 @@ class _TLRecords extends StatelessWidget { style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 28,)), - title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username!}"), + title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username}"), subtitle: Text(dateFormat.format(value.timestamp!)), trailing: Column(mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -422,14 +405,14 @@ class _TLRecords extends StatelessWidget { Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary)} PPS", style: TextStyle(height: 1.1)), Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra)} VS", style: TextStyle(height: 1.1)), ]), - onTap: (){}, + onTap: (){Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TlMatchResultView(record: value, initPlayerId: userID), + ), + );}, )] - : [ - Text("No records", - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28)) - ], + : [const Text("No records",style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))], ); } } diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart new file mode 100644 index 0000000..46ffd64 --- /dev/null +++ b/lib/views/tl_match_view.dart @@ -0,0 +1,887 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; + + +final DateFormat dateFormat = DateFormat.yMMMd().add_Hms(); + +class TlMatchResultView extends StatefulWidget { + final TetraLeagueAlphaRecord record; + final String initPlayerId; + const TlMatchResultView({Key? key, required this.record, required this.initPlayerId}) + : super(key: key); + + @override + State createState() => TlMatchResultState(); +} + +class TlMatchResultState extends State { + late ScrollController _scrollController; + + @override + void initState(){ + _scrollController = ScrollController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + bool bigScreen = MediaQuery.of(context).size.width > 768; + return Scaffold( + appBar: AppBar( + title: Text( + "${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} vs. ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} in TL match ${dateFormat.format(widget.record.timestamp!)}"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.green, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, 0.4], + )), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Column(children: [ + Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28)), + Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 42)) + ]), + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 16), + child: Text("VS"), + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.red, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, 0.4], + )), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Column(children: [ + Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28)), + Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 42)) + ]), + ), + ), + ), + ], + ), + ), + ), + const SliverToBoxAdapter( + child: Divider(), + ) + ]; + }, + body: ListView( + children: [ + Column( + children: [ + CompareThingy( + label: "APM", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "PPS", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "VS", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra, + fractionDigits: 2, + higherIsBetter: true, + ), + ], + ), + const Divider(), + Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Nerd Stats", + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "APP", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: + widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Cheese", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Garbage Eff.", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Weighted APP", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Est. of TR", + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr, + fractionDigits: 2, + higherIsBetter: true, + ), + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceAround, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(20, 20, 20, 20), + child: SizedBox( + height: 300, + width: 300, + child: RadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle( + color: Colors.transparent, + fontSize: 10), + radarBorderData: const BorderSide( + color: Colors.transparent, width: 1), + gridBorderData: const BorderSide( + color: Colors.white24, width: 1), + tickBorderData: const BorderSide( + color: Colors.transparent, width: 1), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle( + text: 'APM', + angle: angle, + ); + case 1: + return RadarChartTitle( + text: 'PPS', + angle: angle, + ); + case 2: + return RadarChartTitle( + text: 'VS', angle: angle); + case 3: + return RadarChartTitle( + text: 'APP', + angle: angle + 180); + case 4: + return RadarChartTitle( + text: 'DS/S', + angle: angle + 180); + case 5: + return RadarChartTitle( + text: 'DS/P', + angle: angle + 180); + case 6: + return RadarChartTitle( + text: 'APP+DS/P', + angle: angle + 180); + case 7: + return RadarChartTitle( + text: 'VS/APM', + angle: angle + 180); + case 8: + return RadarChartTitle( + text: 'Cheese', angle: angle); + case 9: + return RadarChartTitle( + text: 'Gb Eff.', angle: angle); + default: + return const RadarChartTitle( + text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: const Color.fromARGB( + 115, 76, 175, 79), + borderColor: Colors.green, + dataEntries: [ + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary * apmWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary * ppsWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra * vsWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app * appWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss * dssWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp * dspWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp * appdspWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm * vsapmWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese * cheeseWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe * gbeWeight), + ], + ), + RadarDataSet( + fillColor: const Color.fromARGB( + 115, 244, 67, 54), + borderColor: Colors.red, + dataEntries: [ + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary * apmWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary * ppsWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra * vsWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app * appWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss * dssWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp * dspWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp * appdspWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm * vsapmWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese * cheeseWeight), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe * gbeWeight), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ), + swapAnimationDuration: const Duration( + milliseconds: 150), // Optional + swapAnimationCurve: + Curves.linear, // Optional + ), + ), + ), + Padding( + padding: + const EdgeInsets.fromLTRB(20, 20, 20, 20), + child: SizedBox( + height: 300, + width: 300, + child: RadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle( + color: Colors.transparent, + fontSize: 10), + radarBorderData: const BorderSide( + color: Colors.transparent, width: 1), + gridBorderData: const BorderSide( + color: Colors.white24, width: 1), + tickBorderData: const BorderSide( + color: Colors.transparent, width: 1), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle( + text: 'Opener', + angle: angle, + ); + case 1: + return RadarChartTitle( + text: 'Stride', + angle: angle, + ); + case 2: + return RadarChartTitle( + text: 'Inf Ds', + angle: angle + 180); + case 3: + return RadarChartTitle( + text: 'Plonk', angle: angle); + default: + return const RadarChartTitle( + text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: const Color.fromARGB( + 115, 76, 175, 79), + borderColor: Colors.green, + dataEntries: [ + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk), + ], + ), + RadarDataSet( + fillColor: const Color.fromARGB( + 115, 244, 67, 54), + borderColor: Colors.red, + dataEntries: [ + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds), + RadarEntry(value: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 1), + const RadarEntry(value: 1), + const RadarEntry(value: 1), + ], + ) + ], + ), + swapAnimationDuration: const Duration( + milliseconds: 150), // Optional + swapAnimationCurve: + Curves.linear, // Optional + ), + ), + ) + ], + ) + ], + ) + ], + ) + ), + ), + ); + } +} + + +class CompareThingy extends StatelessWidget { + final num greenSide; + final num redSide; + final String label; + final bool higherIsBetter; + final int? fractionDigits; + const CompareThingy( + {super.key, + required this.greenSide, + required this.redSide, + required this.label, + required this.higherIsBetter, + this.fractionDigits}); + + String verdict(num greenSide, num redSide, int fraction) { + var f = NumberFormat("+#,###.##;-#,###.##"); + f.maximumFractionDigits = fraction; + return f.format((greenSide - redSide)); + } + + @override + Widget build(BuildContext context) { + var f = NumberFormat("#,###.##"); + f.maximumFractionDigits = fractionDigits ?? 0; + return Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [ + 0.0, + higherIsBetter + ? greenSide > redSide + ? 0.6 + : 0 + : greenSide < redSide + ? 0.6 + : 0 + ], + )), + child: Text( + f.format(greenSide), + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.start, + ), + )), + Column( + children: [ + Text( + label, + style: const TextStyle(fontSize: 22), + textAlign: TextAlign.center, + ), + Text( + verdict(greenSide, redSide, + fractionDigits != null ? fractionDigits! + 2 : 0), + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ) + ], + ), + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.centerRight, + end: Alignment.centerLeft, + stops: [ + 0.0, + higherIsBetter + ? redSide > greenSide + ? 0.6 + : 0 + : redSide < greenSide + ? 0.6 + : 0 + ], + )), + child: Text( + f.format(redSide), + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.end, + ), + )), + ], + ), + ); + } +} + +class CompareBoolThingy extends StatelessWidget { + final bool greenSide; + final bool redSide; + final String label; + final bool trueIsBetter; + const CompareBoolThingy( + {super.key, + required this.greenSide, + required this.redSide, + required this.label, + required this.trueIsBetter}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), + child: Row(children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [ + 0.0, + trueIsBetter + ? greenSide + ? 0.6 + : 0 + : !greenSide + ? 0.6 + : 0 + ], + )), + child: Text( + greenSide ? "Yes" : "No", + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.start, + ), + )), + Column( + children: [ + Text( + label, + style: const TextStyle(fontSize: 22), + textAlign: TextAlign.center, + ), + const Text( + "---", + style: TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ) + ], + ), + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.centerRight, + end: Alignment.centerLeft, + stops: [ + 0.0, + trueIsBetter + ? redSide + ? 0.6 + : 0 + : !redSide + ? 0.6 + : 0 + ], + )), + child: Text( + redSide ? "Yes" : "No", + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.end, + ), + )), + ]), + ); + } +} + +class CompareDurationThingy extends StatelessWidget { + final Duration greenSide; + final Duration redSide; + final String label; + final bool higherIsBetter; + const CompareDurationThingy( + {super.key, + required this.greenSide, + required this.redSide, + required this.label, + required this.higherIsBetter}); + + Duration verdict(Duration greenSide, Duration redSide) { + return greenSide - redSide; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Text( + greenSide.toString(), + style: const TextStyle( + fontSize: 22, + ), + textAlign: TextAlign.start, + )), + Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.center, + ), + Text( + verdict(greenSide, redSide).toString(), + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ) + ], + ), + Expanded( + child: Text( + redSide.toString(), + style: const TextStyle(fontSize: 22), + textAlign: TextAlign.end, + )), + ], + ), + ); + } +} + +class CompareRegTimeThingy extends StatelessWidget { + final DateTime? greenSide; + final DateTime? redSide; + final String label; + final int? fractionDigits; + const CompareRegTimeThingy( + {super.key, + required this.greenSide, + required this.redSide, + required this.label, + this.fractionDigits}); + + String verdict(DateTime? greenSide, DateTime? redSide) { + var f = NumberFormat("#,### days later;#,### days before"); + String result = "---"; + if (greenSide != null && redSide != null) + result = f.format(greenSide.difference(redSide).inDays); + return result; + } + + @override + Widget build(BuildContext context) { + DateFormat f = DateFormat.yMMMd(); + return Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [ + 0.0, + greenSide == null + ? 0.6 + : redSide != null && greenSide!.isBefore(redSide!) + ? 0.6 + : 0 + ], + )), + child: Text( + greenSide != null ? f.format(greenSide!) : "From beginning", + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.start, + ), + )), + Column( + children: [ + Text( + label, + style: const TextStyle(fontSize: 22), + textAlign: TextAlign.center, + ), + Text( + verdict(greenSide, redSide), + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ) + ], + ), + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.centerRight, + end: Alignment.centerLeft, + stops: [ + 0.0, + redSide == null + ? 0.6 + : greenSide != null && redSide!.isBefore(greenSide!) + ? 0.6 + : 0 + ], + )), + child: Text( + redSide != null ? f.format(redSide!) : "From beginning", + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.end, + ), + )), + ], + ), + ); + } +} diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 9dc97fc..6d2dd9b 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -199,16 +199,16 @@ class TLThingy extends StatelessWidget { dataSets: [ RadarDataSet( dataEntries: [ - RadarEntry(value: tl.apm! * 1), - RadarEntry(value: tl.pps! * 45), - RadarEntry(value: tl.vs! * 0.444), - RadarEntry(value: tl.nerdStats!.app * 185), - RadarEntry(value: tl.nerdStats!.dss * 175), - RadarEntry(value: tl.nerdStats!.dsp * 450), - RadarEntry(value: tl.nerdStats!.appdsp * 140), - RadarEntry(value: tl.nerdStats!.vsapm * 60), - RadarEntry(value: tl.nerdStats!.cheese * 1.25), - RadarEntry(value: tl.nerdStats!.gbe * 315), + RadarEntry(value: tl.apm! * apmWeight), + RadarEntry(value: tl.pps! * ppsWeight), + RadarEntry(value: tl.vs! * vsWeight), + RadarEntry(value: tl.nerdStats!.app * appWeight), + RadarEntry(value: tl.nerdStats!.dss * dssWeight), + RadarEntry(value: tl.nerdStats!.dsp * dspWeight), + RadarEntry(value: tl.nerdStats!.appdsp * appdspWeight), + RadarEntry(value: tl.nerdStats!.vsapm * vsWeight), + RadarEntry(value: tl.nerdStats!.cheese * cheeseWeight), + RadarEntry(value: tl.nerdStats!.gbe * gbeWeight), ], ), RadarDataSet(