From 81fda82147c20f81b603c75b12482fef52f58c6d Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 28 Feb 2024 01:00:00 +0300 Subject: [PATCH 01/14] thinking about implementing freyhoe stats --- lib/data_objects/freyhoe_test.dart | 22 +++ .../tetrio_multiplayer_replay.dart | 157 +++++++++++++++++- web/index.html | 2 +- 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 lib/data_objects/freyhoe_test.dart diff --git a/lib/data_objects/freyhoe_test.dart b/lib/data_objects/freyhoe_test.dart new file mode 100644 index 0000000..0707f71 --- /dev/null +++ b/lib/data_objects/freyhoe_test.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + +import 'tetrio_multiplayer_replay.dart'; + +/// That thing allows me to test my new staff i'm trying to implement +void main() async { + // List queue = List.from(tetrominoes); + // TetrioRNG rng = TetrioRNG(0); + // queue = rng.shuffleList(queue); + // print(queue); + // queue = List.from(tetrominoes); + // queue = rng.shuffleList(queue); + // print(queue); + var downloadPath = await getDownloadsDirectory(); + ReplayData replay = ReplayData.fromJson(jsonDecode(File("${downloadPath!.path}/65b504a9ade6d287b8427af0").readAsStringSync())); + List> board = [for (var i = 0 ; i < 40; i++) [for (var i = 0 ; i < 10; i++) Tetromino.empty]]; + print(replay.rawJson); + exit(0); +} \ No newline at end of file diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 34491cf..73ec55b 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,4 +1,6 @@ import 'dart:math'; +import 'package:vector_math/vector_math_64.dart'; + import 'tetrio.dart'; // I want to implement those fancy TWC stats @@ -211,4 +213,157 @@ class ReplayData{ } return data; } -} \ No newline at end of file +} + +// can't belive i have to implement that difficult shit + +class Event{ + int id; + int frame; + String type; + //dynamic data; + + Event(this.id, this.frame, this.type); +} + +class Keypress{ + String key; + double subframe; + + Keypress(this.key, this.subframe); +} + +class EventKeyPress extends Event{ + Keypress data; + + EventKeyPress(super.id, super.frame, super.type, this.data); +} + +class IGE{ + int id; + int frame; + String type; + int amount; + + IGE(this.id, this.frame, this.type, this.amount); +} + +class EventIGE extends Event{ + IGE data; + + EventIGE(super.id, super.frame, super.type, this.data); +} + +class TetrioRNG{ + late double _t; + + TetrioRNG(int seed){ + _t = seed % 2147483647; + if (_t <= 0) _t += 2147483646; + } + + int next(){ + _t = 16807 * _t % 2147483647; + return _t.toInt(); + } + + double nextFloat(){ + return (next() - 1) / 2147483646; + } + + List shuffleList(List array){ + int length = array.length; + if (length == 0) return []; + + for (; --length > 0;){ + int swapIndex = ((nextFloat()) * (length + 1)).toInt(); + Tetromino tmp = array[length]; + array[length] = array[swapIndex]; + array[swapIndex] = tmp; + } + return array; + } +} + +enum Tetromino{ + Z, + L, + O, + S, + I, + J, + T, + garbage, + empty +} + +List tetrominoes = [Tetromino.Z, Tetromino.L, Tetromino.O, Tetromino.S, Tetromino.I, Tetromino.J, Tetromino.T]; +List>> shapes = [ + [ // Z + [Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(2, 1)], + [Vector2(2, 0), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], + [Vector2(0, 1), Vector2(1, 1), Vector2(1, 2), Vector2(2, 2)], + [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(0, 2)] + ], + [ // L + [Vector2(2, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], + [Vector2(1, 0), Vector2(1, 1), Vector2(1, 2), Vector2(2, 2)], + [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(0, 2)], + [Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(1, 2)] + ], + [ // O + [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], + [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], + [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], + [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)] + ], + [ // S + [Vector2(1, 0), Vector2(2, 0), Vector2(0, 1), Vector2(1, 1)], + [Vector2(1, 0), Vector2(1, 1), Vector2(2, 1), Vector2(2, 2)], + [Vector2(1, 1), Vector2(2, 1), Vector2(0, 2), Vector2(1, 2)], + [Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 2)] + ], + [ // I + [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(3, 1)], + [Vector2(2, 0), Vector2(2, 1), Vector2(2, 2), Vector2(2, 3)], + [Vector2(0, 2), Vector2(1, 2), Vector2(2, 2), Vector2(3, 2)], + [Vector2(1, 0), Vector2(1, 1), Vector2(1, 2), Vector2(1, 3)] + ], + [ // J + [Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], + [Vector2(1, 0), Vector2(2, 0), Vector2(1, 1), Vector2(1, 2)], + [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(2, 2)], + [Vector2(1, 0), Vector2(1, 1), Vector2(0, 2), Vector2(1, 2)] + ], + [ // T + [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], + [Vector2(1, 0), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], + [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], + [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 2)] + ] +]; +List spawnPositionFixes = [Vector2(1, 1), Vector2(1, 1), Vector2(0, 1), Vector2(1, 1), Vector2(1, 1), Vector2(1, 1), Vector2(1, 1)]; + +const Map garbage = { + "single": 0, + "double": 1, + "triple": 2, + "quad": 4, + "penta": 5, + "t-spin": 0, + "t-spin single": 2, + "t-spin double": 4, + "t-spin triple": 6, + "t-spin quad": 10, + "t-spin penta": 12, + "t-spin mini": 0, + "t-spin mini single": 0, + "t-spin mini double": 1, + "allclear": 10 +}; +int btbBonus = 1; +double btbLog = 0.8; +double comboBonus = 0.25; +int comboMinifier = 1; +double comboMinifierLog = 1.25; +List comboTable = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5]; diff --git a/web/index.html b/web/index.html index 430a240..57042b3 100644 --- a/web/index.html +++ b/web/index.html @@ -18,7 +18,7 @@ - + From 0d2d83a98aeff1517f4c2d643240d53dcd9c85b5 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 1 Mar 2024 01:24:08 +0300 Subject: [PATCH 02/14] Continuing doing boring monotonous work --- lib/data_objects/freyhoe_test.dart | 11 +- .../tetrio_multiplayer_replay.dart | 253 +++++++++++++++++- 2 files changed, 257 insertions(+), 7 deletions(-) diff --git a/lib/data_objects/freyhoe_test.dart b/lib/data_objects/freyhoe_test.dart index 0707f71..7a6eeb3 100644 --- a/lib/data_objects/freyhoe_test.dart +++ b/lib/data_objects/freyhoe_test.dart @@ -14,9 +14,12 @@ void main() async { // queue = List.from(tetrominoes); // queue = rng.shuffleList(queue); // print(queue); - var downloadPath = await getDownloadsDirectory(); - ReplayData replay = ReplayData.fromJson(jsonDecode(File("${downloadPath!.path}/65b504a9ade6d287b8427af0").readAsStringSync())); - List> board = [for (var i = 0 ; i < 40; i++) [for (var i = 0 ; i < 10; i++) Tetromino.empty]]; - print(replay.rawJson); + + // var downloadPath = await getDownloadsDirectory(); + // ReplayData replay = ReplayData.fromJson(jsonDecode(File("${downloadPath!.path}/65b504a9ade6d287b8427af0").readAsStringSync())); + // List> board = [for (var i = 0 ; i < 40; i++) [for (var i = 0 ; i < 10; i++) Tetromino.empty]]; + // print(replay.rawJson); + + print(""); exit(0); } \ No newline at end of file diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 73ec55b..81a3d08 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:vector_math/vector_math_64.dart'; @@ -217,20 +218,97 @@ class ReplayData{ // can't belive i have to implement that difficult shit +List readEventList(Map json){ + List events = []; + int id = 0; + for (var event in json['data'][0]['replays'][0]['events']){ + int frame = event["frame"]; + EventType type = EventType.values.byName(event['type']); + switch (type) { + case EventType.start: + events.add(Event(id, frame, type)); + break; + case EventType.full: + events.add(EventFull(id, frame, type, DataFull.fromJson(event["data"]))); + break; + case EventType.targets: + // TODO + break; + case EventType.keydown: + events.add(EventKeyPress(id, frame, type, + Keypress( + KeyType.values.byName(event['data']['key']), + event['data']['subframe'], + false) + )); + break; + case EventType.keyup: + events.add(EventKeyPress(id, frame, type, + Keypress( + KeyType.values.byName(event['data']['key']), + event['data']['subframe'], + true) + )); + break; + case EventType.end: + // TODO: Handle this case. + case EventType.ige: + // TODO: Handle this case. + case EventType.exit: + // TODO: Handle this case. + } + id++; + } + return []; +} + +enum EventType +{ + start, + end, + full, + keydown, + keyup, + targets, + ige, + exit +} + +enum KeyType +{ + moveLeft, + moveRight, + softDrop, + rotateCCW, + rotateCW, + rotate180, + hardDrop, + hold, + chat, + exit, + retry +} + class Event{ int id; int frame; - String type; + EventType type; //dynamic data; Event(this.id, this.frame, this.type); + + @override + String toString(){ + return "E#$id f#$frame: $type"; + } } class Keypress{ - String key; + KeyType key; double subframe; + bool released; - Keypress(this.key, this.subframe); + Keypress(this.key, this.subframe, this.released); } class EventKeyPress extends Event{ @@ -254,6 +332,175 @@ class EventIGE extends Event{ EventIGE(super.id, super.frame, super.type, this.data); } +class Hold +{ + String? piece; + bool locked; + + Hold(this.piece, this.locked); +} + +class DataFullOptions{ + int? version; + bool? seedRandom; + int? seed; + double? g; + int? stock; + int? gMargin; + double? gIncrease; + double? garbageMultiplier; + int? garbageMargin; + double? garbageIncrease; + int? garbageCap; + double? garbageCapIncrease; + int? garbageCapMax; + int? garbageHoleSize; + String? garbageBlocking; // TODO: enum + bool? hasGarbage; + int? locktime; + int? garbageSpeed; + int? forfeitTime; + int? are; + int? areLineclear; + bool? infiniteMovement; + int? lockresets; + bool? allow180; + bool? btbChaining; + bool? allclears; + bool? clutch; + bool? noLockout; + String? passthrough; + int? boardwidth; + int? boardheight; + Handling? handling; + int? boardbuffer; + + DataFullOptions.fromJson(Map json){ + version = json["version"]; + seedRandom = json["seed_random"]; + seed = json["seed"]; + g = json["g"]; + stock = json["stock"]; + gMargin = json["gmargin"]; + gIncrease = json["gincrease"]; + garbageMultiplier = json["garbagemultiplier"]; + garbageCapIncrease = json["garbagecapincrease"]; + garbageCapMax = json["garbagecapmax"]; + garbageHoleSize = json["garbageholesize"]; + garbageBlocking = json["garbageblocking"]; + hasGarbage = json["hasgarbage"]; + locktime = json["locktime"]; + garbageSpeed = json["garbagespeed"]; + forfeitTime = json["forfeit_time"]; + are = json["are"]; + areLineclear = json["lineclear_are"]; + infiniteMovement = json["infinitemovement"]; + lockresets = json["lockresets"]; + allow180 = json["allow180"]; + btbChaining = json["b2bchaining"]; + allclears = json["allclears"]; + clutch = json["clutch"]; + noLockout = json["nolockout"]; + passthrough = json["passthrough"]; + boardwidth = json["boardwidth"]; + boardheight = json["boardheight"]; + handling = Handling.fromJson(json["handling"]); + boardbuffer = json["boardbuffer"]; + } +} + +class DataFullStats + { + double? seed; + int? lines; + int? levelLines; + int? levelLinesNeeded; + int? inputs; + int? holds; + int? score; + int? zenLevel; + int? zenProgress; + int? level; + int? combo; + int? currentComboPower; + int? topCombo; + int? btb; + int? topbtb; + int? tspins; + int? piecesPlaced; + Clears? clears; + Garbage? garbage; + int? kills; + Finesse? finesse; + + DataFullStats.fromJson(Map json){ + seed = json["seed"]; + lines = json["lines"]; + levelLines = json["level_lines"]; + levelLinesNeeded = json["level_lines_needed"]; + inputs = json["inputs"]; + holds = json["holds"]; + score = json["score"]; + zenLevel = json["zenlevel"]; + zenProgress = json["zenprogress"]; + level = json["level"]; + combo = json["combo"]; + currentComboPower = json["currentcombopower"]; + topCombo = json["topcombo"]; + btb = json["btb"]; + topbtb = json["topbtb"]; + tspins = json["tspins"]; + piecesPlaced = json["piecesplaced"]; + clears = Clears.fromJson(json["clears"]); + garbage = Garbage.fromJson(json["garbage"]); + kills = json["kills"]; + finesse = Finesse.fromJson(json["finesse"]); + } + } + +class DataFullGame + { + List>? board; + List? bag; + double? g; + bool? playing; + Hold? hold; + String? piece; + Handling? handling; + + DataFullGame.fromJson(Map json){ + board = json["board"]; + bag = json["bag"]; + hold = Hold(json["hold"]["piece"], json["hold"]["locked"]); + g = json["g"]; + handling = Handling.fromJson(json["handling"]); + } + } + +class DataFull{ + bool? successful; + String? gameOverReason; + int? fire; + DataFullOptions? options; + DataFullStats? stats; + DataFullGame? game; + + DataFull.fromJson(Map json){ + successful = json["successful"]; + gameOverReason = json["gameoverreason"]; + fire = json["fire"]; + options = DataFullOptions.fromJson(json["options"]); + stats = DataFullStats.fromJson(json["stats"]); + game = DataFullGame.fromJson(json["game"]); + } +} + +class EventFull extends Event{ + DataFull data; + + EventFull(super.id, super.frame, super.type, this.data); +} + class TetrioRNG{ late double _t; From 49c5dfdf5a33df2f122df916e41050a8bc3646a7 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 3 Mar 2024 01:26:31 +0300 Subject: [PATCH 03/14] Experimental changes for tl_match_view --- lib/views/calc_view.dart | 133 +-- lib/views/compare_view.dart | 889 +++++++++--------- lib/views/main_view.dart | 21 +- lib/views/tl_match_view.dart | 1022 +++++++++++++-------- lib/widgets/list_tile_trailing_stats.dart | 33 + 5 files changed, 1179 insertions(+), 919 deletions(-) create mode 100644 lib/widgets/list_tile_trailing_stats.dart diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart index 0828a73..d942746 100644 --- a/lib/views/calc_view.dart +++ b/lib/views/calc_view.dart @@ -68,72 +68,77 @@ class CalcState extends State { ), backgroundColor: Colors.black, body: SafeArea( - child: NestedScrollView( - controller: _scrollController, - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(14, 16, 16, 32), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: TextField( - onSubmitted: (value) => calc(), - controller: apmController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("APM"), alignLabelWithHint: true), - ), - )), - Expanded( - child: TextField( - onSubmitted: (value) => calc(), - controller: ppsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("PPS"), alignLabelWithHint: true), - )), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 12), - child: TextField( - onSubmitted: (value) => calc(), - controller: vsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("VS"), alignLabelWithHint: true), - ), - )), - TextButton( - onPressed: () => calc(), - child: Text(t.calc), + child: Center( + child: Container( + constraints: BoxConstraints(maxWidth: 768), + child: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 16, 16, 32), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: TextField( + onSubmitted: (value) => calc(), + controller: apmController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(label: Text("APM"), alignLabelWithHint: true), + ), + )), + Expanded( + child: TextField( + onSubmitted: (value) => calc(), + controller: ppsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(label: Text("PPS"), alignLabelWithHint: true), + )), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: TextField( + onSubmitted: (value) => calc(), + controller: vsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(label: Text("VS"), alignLabelWithHint: true), + ), + )), + TextButton( + onPressed: () => calc(), + child: Text(t.calc), + ), + ], ), - ], + ), ), - ), - ), - const SliverToBoxAdapter( - child: Divider(), - ) - ]; - }, - body: nerdStats == null - ? Text(t.calcViewNoValues) - : ListView( - children: [ - _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.vsapm, label: "VS/APM", fractionDigits: 3), - _ListEntry(value: nerdStats!.dss, label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.dsp, label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.appdsp, label: "APP + DS/P", fractionDigits: 3), - _ListEntry(value: nerdStats!.cheese, label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.gbe, label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.nyaapp, label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.area, label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: estTr!.esttr, label: t.statCellNum.estOfTR, fractionDigits: 3), - Graphs(apm!, pps!, vs!, nerdStats!, playstyle!), - ], - )), + const SliverToBoxAdapter( + child: Divider(), + ) + ]; + }, + body: nerdStats == null + ? Text(t.calcViewNoValues) + : ListView( + children: [ + _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.vsapm, label: "VS/APM", fractionDigits: 3), + _ListEntry(value: nerdStats!.dss, label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.dsp, label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.appdsp, label: "APP + DS/P", fractionDigits: 3), + _ListEntry(value: nerdStats!.cheese, label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.gbe, label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.nyaapp, label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.area, label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: estTr!.esttr, label: t.statCellNum.estOfTR, fractionDigits: 3), + Graphs(apm!, pps!, vs!, nerdStats!, playstyle!), + ], + )), + ), + ), ), ); } diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 2dd567f..c3755fb 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -256,443 +256,453 @@ class CompareState extends State { appBar: AppBar(title: Text("$titleGreenSide ${t.vs} $titleRedSide")), 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: PlayerSelector( - data: theGreenSide, - mode: greenSideMode, - fetch: fetchGreenSide, - change: changeGreenSide, - updateState: _justUpdate, + child: Center( + child: Container( + constraints: BoxConstraints(maxWidth: 768), + 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: PlayerSelector( + data: theGreenSide, + mode: greenSideMode, + fetch: fetchGreenSide, + change: changeGreenSide, + updateState: _justUpdate, + ), + ), ), ), - ), - ), - 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: PlayerSelector( - data: theRedSide, - mode: redSideMode, - fetch: fetchRedSide, - change: changeRedSide, - updateState: _justUpdate, + 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: PlayerSelector( + data: theRedSide, + mode: redSideMode, + fetch: fetchRedSide, + change: changeRedSide, + updateState: _justUpdate, + ), + ), ), ), - ), + ], ), - ], + ), ), + const SliverToBoxAdapter( + child: Divider(), + ) + ]; + }, + body: Center( + child: Container( + constraints: BoxConstraints(maxWidth: 768), + child: ListView( + children: !listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])? [ + if (theGreenSide[0] != null && + theRedSide[0] != null && + theGreenSide[0]!.role != "banned" && + theRedSide[0]!.role != "banned") + Column( + children: [ + CompareRegTimeThingy( + greenSide: theGreenSide[0].registrationTime, + redSide: theRedSide[0].registrationTime, + label: t.registred), + CompareThingy( + label: t.statCellNum.level, + greenSide: theGreenSide[0].level, + redSide: theRedSide[0].level, + higherIsBetter: true, + fractionDigits: 2, + ), + if (!theGreenSide[0].gameTime.isNegative && + !theRedSide[0].gameTime.isNegative) + CompareThingy( + greenSide: theGreenSide[0].gameTime.inMicroseconds / + 1000000 / + 60 / + 60, + redSide: theRedSide[0].gameTime.inMicroseconds / + 1000000 / + 60 / + 60, + label: t.statCellNum.hoursPlayed.replaceAll(RegExp(r'\n'), " "), + higherIsBetter: true, + fractionDigits: 2, + ), + if (theGreenSide[0].gamesPlayed >= 0 && + theRedSide[0].gamesPlayed >= 0) + CompareThingy( + label: t.statCellNum.onlineGames.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[0].gamesPlayed, + redSide: theRedSide[0].gamesPlayed, + higherIsBetter: true, + ), + if (theGreenSide[0].gamesWon >= 0 && + theRedSide[0].gamesWon >= 0) + CompareThingy( + label: t.statCellNum.gamesWon.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[0].gamesWon, + redSide: theRedSide[0].gamesWon, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.friends, + greenSide: theGreenSide[0].friendCount, + redSide: theRedSide[0].friendCount, + higherIsBetter: true, + ), + const Divider(), + ], + ), + if (theGreenSide[0] != null && + theRedSide[0] != null && + (theGreenSide[0]!.role == "banned" || + theRedSide[0]!.role == "banned")) + CompareBoolThingy( + greenSide: theGreenSide[0].role == "banned", + redSide: theRedSide[0].role == "banned", + label: t.normalBanned, + trueIsBetter: false), + (theGreenSide[2].gamesPlayed > 0 || greenSideMode == Mode.stats) && + (theRedSide[2].gamesPlayed > 0 || redSideMode == Mode.stats) + ? Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.tetraLeague, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + if (theGreenSide[2].gamesPlayed > 9 && + theRedSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "TR", + greenSide: theGreenSide[2].rating, + redSide: theRedSide[2].rating, + fractionDigits: 2, + higherIsBetter: true, + ), + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].gamesPlayed, + redSide: theRedSide[2].gamesPlayed, + higherIsBetter: true, + ), + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].gamesWon, + redSide: theRedSide[2].gamesWon, + higherIsBetter: true, + ), + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "WR %", + greenSide: + theGreenSide[2].winrate * 100, + redSide: theRedSide[2].winrate * 100, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].gamesPlayed > 9 && + theRedSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "Glicko", + greenSide: theGreenSide[2].glicko!, + redSide: theRedSide[2].glicko!, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].gamesPlayed > 9 && + theRedSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "RD", + greenSide: theGreenSide[2].rd!, + redSide: theRedSide[2].rd!, + fractionDigits: 3, + higherIsBetter: false, + ), + if (theGreenSide[2].standing > 0 && + theRedSide[2].standing > 0 && + greenSideMode == Mode.player && + redSideMode == Mode.player) + CompareThingy( + label: t.statCellNum.lbpShort, + greenSide: theGreenSide[2].standing, + redSide: theRedSide[2].standing, + higherIsBetter: false, + ), + if (theGreenSide[2].standingLocal > 0 && + theRedSide[2].standingLocal > 0 && + greenSideMode == Mode.player && + redSideMode == Mode.player) + CompareThingy( + label: t.statCellNum.lbpcShort, + greenSide: + theGreenSide[2].standingLocal, + redSide: theRedSide[2].standingLocal, + higherIsBetter: false, + ), + if (theGreenSide[2].apm != null && + theRedSide[2].apm != null) + CompareThingy( + label: "APM", + greenSide: theGreenSide[2].apm!, + redSide: theRedSide[2].apm!, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].pps != null && + theRedSide[2].pps != null) + CompareThingy( + label: "PPS", + greenSide: theGreenSide[2].pps!, + redSide: theRedSide[2].pps!, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].vs != null && + theRedSide[2].vs != null) + CompareThingy( + label: "VS", + greenSide: theGreenSide[2].vs!, + redSide: theRedSide[2].vs!, + fractionDigits: 2, + higherIsBetter: true, + ), + ], + ) + : CompareBoolThingy( + greenSide: theGreenSide[2].gamesPlayed > 0, + redSide: theRedSide[2].gamesPlayed > 0, + label: t.playedTL, + trueIsBetter: false), + const Divider(), + if (theGreenSide[2].nerdStats != null && + theRedSide[2].nerdStats != null) + Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.nerdStats, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "APP", + greenSide: theGreenSide[2].nerdStats!.app, + redSide: theRedSide[2].nerdStats!.app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: theGreenSide[2].nerdStats!.vsapm, + redSide: theRedSide[2].nerdStats!.vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: theGreenSide[2].nerdStats!.dss, + redSide: theRedSide[2].nerdStats!.dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: theGreenSide[2].nerdStats!.dsp, + redSide: theRedSide[2].nerdStats!.dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: + theGreenSide[2].nerdStats!.appdsp, + redSide: theRedSide[2].nerdStats!.appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), + greenSide: + theGreenSide[2].nerdStats!.cheese, + redSide: theRedSide[2].nerdStats!.cheese, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Gb Eff.", + greenSide: theGreenSide[2].nerdStats!.gbe, + redSide: theRedSide[2].nerdStats!.gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "wAPP", + greenSide: + theGreenSide[2].nerdStats!.nyaapp, + redSide: theRedSide[2].nerdStats!.nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: theGreenSide[2].nerdStats!.area, + redSide: theRedSide[2].nerdStats!.area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.estOfTRShort, + greenSide: theGreenSide[2].estTr!.esttr, + redSide: theRedSide[2].estTr!.esttr, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].gamesPlayed > 9 && + theGreenSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: t.statCellNum.accOfEstShort, + greenSide: theGreenSide[2].esttracc!, + redSide: theRedSide[2].esttracc!, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Opener", + greenSide: theGreenSide[2].playstyle!.opener, + redSide: theRedSide[2].playstyle!.opener, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Plonk", + greenSide: theGreenSide[2].playstyle!.plonk, + redSide: theRedSide[2].playstyle!.plonk, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Stride", + greenSide: theGreenSide[2].playstyle!.stride, + redSide: theRedSide[2].playstyle!.stride, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Inf. DS", + greenSide: theGreenSide[2].playstyle!.infds, + redSide: theRedSide[2].playstyle!.infds, + fractionDigits: 3, + higherIsBetter: true, + ), + VsGraphs(theGreenSide[2].apm!, theGreenSide[2].pps!, theGreenSide[2].vs!, theGreenSide[2].nerdStats!, theGreenSide[2].playstyle!, theRedSide[2].apm!, theRedSide[2].pps!, theRedSide[2].vs!, theRedSide[2].nerdStats!, theRedSide[2].playstyle!), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.winChance, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + if (greenSideMode != Mode.stats && redSideMode != Mode.stats && + theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9) + CompareThingy( + label: t.byGlicko, + greenSide: getWinrateByTR( + theGreenSide[2].glicko!, + theGreenSide[2].rd!, + theRedSide[2].glicko!, + theRedSide[2].rd!) * + 100, + redSide: getWinrateByTR( + theRedSide[2].glicko!, + theRedSide[2].rd!, + theGreenSide[2].glicko!, + theGreenSide[2].rd!) * + 100, + fractionDigits: 2, + higherIsBetter: true, + postfix: "%", + ), + CompareThingy( + label: t.byEstTR, + greenSide: getWinrateByTR( + theGreenSide[2].estTr!.estglicko, + theGreenSide[2].rd ?? noTrRd, + theRedSide[2].estTr!.estglicko, + theRedSide[2].rd ?? noTrRd) * + 100, + redSide: getWinrateByTR( + theRedSide[2].estTr!.estglicko, + theRedSide[2].rd ?? noTrRd, + theGreenSide[2].estTr!.estglicko, + theGreenSide[2].rd ?? noTrRd) * + 100, + fractionDigits: 2, + higherIsBetter: true, + postfix: "%", + ), + ], + ) + ] : [Padding( + padding: const EdgeInsets.all(8.0), + child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), + )], // This is so fucked up holy shit + ), ), - ), - const SliverToBoxAdapter( - child: Divider(), ) - ]; - }, - body: ListView( - children: !listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])? [ - if (theGreenSide[0] != null && - theRedSide[0] != null && - theGreenSide[0]!.role != "banned" && - theRedSide[0]!.role != "banned") - Column( - children: [ - CompareRegTimeThingy( - greenSide: theGreenSide[0].registrationTime, - redSide: theRedSide[0].registrationTime, - label: t.registred), - CompareThingy( - label: t.statCellNum.level, - greenSide: theGreenSide[0].level, - redSide: theRedSide[0].level, - higherIsBetter: true, - fractionDigits: 2, - ), - if (!theGreenSide[0].gameTime.isNegative && - !theRedSide[0].gameTime.isNegative) - CompareThingy( - greenSide: theGreenSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - redSide: theRedSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - label: t.statCellNum.hoursPlayed.replaceAll(RegExp(r'\n'), " "), - higherIsBetter: true, - fractionDigits: 2, - ), - if (theGreenSide[0].gamesPlayed >= 0 && - theRedSide[0].gamesPlayed >= 0) - CompareThingy( - label: t.statCellNum.onlineGames.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesPlayed, - redSide: theRedSide[0].gamesPlayed, - higherIsBetter: true, - ), - if (theGreenSide[0].gamesWon >= 0 && - theRedSide[0].gamesWon >= 0) - CompareThingy( - label: t.statCellNum.gamesWon.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesWon, - redSide: theRedSide[0].gamesWon, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.friends, - greenSide: theGreenSide[0].friendCount, - redSide: theRedSide[0].friendCount, - higherIsBetter: true, - ), - const Divider(), - ], - ), - if (theGreenSide[0] != null && - theRedSide[0] != null && - (theGreenSide[0]!.role == "banned" || - theRedSide[0]!.role == "banned")) - CompareBoolThingy( - greenSide: theGreenSide[0].role == "banned", - redSide: theRedSide[0].role == "banned", - label: t.normalBanned, - trueIsBetter: false), - (theGreenSide[2].gamesPlayed > 0 || greenSideMode == Mode.stats) && - (theRedSide[2].gamesPlayed > 0 || redSideMode == Mode.stats) - ? Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.tetraLeague, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "TR", - greenSide: theGreenSide[2].rating, - redSide: theRedSide[2].rating, - fractionDigits: 2, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].gamesPlayed, - redSide: theRedSide[2].gamesPlayed, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].gamesWon, - redSide: theRedSide[2].gamesWon, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "WR %", - greenSide: - theGreenSide[2].winrate * 100, - redSide: theRedSide[2].winrate * 100, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "Glicko", - greenSide: theGreenSide[2].glicko!, - redSide: theRedSide[2].glicko!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "RD", - greenSide: theGreenSide[2].rd!, - redSide: theRedSide[2].rd!, - fractionDigits: 3, - higherIsBetter: false, - ), - if (theGreenSide[2].standing > 0 && - theRedSide[2].standing > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpShort, - greenSide: theGreenSide[2].standing, - redSide: theRedSide[2].standing, - higherIsBetter: false, - ), - if (theGreenSide[2].standingLocal > 0 && - theRedSide[2].standingLocal > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpcShort, - greenSide: - theGreenSide[2].standingLocal, - redSide: theRedSide[2].standingLocal, - higherIsBetter: false, - ), - if (theGreenSide[2].apm != null && - theRedSide[2].apm != null) - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].apm!, - redSide: theRedSide[2].apm!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].pps != null && - theRedSide[2].pps != null) - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].pps!, - redSide: theRedSide[2].pps!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].vs != null && - theRedSide[2].vs != null) - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].vs!, - redSide: theRedSide[2].vs!, - fractionDigits: 2, - higherIsBetter: true, - ), - ], - ) - : CompareBoolThingy( - greenSide: theGreenSide[2].gamesPlayed > 0, - redSide: theRedSide[2].gamesPlayed > 0, - label: t.playedTL, - trueIsBetter: false), - const Divider(), - if (theGreenSide[2].nerdStats != null && - theRedSide[2].nerdStats != null) - Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.nerdStats, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].nerdStats!.app, - redSide: theRedSide[2].nerdStats!.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].nerdStats!.vsapm, - redSide: theRedSide[2].nerdStats!.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].nerdStats!.dss, - redSide: theRedSide[2].nerdStats!.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].nerdStats!.dsp, - redSide: theRedSide[2].nerdStats!.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].nerdStats!.appdsp, - redSide: theRedSide[2].nerdStats!.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].nerdStats!.cheese, - redSide: theRedSide[2].nerdStats!.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].nerdStats!.gbe, - redSide: theRedSide[2].nerdStats!.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].nerdStats!.nyaapp, - redSide: theRedSide[2].nerdStats!.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].nerdStats!.area, - redSide: theRedSide[2].nerdStats!.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: theGreenSide[2].estTr!.esttr, - redSide: theRedSide[2].estTr!.esttr, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].gamesPlayed > 9 && - theGreenSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.accOfEstShort, - greenSide: theGreenSide[2].esttracc!, - redSide: theRedSide[2].esttracc!, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].playstyle!.opener, - redSide: theRedSide[2].playstyle!.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].playstyle!.plonk, - redSide: theRedSide[2].playstyle!.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].playstyle!.stride, - redSide: theRedSide[2].playstyle!.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].playstyle!.infds, - redSide: theRedSide[2].playstyle!.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].apm!, theGreenSide[2].pps!, theGreenSide[2].vs!, theGreenSide[2].nerdStats!, theGreenSide[2].playstyle!, theRedSide[2].apm!, theRedSide[2].pps!, theRedSide[2].vs!, theRedSide[2].nerdStats!, theRedSide[2].playstyle!), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.winChance, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (greenSideMode != Mode.stats && redSideMode != Mode.stats && - theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9) - CompareThingy( - label: t.byGlicko, - greenSide: getWinrateByTR( - theGreenSide[2].glicko!, - theGreenSide[2].rd!, - theRedSide[2].glicko!, - theRedSide[2].rd!) * - 100, - redSide: getWinrateByTR( - theRedSide[2].glicko!, - theRedSide[2].rd!, - theGreenSide[2].glicko!, - theGreenSide[2].rd!) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - CompareThingy( - label: t.byEstTR, - greenSide: getWinrateByTR( - theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd ?? noTrRd, - theRedSide[2].estTr!.estglicko, - theRedSide[2].rd ?? noTrRd) * - 100, - redSide: getWinrateByTR( - theRedSide[2].estTr!.estglicko, - theRedSide[2].rd ?? noTrRd, - theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd ?? noTrRd) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - ], - ) - ] : [Padding( - padding: const EdgeInsets.all(8.0), - child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), - )], // This is so fucked up holy shit - ) + ), + ), ), ), ); @@ -786,6 +796,8 @@ class PlayerSelector extends StatelessWidget { } } +const TextStyle verdictStyle = TextStyle(fontSize: 14, fontFamily: "Eurostile Round Condensed", color: Colors.grey, height: 1.1); + class CompareThingy extends StatelessWidget { final num greenSide; final num redSide; @@ -868,7 +880,7 @@ class CompareThingy extends StatelessWidget { Text( verdict(greenSide, redSide, fractionDigits != null ? fractionDigits! + 2 : 0), - style: const TextStyle(fontSize: 16), + style: verdictStyle, textAlign: TextAlign.center, ) ], @@ -981,11 +993,7 @@ class CompareBoolThingy extends StatelessWidget { style: const TextStyle(fontSize: 22), textAlign: TextAlign.center, ), - const Text( - "---", - style: TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ) + const Text("---", style: verdictStyle, textAlign: TextAlign.center) ], ), Expanded( @@ -1085,10 +1093,7 @@ class CompareDurationThingy extends StatelessWidget { textAlign: TextAlign.center, ), Text( - verdict(greenSide, redSide).toString(), - style: const TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ) + verdict(greenSide, redSide).toString(), style: verdictStyle, textAlign: TextAlign.center) ], ), Expanded( @@ -1176,11 +1181,7 @@ class CompareRegTimeThingy extends StatelessWidget { style: const TextStyle(fontSize: 22), textAlign: TextAlign.center, ), - Text( - verdict(greenSide, redSide), - style: const TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ) + Text(verdict(greenSide, redSide), style: verdictStyle, textAlign: TextAlign.center) ], ), Expanded( diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index c56a593..3e23981 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -20,6 +20,7 @@ import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/ranks_averages_view.dart' show RankAveragesView; import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; +import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/search_box.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; @@ -582,18 +583,14 @@ class _TLRecords extends StatelessWidget { style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), title: Text("vs. ${data[index].endContext.firstWhere((element) => element.userId != userID).username}"), subtitle: Text(_dateFormat.format(data[index].timestamp)), - trailing: Table(defaultColumnWidth: const IntrinsicColumnWidth(), - defaultVerticalAlignment: TableCellVerticalAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - columnWidths: const { - 0: FixedColumnWidth(50), - 2: FixedColumnWidth(50), - }, - children: [ - TableRow(children: [Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId == userID).secondary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId != userID).secondary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" APM", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - TableRow(children: [Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId == userID).tertiary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId != userID).tertiary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" PPS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - TableRow(children: [Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId == userID).extra), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId != userID).extra), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" VS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - ],), + trailing: TrailingStats( + data[index].endContext.firstWhere((element) => element.userId == userID).secondary, + data[index].endContext.firstWhere((element) => element.userId == userID).tertiary, + data[index].endContext.firstWhere((element) => element.userId == userID).extra, + data[index].endContext.firstWhere((element) => element.userId != userID).secondary, + data[index].endContext.firstWhere((element) => element.userId != userID).tertiary, + data[index].endContext.firstWhere((element) => element.userId != userID).extra + ), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), ), ); diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 219b725..373441e 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -1,9 +1,11 @@ // ignore_for_file: use_build_context_synchronously import 'dart:io'; +import 'dart:math'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/services/crud_exceptions.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/vs_graphs.dart'; import 'main_view.dart' show teto, secs; import 'package:flutter/foundation.dart'; @@ -36,12 +38,10 @@ class TlMatchResultView extends StatefulWidget { } class TlMatchResultState extends State { - late ScrollController _scrollController; late Future replayData; @override void initState(){ - _scrollController = ScrollController(); rounds = [DropdownMenuItem(value: -1, child: Text(t.match))]; rounds.addAll([for (int i = 0; i < widget.record.endContext.first.secondaryTracking.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]); replayData = teto.analyzeReplay(widget.record.replayId, widget.record.replayAvalable); @@ -59,10 +59,628 @@ class TlMatchResultState extends State { super.dispose(); } + Widget buildComparison(bool bigScreen, bool showMobileSelector){ + return NestedScrollView( + 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: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], + )), + 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: bigScreen ? const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28) : const TextStyle()), + 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: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], + )), + 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: bigScreen ? const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28) : const TextStyle()), + Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 42)) + ]), + ), + ), + ), + ], + ), + ), + ), + if (showMobileSelector) SliverToBoxAdapter( + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("${t.statsFor}: ", + style: const TextStyle(color: Colors.white, fontSize: 25)), + DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) { + roundSelector = value; + setState(() {}); + }),), + ], + ), + ), + ), + if (widget.record.ownId == widget.record.replayId && showMobileSelector) SliverToBoxAdapter( + child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), + ), + if (showMobileSelector) SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) { + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const LinearProgressIndicator(); + case ConnectionState.done: + if (!snapshot.hasError){ + if (roundSelector.isNegative){ + var time = framesToTime(snapshot.data!.totalLength); + return Center(child: Text("${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center)); + }else{ + var time = framesToTime(snapshot.data!.roundLengths[roundSelector]); + return Center(child: Text("${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}", textAlign: TextAlign.center,)); + } + }else{ + String reason; + switch (snapshot.error.runtimeType){ + case ReplayNotAvalable: + reason = t.matchIsTooOld; + break; + case SzyNotFound: + reason = t.matchIsTooOld; + break; + case SzyForbidden: + reason = t.errors.replayRejected; + break; + case SzyTooManyRequests: + reason = t.errors.tooManyRequests; + break; + default: + reason = snapshot.error.toString(); + break; + } + return Text("${t.replayIssue}: $reason", textAlign: TextAlign.center); + } + + } + },),), + const SliverToBoxAdapter( + child: Divider(), + ) + ]; + }, + body: ListView( + children: [ + Column( + children: [ + CompareThingy( + label: "APM", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "PPS", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "VS", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + FutureBuilder(future: replayData, builder: (BuildContext context, AsyncSnapshot snapshot){ + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const LinearProgressIndicator(); + case ConnectionState.done: + if (!snapshot.hasError){ + var greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); + var redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); + return Column(children: [ + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, + label: "Inputs", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, + label: "Pieces Placed", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, + label: "KpP", higherIsBetter: false, fractionDigits: 2,), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, + label: "KpS", higherIsBetter: true, fractionDigits: 2,), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared, + label: "Lines Cleared", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score, + label: "Score", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, + label: "SpP", higherIsBetter: true, fractionDigits: 2,), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100, + label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike, + label: "Best Spike", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo, + label: "Best Combo", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB, + label: "Best BtB", higherIsBetter: true), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent, + label: "Sent", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived, + label: "Recived", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack, + label: "Attack", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared, + label: "Cleared", higherIsBetter: true), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears, + label: "PC", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins, + label: "T-spins", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads, + label: "Quads", higherIsBetter: true), + ],); + }else{ + return Container(); + } + + } + }) + ], + ), + const Divider(), + Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.nerdStats, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "APP", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Gb Eff.", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "wAPP", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.estOfTRShort, + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Opener", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Plonk", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Stride", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Inf. DS", + greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds, + redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds, + fractionDigits: 3, + higherIsBetter: true, + ), + VsGraphs( + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector], + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector] + ) + ], + ), + if (widget.record.ownId != widget.record.replayId) const Divider(), + if (widget.record.ownId != widget.record.replayId) Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Handling", + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, + label: "DAS", fractionDigits: 1, postfix: "F", + higherIsBetter: false), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, + label: "ARR", fractionDigits: 1, postfix: "F", + higherIsBetter: false), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, + label: "SDF", prefix: "x", + higherIsBetter: true), + CompareBoolThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, + label: "Safe HD", + trueIsBetter: true) + ], + ) + ], + ) + ); + } + + Widget buildRoundSelector(double width){ + return Padding( + padding: EdgeInsets.all(8.0000000), + child: SizedBox( + width: width, + child: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverToBoxAdapter(child: + Wrap( + alignment: WrapAlignment.spaceBetween, + children: [ + FutureBuilder(future: replayData, builder: (context, snapshot) { + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const CircularProgressIndicator(); + case ConnectionState.done: + if (!snapshot.hasError){ + var time = framesToTime(snapshot.data!.totalLength); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.matchLength), + RichText( + text: TextSpan( + text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500), + children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ) + ],); + }else{ + String reason; + switch (snapshot.error.runtimeType){ + case ReplayNotAvalable: + reason = t.matchIsTooOld; + break; + case SzyNotFound: + reason = t.matchIsTooOld; + break; + case SzyForbidden: + reason = t.errors.replayRejected; + break; + case SzyTooManyRequests: + reason = t.errors.tooManyRequests; + break; + default: + reason = snapshot.error.toString(); + break; + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.record.ownId != widget.record.replayId) Text("${t.replayIssue}: $reason"), + if (widget.record.ownId == widget.record.replayId) Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), + if (widget.record.ownId != widget.record.replayId) RichText( + text: TextSpan( + text: "-:--", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.grey), + children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ) + ],); + } + + } + },), + if (widget.record.ownId != widget.record.replayId) Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("Number of rounds"), + RichText( + text: TextSpan( + text: widget.record.endContext.first.secondaryTracking.length > 0 ? widget.record.endContext.first.secondaryTracking.length.toString() : "---", + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28, + fontWeight: FontWeight.w500, + color: widget.record.endContext.first.secondaryTracking.length == 0 ? Colors.grey : null + ), + ), + ) + ],), + Column(children: [ + OverflowBar( + alignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( child: const Text('Match stats'), + style: roundSelector == -1 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, + onPressed: () { + roundSelector = -1; + setState(() {}); + }), + TextButton( child: const Text('Time-weighted match stats'), onPressed: () { + roundSelector = -1; + setState(() {}); + }), + //TextButton( child: const Text('Button 3'), onPressed: () {}), + ], + ) + ]), + // Column( + // children: [ + // ListTile( + // leading: Text("Round time"), + // title: Text("Winner", textAlign: TextAlign.center,), + // trailing: Text("Round stats"), + // ) + // ], + // ) + ], + ) + ) + ]; + }, + body: ListView.builder(itemCount: widget.record.endContext.first.secondaryTracking.length, + itemBuilder: (BuildContext context, int index) { + return FutureBuilder(future: replayData, builder: (context, snapshot) { + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const LinearProgressIndicator(); + case ConnectionState.done: + if (!snapshot.hasError){ + var time = framesToTime(snapshot.data!.roundLengths[index]); + var accentColor = snapshot.data!.roundWinners[index][0] == widget.initPlayerId ? Colors.green : Colors.red; + var bgColor = roundSelector == index ? Colors.grey.shade900 : Colors.transparent; + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + stops: const [0, 0.05], + colors: [accentColor, bgColor] + ) + ), + child: ListTile( + leading:RichText( + text: TextSpan( + text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500), + children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ), + title: Text(snapshot.data!.roundWinners[index][1], textAlign: TextAlign.center), + trailing: TrailingStats( + widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[index], + widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[index] + ), + onTap:(){ + roundSelector = index; + setState(() {}); + }, + ), + ); + }else{ + return Container( + decoration: BoxDecoration( + color: roundSelector == index ? Colors.grey.shade900 : Colors.transparent + ), + child: ListTile( + leading: RichText( + text: TextSpan( + text: "-:--", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.grey), + children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ), + title: Text("---", style: TextStyle(color: Colors.grey), textAlign: TextAlign.center), + trailing: TrailingStats( + widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[index], + widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[index], + widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[index] + ), + onTap:(){ + roundSelector = index; + setState(() {}); + }, + ), + ); + } + } + } + ); + }) + ), + ), + ); + } + + Widget getMainWidget(double viewportWidth) { + if (viewportWidth <= 1024) { + return Center( + child: Container( + constraints: BoxConstraints(maxWidth: 768), + child: buildComparison(viewportWidth > 768, true) + ), + ); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + //mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 768, + child: buildComparison(true, false) + ), + Container( + constraints: BoxConstraints(maxWidth: 768), + child: buildRoundSelector(max(viewportWidth-768-16, 200)), + ) + ], + ); + } + } + @override Widget build(BuildContext context) { final t = Translations.of(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()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${dateFormat.format(widget.record.timestamp)}"), @@ -114,401 +732,7 @@ class TlMatchResultState extends State { ] ), 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: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], - )), - 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: bigScreen ? const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28) : const TextStyle()), - 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: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.red, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], - )), - 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: bigScreen ? const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28) : const TextStyle()), - Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 42)) - ]), - ), - ), - ), - ], - ), - ), - ), - SliverToBoxAdapter( - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.statsFor}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) { - roundSelector = value; - setState(() {}); - }),), - ], - ), - ), - ), - if (widget.record.ownId == widget.record.replayId) SliverToBoxAdapter( - child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), - ), - SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) { - switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const LinearProgressIndicator(); - case ConnectionState.done: - if (!snapshot.hasError){ - if (roundSelector.isNegative){ - var time = framesToTime(snapshot.data!.totalLength); - return Center(child: Text("${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center)); - }else{ - var time = framesToTime(snapshot.data!.roundLengths[roundSelector]); - return Center(child: Text("${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}", textAlign: TextAlign.center,)); - } - }else{ - String reason; - switch (snapshot.error.runtimeType){ - case ReplayNotAvalable: - reason = t.matchIsTooOld; - break; - case SzyNotFound: - reason = t.matchIsTooOld; - break; - case SzyForbidden: - reason = t.errors.replayRejected; - break; - case SzyTooManyRequests: - reason = t.errors.tooManyRequests; - break; - default: - reason = snapshot.error.toString(); - break; - } - return Text("${t.replayIssue}: $reason", textAlign: TextAlign.center); - } - - } - },),), - const SliverToBoxAdapter( - child: Divider(), - ) - ]; - }, - body: ListView( - children: [ - Column( - children: [ - CompareThingy( - label: "APM", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "VS", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - FutureBuilder(future: replayData, builder: (BuildContext context, AsyncSnapshot snapshot){ - switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const LinearProgressIndicator(); - case ConnectionState.done: - if (!snapshot.hasError){ - var greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); - var redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); - return Column(children: [ - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, - label: "Inputs", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, - label: "Pieces Placed", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, - label: "KpP", higherIsBetter: false, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, - label: "KpS", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared, - label: "Lines Cleared", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score, - label: "Score", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, - label: "SpP", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100, - label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike, - label: "Best Spike", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo, - label: "Best Combo", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB, - label: "Best BtB", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent, - label: "Sent", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived, - label: "Recived", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack, - label: "Attack", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared, - label: "Cleared", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears, - label: "PC", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins, - label: "T-spins", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads, - label: "Quads", higherIsBetter: true), - ],); - }else{ - return Container(); - } - - } - }) - ], - ), - const Divider(), - Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.nerdStats, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "APP", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs( - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector] - ) - ], - ), - if (widget.record.ownId != widget.record.replayId) const Divider(), - if (widget.record.ownId != widget.record.replayId) Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Handling", - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, - label: "DAS", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, - label: "ARR", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, - label: "SDF", prefix: "x", - higherIsBetter: true), - CompareBoolThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, - label: "Safe HD", - trueIsBetter: true) - ], - ) - ], - ) - ), - ), - ); + body: getMainWidget(MediaQuery.of(context).size.width), + ); } } diff --git a/lib/widgets/list_tile_trailing_stats.dart b/lib/widgets/list_tile_trailing_stats.dart new file mode 100644 index 0000000..52db4b9 --- /dev/null +++ b/lib/widgets/list_tile_trailing_stats.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; + +class TrailingStats extends StatelessWidget{ + final double yourAPM; + final double yourPPS; + final double yourVS; + final double notyourAPM; + final double notyourPPS; + final double notyourVS; + + const TrailingStats(this.yourAPM, this.yourPPS, this.yourVS, this.notyourAPM, this.notyourPPS, this.notyourVS, {super.key}); + + @override + Widget build(BuildContext context) { + final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); + return Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + defaultVerticalAlignment: TableCellVerticalAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + columnWidths: const { + 0: FixedColumnWidth(42), + 2: FixedColumnWidth(42), + }, + children: [ + TableRow(children: [Text(f2.format(yourAPM), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(f2.format(notyourAPM), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" APM", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), + TableRow(children: [Text(f2.format(yourPPS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(f2.format(notyourPPS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" PPS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), + TableRow(children: [Text(f2.format(yourVS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(f2.format(notyourVS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" VS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), + ], + ); + } +} \ No newline at end of file From 0648ca9a5dcfba05cd0ba6265ca6c26c7dbf1981 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 5 Mar 2024 01:05:59 +0300 Subject: [PATCH 04/14] 40 Lines / Blitz grades and averages + another design changes --- lib/data_objects/tetrio.dart | 42 +++++++++ lib/services/tetrio_crud.dart | 6 ++ lib/views/main_view.dart | 109 ++++++++++++++++++++-- lib/views/settings_view.dart | 14 +++ lib/widgets/list_tile_trailing_stats.dart | 7 +- lib/widgets/stat_sell_num.dart | 4 +- lib/widgets/tl_thingy.dart | 8 +- 7 files changed, 174 insertions(+), 16 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 6df9f83..cc7607e 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -117,6 +117,48 @@ const Map rankColors = { // thanks osk for const rankColors at ht 'z': Color(0xFF375433) }; +const Map sprintAverages = { // based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098 + 'x': Duration(seconds: 25, milliseconds: 413), + 'u': Duration(seconds: 34, milliseconds: 549), + 'ss': Duration(seconds: 43, milliseconds: 373), + 's+': Duration(seconds: 54, milliseconds: 027), + 's': Duration(seconds: 60, milliseconds: 412), + 's-': Duration(seconds: 67, milliseconds: 381), + 'a+': Duration(seconds: 73, milliseconds: 694), + 'a': Duration(seconds: 81, milliseconds: 166), + 'a-': Duration(seconds: 88, milliseconds: 334), + 'b+': Duration(seconds: 93, milliseconds: 741), + 'b': Duration(seconds: 98, milliseconds: 354), + 'b-': Duration(seconds: 109, milliseconds: 610), + 'c+': Duration(seconds: 124, milliseconds: 641), + 'c': Duration(seconds: 126, milliseconds: 104), + 'c-': Duration(seconds: 145, milliseconds: 865), + 'd+': Duration(seconds: 154, milliseconds: 338), + 'd': Duration(seconds: 162, milliseconds: 063), + //'z': Duration(seconds: 66, milliseconds: 802) +}; + +const Map blitzAverages = { + 'x': 626494, + 'u': 406059, + 'ss': 243166, + 's+': 168636, + 's': 121594, + 's-': 107845, + 'a+': 87142, + 'a': 73413, + 'a-': 60799, + 'b+': 55417, + 'b': 47608, + 'b-': 40534, + 'c+': 34200, + 'c': 32535, + 'c-': 25808, + 'd+': 23345, + 'd': 23063, + //'z': 72084 +}; + String getStatNameByEnum(Stats stat){ return t[stat.name]; } diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index b5bf668..073a7fa 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -535,6 +535,12 @@ class TetrioService extends DB { } } + TetrioPlayersLeaderboard? getCachedLeaderboard(){ + return _leaderboardsCache.entries.firstOrNull?.value; + // That function will break if i decide to recive other leaderboards + // TODO: Think about better solution + } + /// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve. Future> fetchNews(String userID) async{ try{ diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 3e23981..3c630e8 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -30,6 +30,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up +TetrioPlayersLeaderboard? everyone; String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for String _titleNickname = "dan63047"; final TetrioService teto = TetrioService(); // thing, that manadge our local DB @@ -38,8 +39,8 @@ var chartsData = >>[]; int _chartsIndex = 0; 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."); -final NumberFormat secs = NumberFormat("00.###"); +final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode); +final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final DateFormat _dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); @@ -67,6 +68,20 @@ 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 { final bodyGlobalKey = GlobalKey(); bool _showSearchBar = false; @@ -153,6 +168,9 @@ class _MainState extends State with TickerProviderStateMixin { news = requests[2] as List; topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR + // Get tetra League leaderboard if needed + // if(prefs.getBool("loadLeaderboard") == true) everyone = await teto.fetchTLLeaderboard(); + // Making list of Tetra League matches List tlMatches = []; bool isTracking = await teto.isPlayerTracking(me.userId); @@ -379,8 +397,8 @@ class _MainState extends State with TickerProviderStateMixin { TLThingy(tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, states: snapshot.data![2], topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon"), _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]), _History(states: snapshot.data![2], update: _justUpdate), - _RecordThingy(record: snapshot.data![1]['sprint']), - _RecordThingy(record: snapshot.data![1]['blitz']), + _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank), + _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) ], ), @@ -906,13 +924,27 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { class _RecordThingy extends StatelessWidget { final RecordSingle? record; + final String? rank; /// Widget that displays data from [record] - const _RecordThingy({required this.record}); + const _RecordThingy({required this.record, this.rank}); @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!.stream.contains("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!.stream.contains("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 ListView.builder( @@ -926,8 +958,71 @@ class _RecordThingy extends StatelessWidget { else if (record!.stream.contains("blitz")) Text(t.blitz, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), // show main metric - if (record!.stream.contains("40l")) Text(get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - else if (record!.stream.contains("blitz")) Text(NumberFormat.decimalPattern().format(record!.endContext!.score), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + clipBehavior: Clip.hardEdge, + children: [ + // Show grade based on closest rank average + if (record!.stream.contains("40l")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) + else if (record!.stream.contains("blitz")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96), + + // TODO: I'm not sure abour that element. Maybe, it could be done differenly + Column( + children: [ + // Show result + if (record!.stream.contains("40l")) Text(get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + else if (record!.stream.contains("blitz")) Text(NumberFormat.decimalPattern().format(record!.endContext!.score), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + + // Show difference between rank average + if (record!.stream.contains("40l") && (rank != null && rank != "z")) Text( + "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: sprintBetterThanRankAverage??false ? + Colors.greenAccent : + Colors.redAccent + ) + ) + else if (record!.stream.contains("40l") && (rank == null || rank == "z")) Text( + "${readableTimeDifference(record!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: sprintBetterThanClosestAverage ? + Colors.greenAccent : + Colors.redAccent + ) + ) + else if (record!.stream.contains("blitz") && (rank != null && rank != "z")) Text( + "${readableIntDifference(record!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: blitzBetterThanRankAverage??false ? + Colors.greenAccent : + Colors.redAccent + ) + ) + else if (record!.stream.contains("blitz") && (rank == null || rank == "z")) Text( + "${readableIntDifference(record!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: blitzBetterThanClosestAverage ? + Colors.greenAccent : + Colors.redAccent + ) + ), + ], + ), + ], + ), + // if (record!.stream.contains("40l")) Text(get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + // else if (record!.stream.contains("blitz")) Text(NumberFormat.decimalPattern().format(record!.endContext!.score), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + + // // Compare with averages + // if (record!.stream.contains("40l") && rank != null) RichText(text: TextSpan(text: "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", style: TextStyle(fontFamily: "Eurostile Round", color: sprintBetterThanRankAverage??false ? Colors.green : Colors.red))) + // //Text("${record!.endContext!.finalTime - sprintAverages[rank]!}; ${sprintAverages[rank]}; ${get40lTime((record!.endContext!.finalTime - sprintAverages[rank]!).inMicroseconds)}") + // else if (record!.stream.contains("blitz")) Text("${closestAverageBlitz}; ${blitzAverages[rank]}"), // Show rank if presented if (record!.rank != null) StatCellNum(playerStat: record!.rank!, playerStatLabel: "Leaderboard Placement", isScreenBig: bigScreen, higherIsBetter: false), diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 6edb8b9..b4d88ac 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -26,6 +26,7 @@ class SettingsState extends State { late SharedPreferences prefs; final TetrioService teto = TetrioService(); String defaultNickname = "Checking..."; + late bool loadLeaderboard; final TextEditingController _playertext = TextEditingController(); @override @@ -46,6 +47,11 @@ class SettingsState extends State { Future _getPreferences() async { prefs = await SharedPreferences.getInstance(); + if (prefs.getBool("loadLeaderboard") != null) { + loadLeaderboard = prefs.getBool("loadLeaderboard")!; + } else { + loadLeaderboard = false; + } _setDefaultNickname(prefs.getString("player")); } @@ -260,6 +266,14 @@ class SettingsState extends State { onTap: () { Navigator.pushNamed(context, "/customization"); },), + ListTile(title: Text("Load leaderboard on startup"), + subtitle: Text("That will allow app to show additional stats, like..."), + trailing: Switch(value: loadLeaderboard, onChanged: (bool value){ + prefs.setBool("loadLeaderboard", value); + setState(() { + loadLeaderboard = value; + }); + }),), const Divider(), ListTile( onTap: (){ diff --git a/lib/widgets/list_tile_trailing_stats.dart b/lib/widgets/list_tile_trailing_stats.dart index 52db4b9..c5b223b 100644 --- a/lib/widgets/list_tile_trailing_stats.dart +++ b/lib/widgets/list_tile_trailing_stats.dart @@ -15,6 +15,7 @@ class TrailingStats extends StatelessWidget{ @override Widget build(BuildContext context) { final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); + const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100); return Table( defaultColumnWidth: const IntrinsicColumnWidth(), defaultVerticalAlignment: TableCellVerticalAlignment.baseline, @@ -24,9 +25,9 @@ class TrailingStats extends StatelessWidget{ 2: FixedColumnWidth(42), }, children: [ - TableRow(children: [Text(f2.format(yourAPM), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(f2.format(notyourAPM), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" APM", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - TableRow(children: [Text(f2.format(yourPPS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(f2.format(notyourPPS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" PPS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - TableRow(children: [Text(f2.format(yourVS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(f2.format(notyourVS), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" VS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), + TableRow(children: [Text(f2.format(yourAPM), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourAPM), textAlign: TextAlign.right, style: style), const Text(" APM", textAlign: TextAlign.right, style: style)]), + TableRow(children: [Text(f2.format(yourPPS), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourPPS), textAlign: TextAlign.right, style: style), const Text(" PPS", textAlign: TextAlign.right, style: style)]), + TableRow(children: [Text(f2.format(yourVS), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourVS), textAlign: TextAlign.right, style: style), const Text(" VS", textAlign: TextAlign.right, style: style)]), ], ); } diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 32be43a..6a11a89 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -50,8 +50,8 @@ class StatCellNum extends StatelessWidget { ), if (oldPlayerStat != null) Text(comparef.format(playerStat - oldPlayerStat!), style: TextStyle( color: higherIsBetter ? - oldPlayerStat! > playerStat ? Colors.red : Colors.green : - oldPlayerStat! < playerStat ? Colors.red : Colors.green + oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent : + oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent ),), alertWidgets == null ? Text( diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 2207abe..175a93a 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -241,8 +241,8 @@ class _TLThingyState extends State { },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05,), if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.app - oldTl!.nerdStats!.app), style: TextStyle( color: currentTl.nerdStats!.app - oldTl!.nerdStats!.app < 0 ? - Colors.red : - Colors.green + Colors.redAccent : + Colors.greenAccent ),), positionFactor: 0.05,)], )],), ), @@ -303,8 +303,8 @@ class _TLThingyState extends State { },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05), if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm), style: TextStyle( color: currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm < 0 ? - Colors.red : - Colors.green + Colors.redAccent : + Colors.greenAccent ),), positionFactor: 0.05,)], )],), ),]), From f95ffb59aa3118ef13daf36d3a8254f9e5cc650f Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 7 Mar 2024 01:34:15 +0300 Subject: [PATCH 05/14] Experimental new feature --- lib/data_objects/tetrio.dart | 94 +++++++ lib/services/tetrio_crud.dart | 10 + lib/utils/numers_formats.dart | 7 + lib/views/calc_view.dart | 1 - lib/views/main_view.dart | 129 ++++----- lib/views/mathes_view.dart | 1 - lib/views/ranks_averages_view.dart | 3 +- lib/views/settings_view.dart | 18 +- lib/views/tl_leaderboard_view.dart | 2 +- lib/widgets/gauget_num.dart | 98 +++++++ lib/widgets/list_tile_trailing_stats.dart | 4 +- lib/widgets/stat_sell_num.dart | 25 +- lib/widgets/tl_thingy.dart | 302 ++++++++++------------ 13 files changed, 439 insertions(+), 255 deletions(-) create mode 100644 lib/utils/numers_formats.dart create mode 100644 lib/widgets/gauget_num.dart diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index cc7607e..c9fa2f3 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -1139,6 +1139,73 @@ class News { } } +class PlayerLeaderboardPosition{ + late LeaderboardPosition apm; + late LeaderboardPosition pps; + late LeaderboardPosition vs; + late LeaderboardPosition gamesPlayed; + late LeaderboardPosition gamesWon; + late LeaderboardPosition winrate; + late LeaderboardPosition app; + late LeaderboardPosition vsapm; + late LeaderboardPosition dss; + late LeaderboardPosition dsp; + late LeaderboardPosition appdsp; + late LeaderboardPosition cheese; + late LeaderboardPosition gbe; + late LeaderboardPosition nyaapp; + late LeaderboardPosition area; + late LeaderboardPosition estTr; + late LeaderboardPosition accOfEst; + + PlayerLeaderboardPosition({ + required this.apm, + required this.pps, + required this.vs, + required this.gamesPlayed, + required this.gamesWon, + required this.winrate, + required this.app, + required this.vsapm, + required this.dss, + required this.dsp, + required this.appdsp, + required this.cheese, + required this.gbe, + required this.nyaapp, + required this.area, + required this.estTr, + required this.accOfEst + }); + + PlayerLeaderboardPosition.fromSearchResults(List results){ + apm = results[0]; + pps = results[1]; + vs = results[2]; + gamesPlayed = results[3]; + gamesWon = results[4]; + winrate = results[5]; + app = results[6]; + vsapm = results[7]; + dss = results[8]; + dsp = results[9]; + appdsp = results[10]; + cheese = results[11]; + gbe = results[12]; + nyaapp = results[13]; + area = results[14]; + estTr = results[15]; + accOfEst = results[16]; + } +} + +class LeaderboardPosition{ + int position; + double percentage; + + LeaderboardPosition(this.position, this.percentage); +} + class TetrioPlayersLeaderboard { late String type; late DateTime timestamp; @@ -1163,6 +1230,20 @@ class TetrioPlayersLeaderboard { return lb; } + List getStatRankingSequel(Stats stat){ + List lb = List.from(leaderboard); + lb.sort(((a, b) { + if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){ + return -1; + }else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){ + return 0; + }else{ + return 1; + } + })); + return lb; + } + List getAverageOfRank(String rank){ // i tried to refactor it and that's was terrible if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank"); List filtredLeaderboard = List.from(leaderboard); @@ -1753,6 +1834,19 @@ class TetrioPlayersLeaderboard { } } + PlayerLeaderboardPosition? getLeaderboardPosition(String userID) { + if (leaderboard.indexWhere((element) => element.userId == userID) == -1) return null; + List stats = [Stats.apm, Stats.pps, Stats.vs, Stats.gp, Stats.gw, Stats.wr, + Stats.app, Stats.vsapm, Stats.dss, Stats.dsp, Stats.appdsp, Stats.cheese, Stats.gbe, Stats.nyaapp, Stats.area, Stats.eTR, Stats.acceTR]; + List results = []; + for (Stats stat in stats) { + List sortedLeaderboard = getStatRanking(leaderboard, stat, reversed: false); + int position = sortedLeaderboard.indexWhere((element) => element.userId == userID) + 1; + results.add(LeaderboardPosition(position, position / sortedLeaderboard.length)); + } + return PlayerLeaderboardPosition.fromSearchResults(results); + } + Map> get averages => { 'x': getAverageOfRank("x"), 'u': getAverageOfRank("u"), diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 073a7fa..a99c8c7 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -74,6 +74,7 @@ class TetrioService extends DB { final Map> _recordsCache = {}; final Map _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]} final Map _leaderboardsCache = {}; + final Map _lbPositions = {}; final Map> _newsCache = {}; final Map> _topTRcache = {}; final Map _tlStreamsCache = {}; @@ -142,6 +143,14 @@ class TetrioService extends DB { db.insert(tetrioTLReplayStatsTable, {idCol: replay.id, "data": jsonEncode(replay.toJson())}); } + void cacheLeaderboardPositions(String userID, PlayerLeaderboardPosition positions){ + _lbPositions[userID] = positions; + } + + PlayerLeaderboardPosition? getCachedLeaderboardPositions(String userID){ + return _lbPositions[userID]; + } + /// Downloads replay from inoue (szy API). Requiers [replayID]. If request have /// different from 200 statusCode, it will throw an excepction. Returns list, that contains same replay /// as string and as binary. @@ -504,6 +513,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: + _lbPositions.clear(); var rawJson = jsonDecode(response.body); if (rawJson['success']) { // if api confirmed that everything ok TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at'])); diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart new file mode 100644 index 0000000..52e2c2a --- /dev/null +++ b/lib/utils/numers_formats.dart @@ -0,0 +1,7 @@ +import 'package:intl/intl.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; + +final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; +final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); +final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); +final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); \ No newline at end of file diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart index d942746..5c9334e 100644 --- a/lib/views/calc_view.dart +++ b/lib/views/calc_view.dart @@ -13,7 +13,6 @@ double? vs; NerdStats? nerdStats; EstTr? estTr; Playstyle? playstyle; -final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); late String oldWindowTitle; class CalcView extends StatefulWidget { diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 3c630e8..740a8a7 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -31,6 +31,7 @@ import 'package:go_router/go_router.dart'; Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up TetrioPlayersLeaderboard? everyone; +PlayerLeaderboardPosition? meAmongEveryone; String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for String _titleNickname = "dan63047"; final TetrioService teto = TetrioService(); // thing, that manadge our local DB @@ -168,8 +169,14 @@ class _MainState extends State with TickerProviderStateMixin { news = requests[2] as List; topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR - // Get tetra League leaderboard if needed - // if(prefs.getBool("loadLeaderboard") == true) everyone = await teto.fetchTLLeaderboard(); + meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); + if (meAmongEveryone == null && prefs.getBool("showPositions") == true){ + // Get tetra League leaderboard + everyone = teto.getCachedLeaderboard(); + everyone ??= await teto.fetchTLLeaderboard(); + meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me.userId); + if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); + } // Making list of Tetra League matches List tlMatches = []; @@ -394,7 +401,15 @@ class _MainState extends State with TickerProviderStateMixin { body: TabBarView( controller: _tabController, children: [ - TLThingy(tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, states: snapshot.data![2], topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon"), + TLThingy( + tl: snapshot.data![0].tlSeason1, + userID: snapshot.data![0].userId, + states: snapshot.data![2], + topTR: snapshot.data![7], + bot: snapshot.data![0].role == "bot", + guest: snapshot.data![0].role == "anon", + lbPositions: meAmongEveryone + ), _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]), _History(states: snapshot.data![2], update: _justUpdate), _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank), @@ -958,71 +973,58 @@ class _RecordThingy extends StatelessWidget { else if (record!.stream.contains("blitz")) Text(t.blitz, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), // show main metric - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceAround, - crossAxisAlignment: WrapCrossAlignment.center, - clipBehavior: Clip.hardEdge, + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + textBaseline: TextBaseline.alphabetic, children: [ // Show grade based on closest rank average - if (record!.stream.contains("40l")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) - else if (record!.stream.contains("blitz")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96), + if (record!.stream.contains("40l")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 48) + else if (record!.stream.contains("blitz")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 48), - // TODO: I'm not sure abour that element. Maybe, it could be done differenly - Column( - children: [ - // Show result - if (record!.stream.contains("40l")) Text(get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - else if (record!.stream.contains("blitz")) Text(NumberFormat.decimalPattern().format(record!.endContext!.score), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - - // Show difference between rank average - if (record!.stream.contains("40l") && (rank != null && rank != "z")) Text( - "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: sprintBetterThanRankAverage??false ? - Colors.greenAccent : - Colors.redAccent - ) - ) - else if (record!.stream.contains("40l") && (rank == null || rank == "z")) Text( - "${readableTimeDifference(record!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: sprintBetterThanClosestAverage ? - Colors.greenAccent : - Colors.redAccent - ) - ) - else if (record!.stream.contains("blitz") && (rank != null && rank != "z")) Text( - "${readableIntDifference(record!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: blitzBetterThanRankAverage??false ? - Colors.greenAccent : - Colors.redAccent - ) - ) - else if (record!.stream.contains("blitz") && (rank == null || rank == "z")) Text( - "${readableIntDifference(record!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: blitzBetterThanClosestAverage ? - Colors.greenAccent : - Colors.redAccent - ) - ), - ], - ), + // Show result + if (record!.stream.contains("40l")) Text(get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + else if (record!.stream.contains("blitz")) Text(NumberFormat.decimalPattern().format(record!.endContext!.score), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), ], ), - // if (record!.stream.contains("40l")) Text(get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - // else if (record!.stream.contains("blitz")) Text(NumberFormat.decimalPattern().format(record!.endContext!.score), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - // // Compare with averages - // if (record!.stream.contains("40l") && rank != null) RichText(text: TextSpan(text: "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", style: TextStyle(fontFamily: "Eurostile Round", color: sprintBetterThanRankAverage??false ? Colors.green : Colors.red))) - // //Text("${record!.endContext!.finalTime - sprintAverages[rank]!}; ${sprintAverages[rank]}; ${get40lTime((record!.endContext!.finalTime - sprintAverages[rank]!).inMicroseconds)}") - // else if (record!.stream.contains("blitz")) Text("${closestAverageBlitz}; ${blitzAverages[rank]}"), + // Show difference between rank average + if (record!.stream.contains("40l") && (rank != null && rank != "z")) Text( + "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: sprintBetterThanRankAverage??false ? + Colors.greenAccent : + Colors.redAccent + ) + ) + else if (record!.stream.contains("40l") && (rank == null || rank == "z")) Text( + "${readableTimeDifference(record!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: sprintBetterThanClosestAverage ? + Colors.greenAccent : + Colors.redAccent + ) + ) + else if (record!.stream.contains("blitz") && (rank != null && rank != "z")) Text( + "${readableIntDifference(record!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: blitzBetterThanRankAverage??false ? + Colors.greenAccent : + Colors.redAccent + ) + ) + else if (record!.stream.contains("blitz") && (rank == null || rank == "z")) Text( + "${readableIntDifference(record!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average", + textAlign: TextAlign.center, + style: TextStyle( + color: blitzBetterThanClosestAverage ? + Colors.greenAccent : + Colors.redAccent + ) + ), // Show rank if presented if (record!.rank != null) StatCellNum(playerStat: record!.rank!, playerStatLabel: "Leaderboard Placement", isScreenBig: bigScreen, higherIsBetter: false), @@ -1054,7 +1056,8 @@ class _RecordThingy extends StatelessWidget { // List of actions Padding(padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), - child: SizedBox(width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, + child: Container(width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, + constraints: BoxConstraints(maxWidth: 452), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( diff --git a/lib/views/mathes_view.dart b/lib/views/mathes_view.dart index 7656d40..0a1c3f5 100644 --- a/lib/views/mathes_view.dart +++ b/lib/views/mathes_view.dart @@ -8,7 +8,6 @@ import 'package:tetra_stats/views/tl_match_view.dart'; import 'package:window_manager/window_manager.dart'; final TetrioService teto = TetrioService(); -final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); late String oldWindowTitle; class MatchesView extends StatefulWidget { diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index d928728..02f4e56 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/views/rank_averages_view.dart'; import 'package:window_manager/window_manager.dart'; import 'main_view.dart'; // lol @@ -40,7 +40,6 @@ class RanksAverages extends State { @override Widget build(BuildContext context) { - final NumberFormat f2 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2; return Scaffold( appBar: AppBar( title: Text(t.rankAveragesViewTitle), diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index b4d88ac..97e3c70 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -26,7 +26,7 @@ class SettingsState extends State { late SharedPreferences prefs; final TetrioService teto = TetrioService(); String defaultNickname = "Checking..."; - late bool loadLeaderboard; + late bool showPositions; final TextEditingController _playertext = TextEditingController(); @override @@ -47,10 +47,10 @@ class SettingsState extends State { Future _getPreferences() async { prefs = await SharedPreferences.getInstance(); - if (prefs.getBool("loadLeaderboard") != null) { - loadLeaderboard = prefs.getBool("loadLeaderboard")!; + if (prefs.getBool("showPositions") != null) { + showPositions = prefs.getBool("showPositions")!; } else { - loadLeaderboard = false; + showPositions = false; } _setDefaultNickname(prefs.getString("player")); } @@ -266,12 +266,12 @@ class SettingsState extends State { onTap: () { Navigator.pushNamed(context, "/customization"); },), - ListTile(title: Text("Load leaderboard on startup"), - subtitle: Text("That will allow app to show additional stats, like..."), - trailing: Switch(value: loadLeaderboard, onChanged: (bool value){ - prefs.setBool("loadLeaderboard", value); + ListTile(title: Text("Show LB position for each stat"), + subtitle: Text("That will impact on app performance..."), + trailing: Switch(value: showPositions, onChanged: (bool value){ + prefs.setBool("showPositions", value); setState(() { - loadLeaderboard = value; + showPositions = value; }); }),), const Divider(), diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index 1f21484..7eb9f29 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -71,7 +71,7 @@ class TLLeaderboardState extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - return const Center(child: Text('Fetching...')); + return const Center(child: CircularProgressIndicator()); case ConnectionState.done: final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers!.length)}"); diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart new file mode 100644 index 0000000..17295ed --- /dev/null +++ b/lib/widgets/gauget_num.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_gauges/gauges.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/widgets/tl_thingy.dart'; + +class GaugetNum extends StatelessWidget { + final num playerStat; + final num? oldPlayerStat; + final bool higherIsBetter; + final List ranges; + final double minimum; + final double maximum; + final String playerStatLabel; + final String? okText; + final String? alertTitle; + final List? alertWidgets; + final LeaderboardPosition? pos; + + const GaugetNum( + {super.key, + required this.playerStat, + required this.playerStatLabel, + this.alertWidgets, + this.oldPlayerStat, + required this.higherIsBetter, + required this.minimum, + required this.maximum, + required this.ranges, + this.okText, this.alertTitle, this.pos}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200, + height: 120, + child: SfRadialGauge( + title: GaugeTitle(text: playerStatLabel), + axes: [RadialAxis( + startAngle: 180, + endAngle: 360, + showLabels: false, + showTicks: false, + radiusFactor: 2.1, + centerY: 0.5, + minimum: minimum, + maximum: maximum, + ranges: ranges, + pointers: [ + NeedlePointer( + value: playerStat as double, + enableAnimation: true, + needleLength: 0.9, + needleStartWidth: 2, + needleEndWidth: 15, + knobStyle: const KnobStyle(color: Colors.transparent), + gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) + ], + annotations: [GaugeAnnotation( + widget: TextButton(child: Text(f3.format(playerStat), + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), + onPressed: (){ + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(alertTitle??playerStatLabel, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + content: SingleChildScrollView(child: ListBody(children: alertWidgets!)), + actions: [ + TextButton( + child: Text(okText??t.popupActions.ok), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + )); + },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05), + if (oldPlayerStat != null || pos != null) GaugeAnnotation( + widget: RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (oldPlayerStat != null) TextSpan(text: comparef.format(playerStat - oldPlayerStat!), style: TextStyle( + color: higherIsBetter ? + oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent : + oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent + ),), + if ((oldTl != null && oldTl!.gamesPlayed > 0) && pos != null) const TextSpan(text: " • "), + if (pos != null) TextSpan(text: pos!.position >= 1000 ? "Top ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") + ] + ), + ), + positionFactor: 0.05)], + )],), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/list_tile_trailing_stats.dart b/lib/widgets/list_tile_trailing_stats.dart index c5b223b..5690929 100644 --- a/lib/widgets/list_tile_trailing_stats.dart +++ b/lib/widgets/list_tile_trailing_stats.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; class TrailingStats extends StatelessWidget{ final double yourAPM; @@ -14,7 +13,6 @@ class TrailingStats extends StatelessWidget{ @override Widget build(BuildContext context) { - final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100); return Table( defaultColumnWidth: const IntrinsicColumnWidth(), diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 6a11a89..09bead9 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -1,6 +1,8 @@ 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'; class StatCellNum extends StatelessWidget { const StatCellNum( @@ -12,7 +14,7 @@ class StatCellNum extends StatelessWidget { this.fractionDigits, this.oldPlayerStat, required this.higherIsBetter, - this.okText, this.alertTitle}); + this.okText, this.alertTitle, this.pos}); final num playerStat; final num? oldPlayerStat; @@ -23,11 +25,11 @@ class StatCellNum extends StatelessWidget { final String? alertTitle; final List? alertWidgets; final int? fractionDigits; + final LeaderboardPosition? pos; @override Widget build(BuildContext context) { NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = fractionDigits ?? 0; - NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); NumberFormat fractionf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits ?? 0)..maximumIntegerDigits = 0; num fraction = playerStat.isNegative ? 1 - (playerStat - playerStat.floor()) : playerStat - playerStat.floor(); int integer = playerStat.isNegative ? (playerStat + fraction).toInt() : (playerStat - fraction).toInt(); @@ -48,11 +50,20 @@ class StatCellNum extends StatelessWidget { ) ) ), - if (oldPlayerStat != null) Text(comparef.format(playerStat - oldPlayerStat!), style: TextStyle( - color: higherIsBetter ? - oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent : - oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent - ),), + if (oldPlayerStat != null || pos != null) RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (oldPlayerStat != null) TextSpan(text: comparef.format(playerStat - oldPlayerStat!), style: TextStyle( + color: higherIsBetter ? + oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent : + oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent + ),), + if (oldPlayerStat != null && pos != null) const TextSpan(text: " • "), + if (pos != null) TextSpan(text: pos!.position >= 1000 ? "Top ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") + ] + ), + ), alertWidgets == null ? Text( playerStatLabel, diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 175a93a..dc59322 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -3,13 +3,14 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; var fDiff = NumberFormat("+#,###.###;-#,###.###"); +var intFDiff = NumberFormat("+#,###;-#,###"); final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); -final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); -final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); late RangeValues _currentRangeValues; TetraLeagueAlpha? oldTl; late TetraLeagueAlpha currentTl; @@ -23,7 +24,8 @@ class TLThingy extends StatefulWidget { final bool bot; final bool guest; final double? topTR; - const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR}); + final PlayerLeaderboardPosition? lbPositions; + const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions}); @override State createState() => _TLThingyState(); @@ -47,6 +49,8 @@ class _TLThingyState extends State { @override Widget build(BuildContext context) { final t = Translations.of(context); + NumberFormat fractionfEstTR = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..maximumIntegerDigits = 0; + NumberFormat fractionfEstTRAcc = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3)..maximumIntegerDigits = 0; if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,)); return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; @@ -159,13 +163,13 @@ class _TLThingyState extends State { crossAxisAlignment: WrapCrossAlignment.start, clipBehavior: Clip.hardEdge, children: [ - if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm), - if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps), - if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs), + if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm, pos: widget.lbPositions?.apm), + if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps, pos: widget.lbPositions?.pps), + if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs, pos: widget.lbPositions?.vs), if (currentTl.standingLocal > 0) StatCellNum(playerStat: currentTl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), - StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed), - StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon), - StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null), + StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed, pos: widget.lbPositions?.gamesPlayed), + StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon, pos: widget.lbPositions?.gamesWon), + StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null, pos: widget.lbPositions?.winrate), ], ), ), @@ -176,138 +180,31 @@ class _TLThingyState extends State { Padding( padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 35, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - SizedBox( - width: 200, - height: 120, - child: SfRadialGauge( - title: GaugeTitle(text: t.statCellNum.app), - axes: [RadialAxis( - startAngle: 180, - endAngle: 360, - showLabels: false, - showTicks: false, - radiusFactor: 2.1, - centerY: 0.5, - minimum: 0, - maximum: 1, - ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], - pointers: [ - NeedlePointer( - value: currentTl.nerdStats!.app, - enableAnimation: true, - needleLength: 0.9, - needleStartWidth: 2, - needleEndWidth: 15, - knobStyle: const KnobStyle(color: Colors.transparent), - gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) - ], - annotations: [GaugeAnnotation( - widget: TextButton(child: Text(f3.format(currentTl.nerdStats!.app), - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), - onPressed: (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.statCellNum.app, - style: const TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.app}") - ]), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - )); - },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05,), - if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.app - oldTl!.nerdStats!.app), style: TextStyle( - color: currentTl.nerdStats!.app - oldTl!.nerdStats!.app < 0 ? - Colors.redAccent : - Colors.greenAccent - ),), positionFactor: 0.05,)], - )],), - ), - SizedBox( - width: 200, - height: 120, - child: SfRadialGauge( - title: const GaugeTitle(text: "VS / APM"), - axes: [RadialAxis( - startAngle: 180, - endAngle: 360, - showTicks: false, - showLabels: false, - radiusFactor: 2.1, - centerY: 0.5, - minimum: 1.8, - maximum: 2.4, - ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], - pointers: [ - NeedlePointer( - value: currentTl.nerdStats!.vsapm, - enableAnimation: true, - needleLength: 0.9, - needleStartWidth: 2, - needleEndWidth: 15, - knobStyle: const KnobStyle(color: Colors.transparent), - gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) - ], - annotations: [GaugeAnnotation( - widget: TextButton(child: Text(f3.format(currentTl.nerdStats!.vsapm), - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), - onPressed: (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: const Text("VS / APM", - style: TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") - ]), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - )); - },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05), - if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm), style: TextStyle( - color: currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm < 0 ? - Colors.redAccent : - Colors.greenAccent - ),), positionFactor: 0.05,)], - )],), - ),]), + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 35, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + GaugetNum(playerStat: currentTl.nerdStats!.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ + GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), + GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), + GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), + GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), + GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.appDescription), + Text("${t.exactValue}: ${currentTl.nerdStats!.app}") + ], oldPlayerStat: oldTl?.nerdStats?.app, pos: widget.lbPositions?.app), + GaugetNum(playerStat: currentTl.nerdStats!.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ + GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), + GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), + GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.vsapmDescription), + Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") + ], oldPlayerStat: oldTl?.nerdStats?.vsapm, pos: widget.lbPositions?.vsapm) + ]), ), Wrap( direction: Axis.horizontal, @@ -317,6 +214,7 @@ class _TLThingyState extends State { clipBehavior: Clip.hardEdge, children: [ StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, + pos: widget.lbPositions?.dss, alertWidgets: [Text(t.statCellNum.dssDescription), Text("${t.formula}: (VS / 100) - (APM / 60)"), Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], @@ -324,6 +222,7 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.dss,), StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, + pos: widget.lbPositions?.dsp, alertWidgets: [Text(t.statCellNum.dspDescription), Text("${t.formula}: DS/S / PPS"), Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], @@ -331,6 +230,7 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.dsp,), StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, + pos: widget.lbPositions?.appdsp, alertWidgets: [Text(t.statCellNum.appdspDescription), Text("${t.formula}: APP + DS/P"), Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], @@ -338,6 +238,7 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.appdsp,), StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, + pos: widget.lbPositions?.cheese, alertWidgets: [Text(t.statCellNum.cheeseDescription), Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], @@ -345,6 +246,7 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.cheese,), StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, + pos: widget.lbPositions?.gbe, alertWidgets: [Text(t.statCellNum.gbeDescription), Text("${t.formula}: APP * DS/P * 2"), Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], @@ -352,6 +254,7 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.gbe,), StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, + pos: widget.lbPositions?.nyaapp, alertWidgets: [Text(t.statCellNum.nyaappDescription), Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], @@ -359,6 +262,7 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.nyaapp,), StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, + pos: widget.lbPositions?.area, alertWidgets: [Text(t.statCellNum.areaDescription), Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], @@ -370,42 +274,104 @@ class _TLThingyState extends State { ), if (currentTl.estTr != null) Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), - child: SizedBox( + padding: const EdgeInsets.fromLTRB(0, 48, 0, 48), + child: Container( + //alignment: Alignment.center, width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + constraints: BoxConstraints(maxWidth: 768), + child: Wrap( + alignment: WrapAlignment.spaceBetween, + spacing: 20, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "${bigScreen ? t.statCellNum.estOfTR : t.statCellNum.estOfTRShort}:", - style: const TextStyle(fontSize: 24), + Text(t.statCellNum.estOfTR, style: TextStyle(height: 0.1),), + RichText( + text: TextSpan( + text: intf.format(currentTl.estTr!.esttr.truncate()), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500), + children: [TextSpan(text: fractionfEstTR.format(currentTl.estTr!.esttr - currentTl.estTr!.esttr.truncate()).substring(1), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), ), - Text( - f2.format(currentTl.estTr!.esttr), - style: const TextStyle(fontSize: 24), + if (oldTl?.estTr?.esttr != null || widget.lbPositions != null) RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), + children: [ + if (oldTl?.estTr?.esttr != null) TextSpan(text: comparef.format(currentTl.estTr!.esttr - oldTl!.estTr!.esttr), style: TextStyle( + color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent + ),), + if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), + if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.estTr.percentage*100)}%" : "№${widget.lbPositions!.estTr.position}") + ] + ), ), - ], - ), - if (currentTl.rating >= 0) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ],), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(t.statCellNum.accOfEst, style: const TextStyle(height: 0.1),), + RichText( + text: TextSpan( + text: (currentTl.esttracc != null) ? intFDiff.format(currentTl.esttracc!.truncate()) : "-", + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500), + children: [ + TextSpan(text: (currentTl.esttracc != null) ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) + ] + ), + ), + if (oldTl?.esttracc != null || widget.lbPositions != null) RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), children: [ - Text( - "${bigScreen ? t.statCellNum.accOfEst : t.statCellNum.accOfEstShort}:", - style: const TextStyle(fontSize: 24), - ), - Text( - fDiff.format(currentTl.esttracc!), - style: const TextStyle(fontSize: 24), - ), - ], + if (oldTl?.esttracc != null) TextSpan(text: comparef.format(currentTl.esttracc! - oldTl!.esttracc!), style: TextStyle( + color: oldTl!.esttracc! > currentTl.esttracc! ? Colors.redAccent : Colors.greenAccent + ),), + if (oldTl?.esttracc != null && widget.lbPositions?.accOfEst != null) const TextSpan(text: " • "), + if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.accOfEst.percentage*100)}%" : "№${widget.lbPositions!.accOfEst.position}") + ] + ), ), + ],) ], ), - ), + ) + // child: Container( + // width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, + // constraints: BoxConstraints(maxWidth: 452), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "${bigScreen ? t.statCellNum.estOfTR : t.statCellNum.estOfTRShort}:", + // style: const TextStyle(fontSize: 24), + // ), + // Text( + // f2.format(currentTl.estTr!.esttr), + // style: const TextStyle(fontSize: 24), + // ), + // ], + // ), + // if (currentTl.rating >= 0) + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "${bigScreen ? t.statCellNum.accOfEst : t.statCellNum.accOfEstShort}:", + // style: const TextStyle(fontSize: 24), + // ), + // Text( + // fDiff.format(currentTl.esttracc!), + // style: const TextStyle(fontSize: 24), + // ), + // ], + // ), + // ], + // ), + // ), ), if (currentTl.nerdStats != null) Graphs(currentTl.apm!, currentTl.pps!, currentTl.vs!, currentTl.nerdStats!, currentTl.playstyle!) ] From 66acbc2d486d775d51f3250b0de81fe37ed0b17e Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 8 Mar 2024 02:06:35 +0300 Subject: [PATCH 06/14] Stats ranks for everyone --- lib/data_objects/tetrio.dart | 69 +++++++++++++++++++++++------------- lib/views/main_view.dart | 2 +- lib/widgets/tl_thingy.dart | 4 +-- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index c9fa2f3..f5cffb2 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -313,6 +313,10 @@ class TetrioPlayer { return tlSeason1.lessStrictCheck(other.tlSeason1); } + TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard() => TetrioPlayerFromLeaderboard( + userId, username, role, xp, country, supporterTier > 0, verified, state, gamesPlayed, gamesWon, + tlSeason1.rating, tlSeason1.glicko??0, tlSeason1.rd??noTrRd, tlSeason1.rank, tlSeason1.bestRank, tlSeason1.apm??0, tlSeason1.pps??0, tlSeason1.vs??0, tlSeason1.decaying); + @override String toString() { return "$username ($state)"; @@ -1140,23 +1144,23 @@ class News { } class PlayerLeaderboardPosition{ - late LeaderboardPosition apm; - late LeaderboardPosition pps; - late LeaderboardPosition vs; - late LeaderboardPosition gamesPlayed; - late LeaderboardPosition gamesWon; - late LeaderboardPosition winrate; - late LeaderboardPosition app; - late LeaderboardPosition vsapm; - late LeaderboardPosition dss; - late LeaderboardPosition dsp; - late LeaderboardPosition appdsp; - late LeaderboardPosition cheese; - late LeaderboardPosition gbe; - late LeaderboardPosition nyaapp; - late LeaderboardPosition area; - late LeaderboardPosition estTr; - late LeaderboardPosition accOfEst; + late LeaderboardPosition? apm; + late LeaderboardPosition? pps; + late LeaderboardPosition? vs; + late LeaderboardPosition? gamesPlayed; + late LeaderboardPosition? gamesWon; + late LeaderboardPosition? winrate; + late LeaderboardPosition? app; + late LeaderboardPosition? vsapm; + late LeaderboardPosition? dss; + late LeaderboardPosition? dsp; + late LeaderboardPosition? appdsp; + late LeaderboardPosition? cheese; + late LeaderboardPosition? gbe; + late LeaderboardPosition? nyaapp; + late LeaderboardPosition? area; + late LeaderboardPosition? estTr; + late LeaderboardPosition? accOfEst; PlayerLeaderboardPosition({ required this.apm, @@ -1178,7 +1182,7 @@ class PlayerLeaderboardPosition{ required this.accOfEst }); - PlayerLeaderboardPosition.fromSearchResults(List results){ + PlayerLeaderboardPosition.fromSearchResults(List results){ apm = results[0]; pps = results[1]; vs = results[2]; @@ -1834,15 +1838,26 @@ class TetrioPlayersLeaderboard { } } - PlayerLeaderboardPosition? getLeaderboardPosition(String userID) { - if (leaderboard.indexWhere((element) => element.userId == userID) == -1) return null; + PlayerLeaderboardPosition? getLeaderboardPosition(TetrioPlayer user) { + if (user.tlSeason1.gamesPlayed == 0) return null; + bool fakePositions = false; + late List copyOfLeaderboard; + if (leaderboard.indexWhere((element) => element.userId == user.userId) == -1){ + fakePositions =true; + copyOfLeaderboard = List.of(leaderboard); + copyOfLeaderboard.add(user.convertToPlayerFromLeaderboard()); + } List stats = [Stats.apm, Stats.pps, Stats.vs, Stats.gp, Stats.gw, Stats.wr, Stats.app, Stats.vsapm, Stats.dss, Stats.dsp, Stats.appdsp, Stats.cheese, Stats.gbe, Stats.nyaapp, Stats.area, Stats.eTR, Stats.acceTR]; - List results = []; + List results = []; for (Stats stat in stats) { - List sortedLeaderboard = getStatRanking(leaderboard, stat, reversed: false); - int position = sortedLeaderboard.indexWhere((element) => element.userId == userID) + 1; - results.add(LeaderboardPosition(position, position / sortedLeaderboard.length)); + List sortedLeaderboard = getStatRanking(fakePositions ? copyOfLeaderboard : leaderboard, stat, reversed: false); + int position = sortedLeaderboard.indexWhere((element) => element.userId == user.userId) + 1; + if (position == 0) { + results.add(null); + } else { + results.add(LeaderboardPosition(fakePositions ? 1001 : position, position / sortedLeaderboard.length)); + } } return PlayerLeaderboardPosition.fromSearchResults(results); } @@ -1921,7 +1936,11 @@ class TetrioPlayerFromLeaderboard { this.apm, this.pps, this.vs, - this.decaying); + this.decaying){ + nerdStats = NerdStats(apm, pps, vs); + estTr = EstTr(apm, pps, vs, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); + playstyle = Playstyle(apm, pps, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); + } double get winrate => gamesWon / gamesPlayed; double get esttracc => estTr.esttr - rating; diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 740a8a7..1a576fd 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -174,7 +174,7 @@ class _MainState extends State with TickerProviderStateMixin { // Get tetra League leaderboard everyone = teto.getCachedLeaderboard(); everyone ??= await teto.fetchTLLeaderboard(); - meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me.userId); + meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index dc59322..ecd13ee 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -302,7 +302,7 @@ class _TLThingyState extends State { color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent ),), if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), - if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.estTr.percentage*100)}%" : "№${widget.lbPositions!.estTr.position}") + if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}") ] ), ), @@ -328,7 +328,7 @@ class _TLThingyState extends State { color: oldTl!.esttracc! > currentTl.esttracc! ? Colors.redAccent : Colors.greenAccent ),), if (oldTl?.esttracc != null && widget.lbPositions?.accOfEst != null) const TextSpan(text: " • "), - if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.accOfEst.percentage*100)}%" : "№${widget.lbPositions!.accOfEst.position}") + if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst!.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.accOfEst!.percentage*100)}%" : "№${widget.lbPositions!.accOfEst!.position}") ] ), ), From e952edb7dcc7866e29006e3b31ee886c1112994b Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 11 Mar 2024 01:34:30 +0300 Subject: [PATCH 07/14] Comparing against rank averages --- lib/data_objects/tetrio.dart | 2 +- lib/views/main_view.dart | 7 ++++-- lib/views/rank_averages_view.dart | 20 +++++++++------- lib/views/settings_view.dart | 4 ++-- lib/widgets/stat_sell_num.dart | 22 ++++++++++++----- lib/widgets/tl_thingy.dart | 39 +++++++++++++++++++------------ 6 files changed, 60 insertions(+), 34 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index f5cffb2..d67a38b 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -1851,7 +1851,7 @@ class TetrioPlayersLeaderboard { Stats.app, Stats.vsapm, Stats.dss, Stats.dsp, Stats.appdsp, Stats.cheese, Stats.gbe, Stats.nyaapp, Stats.area, Stats.eTR, Stats.acceTR]; List results = []; for (Stats stat in stats) { - List sortedLeaderboard = getStatRanking(fakePositions ? copyOfLeaderboard : leaderboard, stat, reversed: false); + List sortedLeaderboard = getStatRanking(fakePositions ? copyOfLeaderboard : leaderboard, stat, reversed: stat == Stats.cheese ? true : false); int position = sortedLeaderboard.indexWhere((element) => element.userId == user.userId) + 1; if (position == 0) { results.add(null); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 1a576fd..1819f64 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -32,6 +32,7 @@ import 'package:go_router/go_router.dart'; Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up TetrioPlayersLeaderboard? everyone; PlayerLeaderboardPosition? meAmongEveryone; +TetraLeagueAlpha? rankAverages; String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for String _titleNickname = "dan63047"; final TetrioService teto = TetrioService(); // thing, that manadge our local DB @@ -73,14 +74,14 @@ String get40lTime(int microseconds){ String readableTimeDifference(Duration a, Duration b){ Duration result = a - b; - return "${NumberFormat("0.000s;0.000s", LocaleSettings.currentLocale.languageCode).format(result.inMilliseconds/1000)}"; + 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)}"; + return NumberFormat("#,###;#,###", LocaleSettings.currentLocale.languageCode).format(result); } class _MainState extends State with TickerProviderStateMixin { @@ -178,6 +179,8 @@ class _MainState extends State with TickerProviderStateMixin { if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } + if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; + // Making list of Tetra League matches List tlMatches = []; bool isTracking = await teto.isPlayerTracking(me.userId); diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 96ffe5c..19651d5 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -16,6 +16,7 @@ Stats _chartsX = Stats.tr; Stats _chartsY = Stats.apm; List _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; Stats _sortBy = Stats.tr; +late List they; bool _reversed = false; List _itemCountries = [for (MapEntry e in t.countries.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; String _country = ""; @@ -61,6 +62,7 @@ class RankState extends State with SingleTickerProviderStateMixin { } super.initState(); previousAxisTitles = _chartsX.toString()+_chartsY.toString(); + they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); recalculateBoundaries(); resetScale(); } @@ -73,7 +75,7 @@ class RankState extends State with SingleTickerProviderStateMixin { } else { return element; } - }).getStatByEnum(_chartsX) as double; + }).getStatByEnum(_chartsX).toDouble(); actualMaxX = (widget.rank[1]["entries"] as List).reduce((value, element) { num n = max(value.getStatByEnum(_chartsX), element.getStatByEnum(_chartsX)); if (value.getStatByEnum(_chartsX) == n) { @@ -81,7 +83,7 @@ class RankState extends State with SingleTickerProviderStateMixin { } else { return element; } - }).getStatByEnum(_chartsX) as double; + }).getStatByEnum(_chartsX).toDouble(); actualMinY = (widget.rank[1]["entries"] as List).reduce((value, element) { num n = min(value.getStatByEnum(_chartsY), element.getStatByEnum(_chartsY)); if (value.getStatByEnum(_chartsY) == n) { @@ -89,7 +91,7 @@ class RankState extends State with SingleTickerProviderStateMixin { } else { return element; } - }).getStatByEnum(_chartsY) as double; + }).getStatByEnum(_chartsY).toDouble(); actualMaxY = (widget.rank[1]["entries"] as List).reduce((value, element) { num n = max(value.getStatByEnum(_chartsY), element.getStatByEnum(_chartsY)); if (value.getStatByEnum(_chartsY) == n) { @@ -97,7 +99,7 @@ class RankState extends State with SingleTickerProviderStateMixin { } else { return element; } - }).getStatByEnum(_chartsY) as double; + }).getStatByEnum(_chartsY).toDouble(); } void resetScale(){ @@ -164,7 +166,7 @@ class RankState extends State with SingleTickerProviderStateMixin { previousAxisTitles = _chartsX.toString()+_chartsY.toString(); } final t = Translations.of(context); - List they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); + //they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); return Scaffold( appBar: AppBar( title: Text(widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())), @@ -327,8 +329,8 @@ class RankState extends State with SingleTickerProviderStateMixin { for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"]) if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception _MyScatterSpot( - entry.getStatByEnum(_chartsX) as double, - entry.getStatByEnum(_chartsY) as double, + entry.getStatByEnum(_chartsX).toDouble(), + entry.getStatByEnum(_chartsY).toDouble(), entry.userId, entry.username, dotPainter: FlDotCirclePainter(color: rankColors[entry.rank]??Colors.white, radius: 3)) @@ -403,7 +405,9 @@ class RankState extends State with SingleTickerProviderStateMixin { value: _sortBy, onChanged: ((value) { _sortBy = value; - setState(() {}); + setState(() { + they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); + }); }), ), ], diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 97e3c70..3aa1c11 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -266,8 +266,8 @@ class SettingsState extends State { onTap: () { Navigator.pushNamed(context, "/customization"); },), - ListTile(title: Text("Show LB position for each stat"), - subtitle: Text("That will impact on app performance..."), + ListTile(title: Text("Show leaderboard based stats"), + subtitle: Text("That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values"), trailing: Switch(value: showPositions, onChanged: (bool value){ prefs.setBool("showPositions", value); setState(() { diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 09bead9..839457a 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -10,11 +10,12 @@ class StatCellNum extends StatelessWidget { required this.playerStat, required this.playerStatLabel, required this.isScreenBig, + this.smallDecimal = true, this.alertWidgets, this.fractionDigits, this.oldPlayerStat, required this.higherIsBetter, - this.okText, this.alertTitle, this.pos}); + this.okText, this.alertTitle, this.pos, this.averageStat}); final num playerStat; final num? oldPlayerStat; @@ -22,10 +23,22 @@ class StatCellNum extends StatelessWidget { final String playerStatLabel; final String? okText; final bool isScreenBig; + final bool smallDecimal; final String? alertTitle; final List? alertWidgets; final int? fractionDigits; final LeaderboardPosition? pos; + final num? averageStat; + + Color getStatColor(){ + if (averageStat == null) return Colors.white; + num percentile = (higherIsBetter ? playerStat / averageStat! : averageStat! / playerStat).abs(); + if (percentile > 1.50) return Colors.purpleAccent; + else if (percentile > 1.20) return Colors.blueAccent; + else if (percentile > 0.90) return Colors.greenAccent; + else if (percentile > 0.70) return Colors.yellowAccent; + else return Colors.redAccent; + } @override Widget build(BuildContext context) { @@ -33,20 +46,17 @@ class StatCellNum extends StatelessWidget { NumberFormat fractionf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits ?? 0)..maximumIntegerDigits = 0; num fraction = playerStat.isNegative ? 1 - (playerStat - playerStat.floor()) : playerStat - playerStat.floor(); int integer = playerStat.isNegative ? (playerStat + fraction).toInt() : (playerStat - fraction).toInt(); - // String valueAsString = fractionDigits == null ? f.format(playerStat.floor()) : f.format(playerStat); - // var exploded = valueAsString.split("."); return Column( children: [ RichText( text: TextSpan(text: intf.format(integer), children: [ - TextSpan(text: fractionf.format(fraction).substring(1), style: const TextStyle(fontSize: 16)) + TextSpan(text: fractionf.format(fraction).substring(1), style: smallDecimal ? const TextStyle(fontSize: 16) : null) ], style: TextStyle( fontFamily: "Eurostile Round Extended", - //fontWeight: FontWeight.bold, fontSize: isScreenBig ? 32 : 24, - color: Colors.white + color: getStatColor() ) ) ), diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index ecd13ee..6db515d 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -4,6 +4,7 @@ import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; @@ -25,7 +26,8 @@ class TLThingy extends StatefulWidget { final bool guest; final double? topTR; final PlayerLeaderboardPosition? lbPositions; - const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions}); + final TetraLeagueAlpha? averages; + const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages}); @override State createState() => _TLThingyState(); @@ -150,7 +152,7 @@ class _TLThingyState extends State { softWrap: true, textAlign: TextAlign.center, style: TextStyle( - fontFamily: "Eurostile Round Extended", + fontFamily: "Eurostile Round", fontSize: bigScreen ? 42 : 28, overflow: TextOverflow.visible, )), @@ -163,13 +165,13 @@ class _TLThingyState extends State { crossAxisAlignment: WrapCrossAlignment.start, clipBehavior: Clip.hardEdge, children: [ - if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm, pos: widget.lbPositions?.apm), - if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps, pos: widget.lbPositions?.pps), - if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs, pos: widget.lbPositions?.vs), + if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm, pos: widget.lbPositions?.apm, averageStat: rankAverages?.apm), + if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps, pos: widget.lbPositions?.pps, averageStat: rankAverages?.pps, smallDecimal: false), + if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs, pos: widget.lbPositions?.vs, averageStat: rankAverages?.vs), if (currentTl.standingLocal > 0) StatCellNum(playerStat: currentTl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed, pos: widget.lbPositions?.gamesPlayed), StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon, pos: widget.lbPositions?.gamesWon), - StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null, pos: widget.lbPositions?.winrate), + StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null, pos: widget.lbPositions?.winrate, averageStat: rankAverages != null ? rankAverages!.winrate * 100 : null), ], ), ), @@ -214,7 +216,8 @@ class _TLThingyState extends State { clipBehavior: Clip.hardEdge, children: [ StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - pos: widget.lbPositions?.dss, + pos: widget.lbPositions?.dss, + averageStat: rankAverages?.nerdStats?.dss, smallDecimal: false, alertWidgets: [Text(t.statCellNum.dssDescription), Text("${t.formula}: (VS / 100) - (APM / 60)"), Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], @@ -222,7 +225,8 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.dss,), StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - pos: widget.lbPositions?.dsp, + pos: widget.lbPositions?.dsp, + averageStat: rankAverages?.nerdStats?.dsp, smallDecimal: false, alertWidgets: [Text(t.statCellNum.dspDescription), Text("${t.formula}: DS/S / PPS"), Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], @@ -230,7 +234,8 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.dsp,), StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - pos: widget.lbPositions?.appdsp, + pos: widget.lbPositions?.appdsp, + averageStat: rankAverages?.nerdStats?.appdsp, smallDecimal: false, alertWidgets: [Text(t.statCellNum.appdspDescription), Text("${t.formula}: APP + DS/P"), Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], @@ -238,15 +243,17 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.appdsp,), StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - pos: widget.lbPositions?.cheese, + pos: widget.lbPositions?.cheese, + averageStat: rankAverages?.nerdStats?.cheese, alertWidgets: [Text(t.statCellNum.cheeseDescription), Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], okText: t.popupActions.ok, - higherIsBetter: true, + higherIsBetter: false, oldPlayerStat: oldTl?.nerdStats?.cheese,), StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - pos: widget.lbPositions?.gbe, + pos: widget.lbPositions?.gbe, + averageStat: rankAverages?.nerdStats?.gbe, smallDecimal: false, alertWidgets: [Text(t.statCellNum.gbeDescription), Text("${t.formula}: APP * DS/P * 2"), Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], @@ -254,7 +261,8 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.gbe,), StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - pos: widget.lbPositions?.nyaapp, + pos: widget.lbPositions?.nyaapp, + averageStat: rankAverages?.nerdStats?.nyaapp, smallDecimal: false, alertWidgets: [Text(t.statCellNum.nyaappDescription), Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], @@ -262,7 +270,8 @@ class _TLThingyState extends State { higherIsBetter: true, oldPlayerStat: oldTl?.nerdStats?.nyaapp,), StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - pos: widget.lbPositions?.area, + pos: widget.lbPositions?.area, + averageStat: rankAverages?.nerdStats?.area, alertWidgets: [Text(t.statCellNum.areaDescription), Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], @@ -314,7 +323,7 @@ class _TLThingyState extends State { RichText( text: TextSpan( text: (currentTl.esttracc != null) ? intFDiff.format(currentTl.esttracc!.truncate()) : "-", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500), + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500), children: [ TextSpan(text: (currentTl.esttracc != null) ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) ] From a726357f6956cd56a9ce863aa2c2820538c6f622 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 12 Mar 2024 02:14:49 +0300 Subject: [PATCH 08/14] Time weighted stats --- .../tetrio_multiplayer_replay.dart | 47 +- lib/views/tl_match_view.dart | 823 +++++++++--------- lib/widgets/gauget_num.dart | 15 +- lib/widgets/tl_thingy.dart | 8 +- 4 files changed, 489 insertions(+), 404 deletions(-) diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 81a3d08..03207c9 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:math'; import 'package:vector_math/vector_math_64.dart'; @@ -151,10 +150,29 @@ class ReplayStats{ } } +class AggregateStats{ + double apm; + double pps; + double vs; + late NerdStats nerdStats; + late EstTr estTr; + late Playstyle playstyle; + double spp; + double kpp; + double kps; + + AggregateStats(this.apm, this.pps, this.vs, this.spp, this.kpp, this.kps){ + nerdStats = NerdStats(apm, pps, vs); + estTr = EstTr(apm, pps, vs, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); + playstyle = Playstyle(apm, pps, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); + } +} + class ReplayData{ late String id; late Map rawJson; late List endcontext; + late List timeWeightedStats; late List> stats; late List totalStats; late List> roundWinners; @@ -181,20 +199,45 @@ class ReplayData{ totalLength = 0; stats = []; roundWinners = []; + int roundID = 0; + List APMmultipliedByWeights = [0, 0]; + List PPSmultipliedByWeights = [0, 0]; + List VSmultipliedByWeights = [0, 0]; + List SPPmultipliedByWeights = [0, 0]; + List KPPmultipliedByWeights = [0, 0]; + List KPSmultipliedByWeights = [0, 0]; totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()]; for(var round in json['data']) { int firstInEndContext = round['replays'][0]["events"].last['data']['export']['options']['gameid'].startsWith(endcontext[0].userId) ? 0 : 1; int secondInEndContext = round['replays'][1]["events"].last['data']['export']['options']['gameid'].startsWith(endcontext[1].userId) ? 1 : 0; - roundLengths.add(max(round['replays'][0]['frames'], round['replays'][1]['frames'])); + int roundLength = max(round['replays'][0]['frames'], round['replays'][1]['frames']); + roundLengths.add(roundLength); totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']); + APMmultipliedByWeights[0] += endcontext[0].secondaryTracking[roundID]*roundLength; + APMmultipliedByWeights[1] += endcontext[1].secondaryTracking[roundID]*roundLength; + PPSmultipliedByWeights[0] += endcontext[0].tertiaryTracking[roundID]*roundLength; + PPSmultipliedByWeights[1] += endcontext[1].tertiaryTracking[roundID]*roundLength; + VSmultipliedByWeights[0] += endcontext[0].extraTracking[roundID]*roundLength; + VSmultipliedByWeights[1] += endcontext[1].extraTracking[roundID]*roundLength; int winner = round['board'].indexWhere((element) => element['success'] == true); roundWinners.add([round['board'][winner]['id'], round['board'][winner]['username']]); ReplayStats playerOne = ReplayStats.fromJson(round['replays'][firstInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][secondInEndContext]['events']), round['replays'][firstInEndContext]['frames']); // (events contain recived attacks) ReplayStats playerTwo = ReplayStats.fromJson(round['replays'][secondInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][firstInEndContext]['events']), round['replays'][secondInEndContext]['frames']); + SPPmultipliedByWeights[0] += playerOne.spp*roundLength; + SPPmultipliedByWeights[1] += playerTwo.spp*roundLength; + KPPmultipliedByWeights[0] += playerOne.kpp*roundLength; + KPPmultipliedByWeights[1] += playerTwo.kpp*roundLength; + KPSmultipliedByWeights[0] += playerOne.kps*roundLength; + KPSmultipliedByWeights[1] += playerTwo.kps*roundLength; stats.add([playerOne, playerTwo]); totalStats[0] = totalStats[0] + playerOne; totalStats[1] = totalStats[1] + playerTwo; + roundID ++; } + timeWeightedStats = [ + AggregateStats(APMmultipliedByWeights[0]/totalLength, PPSmultipliedByWeights[0]/totalLength, VSmultipliedByWeights[0]/totalLength, SPPmultipliedByWeights[0]/totalLength, KPPmultipliedByWeights[0]/totalLength, KPSmultipliedByWeights[0]/totalLength), + AggregateStats(APMmultipliedByWeights[1]/totalLength, PPSmultipliedByWeights[1]/totalLength, VSmultipliedByWeights[1]/totalLength, SPPmultipliedByWeights[1]/totalLength, KPPmultipliedByWeights[1]/totalLength, KPSmultipliedByWeights[1]/totalLength) + ]; } Map toJson(){ diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 373441e..660dc44 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -22,6 +22,9 @@ import 'package:window_manager/window_manager.dart'; final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); int roundSelector = -1; // -1 = match averages, otherwise round number-1 List rounds = []; // index zero will be match stats +bool timeWeightedStatsAvaliable = true; +int greenSidePlayer = 0; +int redSidePlayer = 1; late String oldWindowTitle; Duration framesToTime(int frames){ @@ -60,403 +63,427 @@ class TlMatchResultState extends State { } Widget buildComparison(bool bigScreen, bool showMobileSelector){ - return NestedScrollView( - 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: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], - )), - 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: bigScreen ? const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28) : const TextStyle()), - Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 42)) - ]), + return FutureBuilder(future: replayData, builder: (context, snapshot){ + late Duration time; + late String readableTime; + late String reason; + timeWeightedStatsAvaliable = true; + if (snapshot.connectionState != ConnectionState.done) return const LinearProgressIndicator(); + if (!snapshot.hasError){ + if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, const DropdownMenuItem(value: -2, child: Text("timeWeightedStats"))); + greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); + redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); + if (roundSelector.isNegative){ + time = framesToTime(snapshot.data!.totalLength); + readableTime = "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}"; + }else{ + time = framesToTime(snapshot.data!.roundLengths[roundSelector]); + readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}"; + } + }else{ + timeWeightedStatsAvaliable = false; + switch (snapshot.error.runtimeType){ + case ReplayNotAvalable: + reason = t.matchIsTooOld; + break; + case SzyNotFound: + reason = t.matchIsTooOld; + break; + case SzyForbidden: + reason = t.errors.replayRejected; + break; + case SzyTooManyRequests: + reason = t.errors.tooManyRequests; + break; + default: + reason = snapshot.error.toString(); + break; + } + } + return NestedScrollView( + 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: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], + )), + 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: bigScreen ? const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28) : const TextStyle()), + 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: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.red, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], - )), - 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: bigScreen ? const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28) : const TextStyle()), - 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: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], + )), + 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: bigScreen ? const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28) : const TextStyle()), + Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 42)) + ]), + ), ), ), - ), - ], + ], + ), ), ), - ), - if (showMobileSelector) SliverToBoxAdapter( - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.statsFor}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) { - roundSelector = value; - setState(() {}); - }),), - ], + if (showMobileSelector) SliverToBoxAdapter( + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("${t.statsFor}: ", + style: const TextStyle(color: Colors.white, fontSize: 25)), + DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) { + roundSelector = value; + setState(() {}); + }),), + ], + ), ), ), - ), - if (widget.record.ownId == widget.record.replayId && showMobileSelector) SliverToBoxAdapter( - child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), - ), - if (showMobileSelector) SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) { - switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const LinearProgressIndicator(); - case ConnectionState.done: - if (!snapshot.hasError){ - if (roundSelector.isNegative){ - var time = framesToTime(snapshot.data!.totalLength); - return Center(child: Text("${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center)); - }else{ - var time = framesToTime(snapshot.data!.roundLengths[roundSelector]); - return Center(child: Text("${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}", textAlign: TextAlign.center,)); - } - }else{ - String reason; - switch (snapshot.error.runtimeType){ - case ReplayNotAvalable: - reason = t.matchIsTooOld; - break; - case SzyNotFound: - reason = t.matchIsTooOld; - break; - case SzyForbidden: - reason = t.errors.replayRejected; - break; - case SzyTooManyRequests: - reason = t.errors.tooManyRequests; - break; - default: - reason = snapshot.error.toString(); - break; - } - return Text("${t.replayIssue}: $reason", textAlign: TextAlign.center); - } - - } - },),), - const SliverToBoxAdapter( - child: Divider(), - ) - ]; - }, - body: ListView( - children: [ - Column( - children: [ - CompareThingy( - label: "APM", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "VS", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - FutureBuilder(future: replayData, builder: (BuildContext context, AsyncSnapshot snapshot){ - switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const LinearProgressIndicator(); - case ConnectionState.done: - if (!snapshot.hasError){ - var greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); - var redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); - return Column(children: [ - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, - label: "Inputs", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, - label: "Pieces Placed", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, - label: "KpP", higherIsBetter: false, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, - label: "KpS", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared, - label: "Lines Cleared", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score, - label: "Score", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, - label: "SpP", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100, - label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike, - label: "Best Spike", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo, - label: "Best Combo", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB, - label: "Best BtB", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent, - label: "Sent", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived, - label: "Recived", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack, - label: "Attack", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared, - label: "Cleared", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears, - label: "PC", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins, - label: "T-spins", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads, - label: "Quads", higherIsBetter: true), - ],); - }else{ - return Container(); - } - - } - }) - ], - ), - const Divider(), - Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.nerdStats, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "APP", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds, - redSide: roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs( - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector], - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector] - ) - ], - ), - if (widget.record.ownId != widget.record.replayId) const Divider(), - if (widget.record.ownId != widget.record.replayId) Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Handling", - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, - label: "DAS", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, - label: "ARR", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, - label: "SDF", prefix: "x", - higherIsBetter: true), - CompareBoolThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, - label: "Safe HD", - trueIsBetter: true) - ], - ) - ], + if (widget.record.ownId == widget.record.replayId && showMobileSelector) SliverToBoxAdapter( + child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), + ), + if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(snapshot.hasError ? reason : readableTime, textAlign: TextAlign.center))), + const SliverToBoxAdapter( + child: Divider(), ) - ); + ]; + }, + body: ListView( + children: [ + Column( + children: [ + CompareThingy( + label: "APM", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : + roundSelector == -1 ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "PPS", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps: + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "VS", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + if (snapshot.hasData) Column(children: [ + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, + label: "Inputs", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, + label: "Pieces Placed", higherIsBetter: true), + CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kpp : + roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kpp : + roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, + label: "KpP", higherIsBetter: false, fractionDigits: 2,), + CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kps : + roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kps : + roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, + label: "KpS", higherIsBetter: true, fractionDigits: 2,), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared, + label: "Lines Cleared", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score, + label: "Score", higherIsBetter: true), + CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].spp : + roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].spp : + roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, + label: "SpP", higherIsBetter: true, fractionDigits: 2,), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100, + label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike, + label: "Best Spike", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo, + label: "Best Combo", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB, + label: "Best BtB", higherIsBetter: true), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent, + label: "Sent", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived, + label: "Recived", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack, + label: "Attack", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared, + label: "Cleared", higherIsBetter: true), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears, + label: "PC", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins, + label: "T-spins", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads, + label: "Quads", higherIsBetter: true), + ],), + ], + ), + const Divider(), + Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.nerdStats, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "APP", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.app : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.app : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.vsapm : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.vsapm : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dss : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dss : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.appdsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.appdsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.cheese : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.cheese : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, + fractionDigits: 2, + higherIsBetter: false, + ), + CompareThingy( + label: "Gb Eff.", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.gbe : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.gbe : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "wAPP", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.nyaapp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.nyaapp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.area : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.area : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.estOfTRShort, + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].estTr.esttr : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].estTr.esttr : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Opener", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.opener : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.opener : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Plonk", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.plonk : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.plonk : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Stride", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.stride : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.stride : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Inf. DS", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.infds : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.infds : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds, + fractionDigits: 3, + higherIsBetter: true, + ), + VsGraphs( + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector] + ) + ], + ), + if (widget.record.ownId != widget.record.replayId) const Divider(), + if (widget.record.ownId != widget.record.replayId) Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Handling", + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, + label: "DAS", fractionDigits: 1, postfix: "F", + higherIsBetter: false), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, + label: "ARR", fractionDigits: 1, postfix: "F", + higherIsBetter: false), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, + label: "SDF", prefix: "x", + higherIsBetter: true), + CompareBoolThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, + label: "Safe HD", + trueIsBetter: true) + ], + ) + ], + ) + ); + }); } Widget buildRoundSelector(double width){ return Padding( - padding: EdgeInsets.all(8.0000000), + padding: const EdgeInsets.all(8.000000), child: SizedBox( width: width, child: NestedScrollView( @@ -482,8 +509,8 @@ class TlMatchResultState extends State { RichText( text: TextSpan( text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500), - children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500), + children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] ), ) ],); @@ -512,7 +539,7 @@ class TlMatchResultState extends State { if (widget.record.ownId != widget.record.replayId) Text("${t.replayIssue}: $reason"), if (widget.record.ownId == widget.record.replayId) Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), if (widget.record.ownId != widget.record.replayId) RichText( - text: TextSpan( + text: const TextSpan( text: "-:--", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.grey), children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] @@ -526,7 +553,7 @@ class TlMatchResultState extends State { if (widget.record.ownId != widget.record.replayId) Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text("Number of rounds"), + const Text("Number of rounds"), RichText( text: TextSpan( text: widget.record.endContext.first.secondaryTracking.length > 0 ? widget.record.endContext.first.secondaryTracking.length.toString() : "---", @@ -549,10 +576,12 @@ class TlMatchResultState extends State { roundSelector = -1; setState(() {}); }), - TextButton( child: const Text('Time-weighted match stats'), onPressed: () { - roundSelector = -1; + TextButton( child: const Text('Time-weighted match stats'), + style: roundSelector == -2 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, + onPressed: timeWeightedStatsAvaliable ? () { + roundSelector = -2; setState(() {}); - }), + } : null) , //TextButton( child: const Text('Button 3'), onPressed: () {}), ], ) @@ -595,8 +624,8 @@ class TlMatchResultState extends State { leading:RichText( text: TextSpan( text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500), - children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500), + children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] ), ), title: Text(snapshot.data!.roundWinners[index][1], textAlign: TextAlign.center), @@ -621,13 +650,13 @@ class TlMatchResultState extends State { ), child: ListTile( leading: RichText( - text: TextSpan( + text: const TextSpan( text: "-:--", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.grey), children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] ), ), - title: Text("---", style: TextStyle(color: Colors.grey), textAlign: TextAlign.center), + title: const Text("---", style: TextStyle(color: Colors.grey), textAlign: TextAlign.center), trailing: TrailingStats( widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[index], widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[index], @@ -656,7 +685,7 @@ class TlMatchResultState extends State { if (viewportWidth <= 1024) { return Center( child: Container( - constraints: BoxConstraints(maxWidth: 768), + constraints: const BoxConstraints(maxWidth: 768), child: buildComparison(viewportWidth > 768, true) ), ); @@ -670,7 +699,7 @@ class TlMatchResultState extends State { child: buildComparison(true, false) ), Container( - constraints: BoxConstraints(maxWidth: 768), + constraints: const BoxConstraints(maxWidth: 768), child: buildRoundSelector(max(viewportWidth-768-16, 200)), ) ], diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart index 17295ed..ef83515 100644 --- a/lib/widgets/gauget_num.dart +++ b/lib/widgets/gauget_num.dart @@ -17,6 +17,7 @@ class GaugetNum extends StatelessWidget { final String? alertTitle; final List? alertWidgets; final LeaderboardPosition? pos; + final num? averageStat; const GaugetNum( {super.key, @@ -28,7 +29,17 @@ class GaugetNum extends StatelessWidget { required this.minimum, required this.maximum, required this.ranges, - this.okText, this.alertTitle, this.pos}); + this.okText, this.alertTitle, this.pos, this.averageStat}); + + Color getStatColor(){ + if (averageStat == null) return Colors.white; + num percentile = (higherIsBetter ? playerStat / averageStat! : averageStat! / playerStat).abs(); + if (percentile > 1.50) return Colors.purpleAccent; + else if (percentile > 1.20) return Colors.blueAccent; + else if (percentile > 0.90) return Colors.greenAccent; + else if (percentile > 0.70) return Colors.yellowAccent; + else return Colors.redAccent; + } @override Widget build(BuildContext context) { @@ -59,7 +70,7 @@ class GaugetNum extends StatelessWidget { ], annotations: [GaugeAnnotation( widget: TextButton(child: Text(f3.format(playerStat), - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: getStatColor())), onPressed: (){ showDialog( context: context, diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 6db515d..437253f 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -197,7 +197,8 @@ class _TLThingyState extends State { ], alertWidgets: [ Text(t.statCellNum.appDescription), Text("${t.exactValue}: ${currentTl.nerdStats!.app}") - ], oldPlayerStat: oldTl?.nerdStats?.app, pos: widget.lbPositions?.app), + ], oldPlayerStat: oldTl?.nerdStats?.app, pos: widget.lbPositions?.app, + averageStat: rankAverages?.nerdStats?.app), GaugetNum(playerStat: currentTl.nerdStats!.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), @@ -205,7 +206,8 @@ class _TLThingyState extends State { ], alertWidgets: [ Text(t.statCellNum.vsapmDescription), Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") - ], oldPlayerStat: oldTl?.nerdStats?.vsapm, pos: widget.lbPositions?.vsapm) + ], oldPlayerStat: oldTl?.nerdStats?.vsapm, pos: widget.lbPositions?.vsapm, + averageStat: rankAverages?.nerdStats?.vsapm) ]), ), Wrap( @@ -244,7 +246,7 @@ class _TLThingyState extends State { oldPlayerStat: oldTl?.nerdStats?.appdsp,), StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, pos: widget.lbPositions?.cheese, - averageStat: rankAverages?.nerdStats?.cheese, + //averageStat: rankAverages?.nerdStats?.cheese, TODO: questonable alertWidgets: [Text(t.statCellNum.cheeseDescription), Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], From 8b95a320070bde4ea09a7f27f31b8f248b5b96e6 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 14 Mar 2024 01:44:53 +0300 Subject: [PATCH 09/14] Experimenting with layout --- lib/data_objects/tetrio.dart | 39 ++++++++++++++++++++++ lib/services/tetrio_crud.dart | 8 +++++ lib/views/main_view.dart | 61 ++++++++++++++++++++++++++++++----- lib/widgets/tl_thingy.dart | 12 +++++-- 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index d67a38b..c3d22dc 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -38,6 +38,25 @@ const Map rankCutoffs = { "z": -1, "": 0.5 }; +const Map rankTargets = { + "x": 24008, + "u": 23038, + "ss": 21583, + "s+": 20128, + "s": 18673, + "s-": 16975, + "a+": 15035, + "a": 13095, + "a-": 11155, + "b+": 9215, + "b": 7275, + "b-": 5335, + "c+": 3880, + "c": 2425, + "c-": 1213, + "d+": 606, + "d": 0, +}; enum Stats { tr, glicko, @@ -1883,6 +1902,26 @@ class TetrioPlayersLeaderboard { 'z': getAverageOfRank("z") }; + Map get cutoffs => { + 'x': getAverageOfRank("x")[1]["toEnterTR"], + 'u': getAverageOfRank("u")[1]["toEnterTR"], + 'ss': getAverageOfRank("ss")[1]["toEnterTR"], + 's+': getAverageOfRank("s+")[1]["toEnterTR"], + 's': getAverageOfRank("s")[1]["toEnterTR"], + 's-': getAverageOfRank("s-")[1]["toEnterTR"], + 'a+': getAverageOfRank("a+")[1]["toEnterTR"], + 'a': getAverageOfRank("a")[1]["toEnterTR"], + 'a-': getAverageOfRank("a-")[1]["toEnterTR"], + 'b+': getAverageOfRank("b+")[1]["toEnterTR"], + 'b': getAverageOfRank("b")[1]["toEnterTR"], + 'b-': getAverageOfRank("b-")[1]["toEnterTR"], + 'c+': getAverageOfRank("c+")[1]["toEnterTR"], + 'c': getAverageOfRank("c")[1]["toEnterTR"], + 'c-': getAverageOfRank("c-")[1]["toEnterTR"], + 'd+': getAverageOfRank("d+")[1]["toEnterTR"], + 'd': getAverageOfRank("d")[1]["toEnterTR"] + }; + TetrioPlayersLeaderboard.fromJson(List json, String t, DateTime ts) { type = t; timestamp = ts; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index a99c8c7..4090a38 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -728,6 +728,14 @@ class TetrioService extends DB { return matches; } + /// Gets and returns an amount of stored Tetra League mathes between [ourPlayerID] and [enemyPlayerID]. + Future getNumberOfTLMatchesBetweenPlayers(String ourPlayerID, String enemyPlayerID) async { + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + final results = await db.rawQuery("SELECT COUNT(*) from tetrioAlphaLeagueMathces WHERE (player1id = $ourPlayerID AND player2id = $enemyPlayerID) OR (player1id = $enemyPlayerID AND player2id = $ourPlayerID)"); + return results.first.values.first as int; + } + /// Deletes match and stats of that match with given [matchID] from local DB. Throws an exception if fails. Future deleteTLMatch(String matchID) async { await ensureDbIsOpen(); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 1819f64..ea38a06 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -88,6 +88,7 @@ class _MainState extends State with TickerProviderStateMixin { final bodyGlobalKey = GlobalKey(); bool _showSearchBar = false; late TabController _tabController; + late TabController _wideScreenTabController; late bool fixedScroll; @override @@ -95,6 +96,7 @@ class _MainState extends State with TickerProviderStateMixin { initDB(); _scrollController = ScrollController(); _tabController = TabController(length: 6, vsync: this); + _wideScreenTabController = TabController(length: 4, vsync: this); // We need to show something if (widget.player != null){ // if we have user input, @@ -279,12 +281,12 @@ class _MainState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { final t = Translations.of(context); - bool bigScreen = MediaQuery.of(context).size.width > 768; + bool bigScreen = MediaQuery.of(context).size.width > 1024; return Scaffold( drawer: widget.player == null ? NavDrawer(changePlayer) : null, // Side menu hidden if player provided drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.2, // 20% of left side of the screen used of Drawer gesture appBar: AppBar( - title: _showSearchBar ? SearchBox(onSubmit: changePlayer, bigScreen: bigScreen) : Text(widget.title, style: const TextStyle(shadows: textShadow)), + title: _showSearchBar ? SearchBox(onSubmit: changePlayer, bigScreen: MediaQuery.of(context).size.width > 768) : Text(widget.title, style: const TextStyle(shadows: textShadow)), backgroundColor: Colors.black, actions: widget.player == null ? [ // search bar and PopupMenuButton hidden if player provided TODO: Subject to change _showSearchBar @@ -374,8 +376,9 @@ class _MainState extends State with TickerProviderStateMixin { return notification.depth == 0; }, child: NestedScrollView( - controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), + controller: _scrollController, + scrollBehavior: const MaterialScrollBehavior(), headerSliverBuilder: (context, value) { return [ SliverToBoxAdapter( @@ -386,10 +389,15 @@ class _MainState extends State with TickerProviderStateMixin { )), SliverToBoxAdapter( child: TabBar( - controller: _tabController, + controller: bigScreen ? _wideScreenTabController : _tabController, padding: const EdgeInsets.all(0.0), isScrollable: true, - tabs: [ + tabs: bigScreen ? [ + Tab(text: t.tetraLeague,), + Tab(text: t.history), + Tab(text: "${t.sprint} & ${t.blitz}"), + Tab(text: t.other), + ] : [ Tab(text: t.tetraLeague), Tab(text: t.tlRecords), Tab(text: t.history), @@ -402,8 +410,44 @@ class _MainState extends State with TickerProviderStateMixin { ]; }, body: TabBarView( - controller: _tabController, - children: [ + controller: bigScreen ? _wideScreenTabController : _tabController, + children: bigScreen ? [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: MediaQuery.of(context).size.width-450, + constraints: BoxConstraints(maxWidth: 1024), + child: TLThingy( + tl: snapshot.data![0].tlSeason1, + userID: snapshot.data![0].userId, + states: snapshot.data![2], + topTR: snapshot.data![7], + bot: snapshot.data![0].role == "bot", + guest: snapshot.data![0].role == "anon", + lbPositions: meAmongEveryone + ), + ), + SizedBox( + width: 450, + child: _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]) + ), + ],), + _History(states: snapshot.data![2], update: _justUpdate), + Row(children: [ + Container( + width: MediaQuery.of(context).size.width/2, + padding: EdgeInsets.only(right: 8), + child: _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank) + ), + Container( + width: MediaQuery.of(context).size.width/2, + padding: EdgeInsets.only(left: 8), + child: _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank) + ), + ],), + _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) + ] : [ TLThingy( tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, @@ -600,9 +644,10 @@ class _TLRecords extends StatelessWidget { @override Widget build(BuildContext context) { if (data.isEmpty) return Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); - bool bigScreen = MediaQuery.of(context).size.width > 768; + bool bigScreen = MediaQuery.of(context).size.width >= 768; return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), + controller: ScrollController(), itemCount: data.length, itemBuilder: (BuildContext context, int index) { var accentColor = data[index].endContext.firstWhere((element) => element.userId == userID).success ? Colors.green : Colors.red; diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 437253f..0fb9104 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -27,7 +27,11 @@ class TLThingy extends StatefulWidget { final double? topTR; final PlayerLeaderboardPosition? lbPositions; final TetraLeagueAlpha? averages; - const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages}); + final double? thatRankCutoff; + final double? thatRankTarget; + final double? nextRankCutoff; + final double? nextRankTarget; + const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff = 25000, this.thatRankCutoff = 0, this.nextRankTarget = 25000, this.thatRankTarget = 0}); @override State createState() => _TLThingyState(); @@ -55,7 +59,7 @@ class _TLThingyState extends State { NumberFormat fractionfEstTRAcc = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3)..maximumIntegerDigits = 0; if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,)); return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; + bool bigScreen = constraints.maxWidth >= 768; return ListView.builder( physics: const ClampingScrollPhysics(), itemCount: 1, @@ -313,7 +317,9 @@ class _TLThingyState extends State { color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent ),), if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), - if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}") + if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}"), + if (widget.lbPositions?.estTr != null) const TextSpan(text: " • "), + TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") ] ), ), From 37a69afd05fa53a2ccb72ca8fdc8e0c351a65c9f Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 15 Mar 2024 01:53:19 +0300 Subject: [PATCH 10/14] Main view variables bugfix --- lib/data_objects/tetrio.dart | 6 ++++ lib/utils/numers_formats.dart | 1 + lib/views/main_view.dart | 53 +++++++++++++++++++---------------- lib/widgets/tl_thingy.dart | 24 ++++++++-------- lib/widgets/user_thingy.dart | 2 +- 5 files changed, 49 insertions(+), 37 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index c3d22dc..fba0986 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -78,6 +78,7 @@ enum Stats { area, eTR, acceTR, + acceTRabs, opener, plonk, infDS, @@ -107,6 +108,7 @@ const Map chartsShortTitles = { Stats.area: "Area", Stats.eTR: "eTR", Stats.acceTR: "±eTR", + Stats.acceTRabs: "+eTR absolute", Stats.opener: "Opener", Stats.plonk: "Plonk", Stats.infDS: "Inf. DS", @@ -383,6 +385,8 @@ class TetrioPlayer { return tlSeason1.estTr?.esttr; case Stats.acceTR: return tlSeason1.esttracc; + case Stats.acceTRabs: + return tlSeason1.esttracc?.abs(); case Stats.opener: return tlSeason1.playstyle?.opener; case Stats.plonk: @@ -2051,6 +2055,8 @@ class TetrioPlayerFromLeaderboard { return estTr.esttr; case Stats.acceTR: return esttracc; + case Stats.acceTRabs: + return esttracc.abs(); case Stats.opener: return playstyle.opener; case Stats.plonk: diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index 52e2c2a..b437749 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -3,5 +3,6 @@ import 'package:tetra_stats/gen/strings.g.dart'; final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); +final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index ea38a06..8358dcf 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -16,6 +16,7 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; import 'package:tetra_stats/main.dart' show prefs; import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/ranks_averages_view.dart' show RankAveragesView; import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView; @@ -29,22 +30,12 @@ import 'package:window_manager/window_manager.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; -Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up -TetrioPlayersLeaderboard? everyone; -PlayerLeaderboardPosition? meAmongEveryone; -TetraLeagueAlpha? rankAverages; -String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for -String _titleNickname = "dan63047"; final TetrioService teto = TetrioService(); // thing, that manadge our local DB -/// Each dropdown menu item contains list of dots for the graph -var chartsData = >>[]; int _chartsIndex = 0; 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); -final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); -final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final DateFormat _dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); @@ -55,8 +46,6 @@ class MainView extends StatefulWidget { /// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu. const MainView({super.key, this.player}); - String get title => "Tetra Stats: $_titleNickname"; - @override State createState() => _MainState(); } @@ -85,12 +74,22 @@ String readableIntDifference(int a, int b){ } 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; + PlayerLeaderboardPosition? meAmongEveryone; + TetraLeagueAlpha? rankAverages; + String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for + String _titleNickname = "dan63047"; + /// Each dropdown menu item contains list of dots for the graph + var chartsData = >>[]; final bodyGlobalKey = GlobalKey(); bool _showSearchBar = false; late TabController _tabController; late TabController _wideScreenTabController; late bool fixedScroll; + String get title => "Tetra Stats: $_titleNickname"; + @override void initState() { initDB(); @@ -153,7 +152,7 @@ class _MainState extends State with TickerProviderStateMixin { // Change view title and window title if avaliable setState((){_titleNickname = me.username;}); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) await windowManager.setTitle(widget.title); + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) await windowManager.setTitle(title); // Requesting Tetra League (alpha), records, news and top TR of player late List requests; @@ -245,9 +244,9 @@ class _MainState extends State with TickerProviderStateMixin { if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1); } // Also i need previous Tetra League State for comparison if avaliable - compareWith = uniqueTL.length >= 2 ? uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2) : null; - - chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid + if (uniqueTL.length >= 2){ + compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); + chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.rating)], child: Text(t.statCellNum.tr)), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.glicko!)], child: const Text("Glicko")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.rd!)], child: const Text("Rating Deviation")), @@ -270,6 +269,10 @@ class _MainState extends State with TickerProviderStateMixin { DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.infds)], child: const Text("Inf. DS")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")), ]; + }else{ + compareWith = null; + chartsData = []; + } return [me, records, states, tlMatches, compareWith, isTracking, news, topTR]; } @@ -286,7 +289,7 @@ class _MainState extends State with TickerProviderStateMixin { drawer: widget.player == null ? NavDrawer(changePlayer) : null, // Side menu hidden if player provided drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.2, // 20% of left side of the screen used of Drawer gesture appBar: AppBar( - title: _showSearchBar ? SearchBox(onSubmit: changePlayer, bigScreen: MediaQuery.of(context).size.width > 768) : Text(widget.title, style: const TextStyle(shadows: textShadow)), + title: _showSearchBar ? SearchBox(onSubmit: changePlayer, bigScreen: MediaQuery.of(context).size.width > 768) : Text(title, style: const TextStyle(shadows: textShadow)), backgroundColor: Colors.black, actions: widget.player == null ? [ // search bar and PopupMenuButton hidden if player provided TODO: Subject to change _showSearchBar @@ -425,6 +428,7 @@ class _MainState extends State with TickerProviderStateMixin { topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon", + averages: rankAverages, lbPositions: meAmongEveryone ), ), @@ -433,7 +437,7 @@ class _MainState extends State with TickerProviderStateMixin { child: _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]) ), ],), - _History(states: snapshot.data![2], update: _justUpdate), + _History(chartsData: chartsData, update: _justUpdate), Row(children: [ Container( width: MediaQuery.of(context).size.width/2, @@ -455,10 +459,11 @@ class _MainState extends State with TickerProviderStateMixin { topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon", + averages: rankAverages, lbPositions: meAmongEveryone ), _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]), - _History(states: snapshot.data![2], update: _justUpdate), + _History(chartsData: chartsData, update: _justUpdate), _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank), _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) @@ -680,17 +685,17 @@ class _TLRecords extends StatelessWidget { } class _History extends StatelessWidget{ - final List states; + final List>> chartsData; final Function update; /// Widget, that can show history of some stat of the player on the graph. /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.states, required this.update}); + const _History({required this.chartsData, required this.update}); @override Widget build(BuildContext context) { bool bigScreen = MediaQuery.of(context).size.width > 768; - return states.isNotEmpty ? + return chartsData.isNotEmpty ? Column( children: [ DropdownButton( @@ -701,7 +706,7 @@ class _History extends StatelessWidget{ update(); } ), - if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? _f2 : NumberFormat.compact(),) + if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),) else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))) ], ) @@ -955,7 +960,7 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { hoveredPointId = -1; // not hovering over any point } else { hoveredPointId = touchResponse!.lineBarSpots!.first.spotIndex; - headerTooltip = "${_f4.format(touchResponse.lineBarSpots!.first.y)} ${widget.yAxisTitle}"; + headerTooltip = "${f4.format(touchResponse.lineBarSpots!.first.y)} ${widget.yAxisTitle}"; footerTooltip = _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(touchResponse.lineBarSpots!.first.x.floor())); } }); diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 0fb9104..ab786ce 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -169,13 +169,13 @@ class _TLThingyState extends State { crossAxisAlignment: WrapCrossAlignment.start, clipBehavior: Clip.hardEdge, children: [ - if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm, pos: widget.lbPositions?.apm, averageStat: rankAverages?.apm), - if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps, pos: widget.lbPositions?.pps, averageStat: rankAverages?.pps, smallDecimal: false), - if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs, pos: widget.lbPositions?.vs, averageStat: rankAverages?.vs), + if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm, pos: widget.lbPositions?.apm, averageStat: widget.averages?.apm), + if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps, pos: widget.lbPositions?.pps, averageStat: widget.averages?.pps, smallDecimal: false), + if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs, pos: widget.lbPositions?.vs, averageStat: widget.averages?.vs), if (currentTl.standingLocal > 0) StatCellNum(playerStat: currentTl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed, pos: widget.lbPositions?.gamesPlayed), StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon, pos: widget.lbPositions?.gamesWon), - StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null, pos: widget.lbPositions?.winrate, averageStat: rankAverages != null ? rankAverages!.winrate * 100 : null), + StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null, pos: widget.lbPositions?.winrate, averageStat: widget.averages != null ? widget.averages!.winrate * 100 : null), ], ), ), @@ -202,7 +202,7 @@ class _TLThingyState extends State { Text(t.statCellNum.appDescription), Text("${t.exactValue}: ${currentTl.nerdStats!.app}") ], oldPlayerStat: oldTl?.nerdStats?.app, pos: widget.lbPositions?.app, - averageStat: rankAverages?.nerdStats?.app), + averageStat: widget.averages?.nerdStats?.app), GaugetNum(playerStat: currentTl.nerdStats!.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), @@ -211,7 +211,7 @@ class _TLThingyState extends State { Text(t.statCellNum.vsapmDescription), Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") ], oldPlayerStat: oldTl?.nerdStats?.vsapm, pos: widget.lbPositions?.vsapm, - averageStat: rankAverages?.nerdStats?.vsapm) + averageStat: widget.averages?.nerdStats?.vsapm) ]), ), Wrap( @@ -223,7 +223,7 @@ class _TLThingyState extends State { children: [ StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, pos: widget.lbPositions?.dss, - averageStat: rankAverages?.nerdStats?.dss, smallDecimal: false, + averageStat: widget.averages?.nerdStats?.dss, smallDecimal: false, alertWidgets: [Text(t.statCellNum.dssDescription), Text("${t.formula}: (VS / 100) - (APM / 60)"), Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], @@ -232,7 +232,7 @@ class _TLThingyState extends State { oldPlayerStat: oldTl?.nerdStats?.dss,), StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, pos: widget.lbPositions?.dsp, - averageStat: rankAverages?.nerdStats?.dsp, smallDecimal: false, + averageStat: widget.averages?.nerdStats?.dsp, smallDecimal: false, alertWidgets: [Text(t.statCellNum.dspDescription), Text("${t.formula}: DS/S / PPS"), Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], @@ -241,7 +241,7 @@ class _TLThingyState extends State { oldPlayerStat: oldTl?.nerdStats?.dsp,), StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, pos: widget.lbPositions?.appdsp, - averageStat: rankAverages?.nerdStats?.appdsp, smallDecimal: false, + averageStat: widget.averages?.nerdStats?.appdsp, smallDecimal: false, alertWidgets: [Text(t.statCellNum.appdspDescription), Text("${t.formula}: APP + DS/P"), Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], @@ -259,7 +259,7 @@ class _TLThingyState extends State { oldPlayerStat: oldTl?.nerdStats?.cheese,), StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, pos: widget.lbPositions?.gbe, - averageStat: rankAverages?.nerdStats?.gbe, smallDecimal: false, + averageStat: widget.averages?.nerdStats?.gbe, smallDecimal: false, alertWidgets: [Text(t.statCellNum.gbeDescription), Text("${t.formula}: APP * DS/P * 2"), Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], @@ -268,7 +268,7 @@ class _TLThingyState extends State { oldPlayerStat: oldTl?.nerdStats?.gbe,), StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, pos: widget.lbPositions?.nyaapp, - averageStat: rankAverages?.nerdStats?.nyaapp, smallDecimal: false, + averageStat: widget.averages?.nerdStats?.nyaapp, smallDecimal: false, alertWidgets: [Text(t.statCellNum.nyaappDescription), Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], @@ -277,7 +277,7 @@ class _TLThingyState extends State { oldPlayerStat: oldTl?.nerdStats?.nyaapp,), StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, pos: widget.lbPositions?.area, - averageStat: rankAverages?.nerdStats?.area, + averageStat: widget.averages?.nerdStats?.area, alertWidgets: [Text(t.statCellNum.areaDescription), Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 03708a4..2c59eab 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -271,7 +271,7 @@ class UserThingy extends StatelessWidget { playerStatLabel: t.statCellNum.hoursPlayed, isScreenBig: bigScreen, alertTitle: t.exactGametime, - alertWidgets: [Text(player.gameTime.toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended"),)], + alertWidgets: [Text(player.gameTime.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24),)], higherIsBetter: true,), if (player.gamesPlayed >= 0) StatCellNum( From fa8d0052d4fbb5c8210eff5a73174fb37005fbcb Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 18 Mar 2024 02:15:44 +0300 Subject: [PATCH 11/14] Work in progress --- lib/views/main_view.dart | 325 ++++++++++++++++++++++++++------- lib/widgets/stat_sell_num.dart | 10 +- lib/widgets/tl_thingy.dart | 153 ++++++++-------- lib/widgets/user_thingy.dart | 30 ++- 4 files changed, 365 insertions(+), 153 deletions(-) diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 8358dcf..23bb17b 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -434,22 +434,23 @@ class _MainState extends State with TickerProviderStateMixin { ), SizedBox( width: 450, - child: _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]) - ), - ],), - _History(chartsData: chartsData, update: _justUpdate), - Row(children: [ - Container( - width: MediaQuery.of(context).size.width/2, - padding: EdgeInsets.only(right: 8), - child: _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank) - ), - Container( - width: MediaQuery.of(context).size.width/2, - padding: EdgeInsets.only(left: 8), - child: _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank) + child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0) ), ],), + _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,), + // Row(children: [ + // Container( + // width: MediaQuery.of(context).size.width/2, + // padding: EdgeInsets.only(right: 8), + // child: _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank) + // ), + // Container( + // width: MediaQuery.of(context).size.width/2, + // padding: EdgeInsets.only(left: 8), + // child: _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank) + // ), + // ],), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) ] : [ TLThingy( @@ -462,8 +463,8 @@ class _MainState extends State with TickerProviderStateMixin { averages: rankAverages, lbPositions: meAmongEveryone ), - _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]), - _History(chartsData: chartsData, update: _justUpdate), + _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _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), _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) @@ -640,15 +641,24 @@ class _NavDrawerState extends State { class _TLRecords extends StatelessWidget { final String userID; + final Function changePlayer; final List data; + final bool wasActiveInTL; /// Widget, that displays Tetra League records. /// Accepts list of TL records ([data]) and [userID] of player from the view - const _TLRecords({required this.userID, required this.data}); + const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL}); @override Widget build(BuildContext context) { - if (data.isEmpty) return Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); + if (data.isEmpty) return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text("Perhaps, you want to"), + if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) + ], + )); bool bigScreen = MediaQuery.of(context).size.width >= 768; return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), @@ -686,11 +696,14 @@ class _TLRecords extends StatelessWidget { class _History extends StatelessWidget{ final List>> chartsData; + final String userID; final Function update; + final Function changePlayer; + final bool wasActiveInTL; /// Widget, that can show history of some stat of the player on the graph. /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.chartsData, required this.update}); + const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); @override Widget build(BuildContext context) { @@ -707,10 +720,24 @@ class _History extends StatelessWidget{ } ), if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),) - else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))) + else Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text("Perhaps, you want"), + if (wasActiveInTL)TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) + ], + )) ], ) - : Center(child: Text(t.noHistorySaved, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); + : Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text("Perhaps, you want"), + if (wasActiveInTL)TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) + ], + )); } } @@ -990,6 +1017,152 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { } } +class _TwoRecordsThingy extends StatelessWidget { + final RecordSingle? sprint; + final RecordSingle? blitz; + final String? rank; + + const _TwoRecordsThingy({required this.sprint, required this.blitz, 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; + } + + @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" && blitz != null) ? blitz!.endContext!.score > blitzAverages[rank]! : null; + late MapEntry closestAverageSprint; + late bool sprintBetterThanClosestAverage; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.endContext!.finalTime < sprintAverages[rank]! : null; + if (sprint != null) { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.endContext!.finalTime).abs() < (b -sprint!.endContext!.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = sprint!.endContext!.finalTime < closestAverageSprint.value; + } + if (blitz != null){ + closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.endContext!.score).abs() < (b -blitz!.endContext!.score).abs() ? a : b)); + blitzBetterThanClosestAverage = blitz!.endContext!.score > closestAverageBlitz.value; + } + return SingleChildScrollView(child: Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding(padding: EdgeInsets.only(right: 8.0), + child: sprint != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.sprint, style: TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: sprint != null ? get40lTime(sprint!.endContext!.finalTime.inMicroseconds) : "---", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey), + //children: [TextSpan(text: get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ), + if (sprint != null) RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${readableTimeDifference(sprint!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (sprint!.rank != null) TextSpan(text: "№${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank!))), + if (sprint!.rank != null) const TextSpan(text: " • "), + TextSpan(text: _dateFormat.format(sprint!.timestamp!)), + ] + ), + ), + ],), + ], + ), + if (sprint != null) Wrap( + //mainAxisSize: MainAxisSize.max, + alignment: WrapAlignment.spaceBetween, + spacing: 20, + children: [ + StatCellNum(playerStat: sprint!.endContext!.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true), + StatCellNum(playerStat: sprint!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true), + StatCellNum(playerStat: sprint!.endContext!.kps, playerStatLabel: t.statCellNum.kps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true,) + ], + ), + ] + ), + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500), + children: [ + TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.endContext!.score) : "---"), + //WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48)) + ] + ), + ), + if (blitz != null) RichText( + textAlign: TextAlign.end, + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${readableIntDifference(blitz!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )), + TextSpan(text: _dateFormat.format(blitz!.timestamp!)), + if (blitz!.rank != null) const TextSpan(text: " • "), + if (blitz!.rank != null) TextSpan(text: "№${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank!))), + ] + ), + ), + ],), + Padding(padding: EdgeInsets.only(left: 8.0), + child: blitz != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96)), + ], + ), + if (blitz != null) Wrap( + //mainAxisSize: MainAxisSize.max, + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 20, + children: [ + StatCellNum(playerStat: blitz!.endContext!.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true), + StatCellNum(playerStat: blitz!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true), + StatCellNum(playerStat: blitz!.endContext!.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true,) + ], + ), + ], + ), + ]), + )); + } +} + class _RecordThingy extends StatelessWidget { final RecordSingle? record; final String? rank; @@ -1423,60 +1596,80 @@ class _OtherThingy extends StatelessWidget { } } + Widget getShit(BuildContext context, bool bigScreen, bool showNewsTitle){ + return Column( + children: [ + if (distinguishment != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), + child: Column( + children: [ + Text(t.distinguishment, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28), textAlign: TextAlign.center), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: getDistinguishmentTitle(distinguishment?.header), + ), + ), + Text(getDistinguishmentSubtitle(distinguishment?.footer), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), + ], + ), + ), + if (bio != null) + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 48), + child: Column( + children: [ + Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended",fontSize: bigScreen ? 42 : 28)), + MarkdownBody(data: bio!, styleSheet: MarkdownStyleSheet(textScaleFactor: 1.5, textAlign: WrapAlignment.center)) // Text(bio!, style: const TextStyle(fontSize: 18)), + ], + ), + ), + if (zen != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), + child: Column( + children: [ + Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Text("${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), + Text("${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: const TextStyle(fontSize: 18)), + ], + ), + ), + if (newsletter != null && newsletter!.isNotEmpty && showNewsTitle) + Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ], + ); + } + @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; - return ListView.builder( + if (constraints.maxWidth >= 1024){ + return Row( + children: [ + SizedBox(width: 450, child: getShit(context, true, false)), + SizedBox(width: constraints.maxWidth - 450, child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: newsletter!.length+1, + itemBuilder: (BuildContext context, int index) { + return index == 0 ? Center(child: Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter![index-1]); + } + )) + ] + ); + } + else { + return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), itemCount: newsletter!.length+1, itemBuilder: (BuildContext context, int index) { - return index == 0 ? Column( - children: [ - if (distinguishment != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), - child: Column( - children: [ - Text(t.distinguishment, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28), textAlign: TextAlign.center), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: getDistinguishmentTitle(distinguishment?.header), - ), - ), - Text(getDistinguishmentSubtitle(distinguishment?.footer), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), - ], - ), - ), - if (bio != null) - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 48), - child: Column( - children: [ - Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended",fontSize: bigScreen ? 42 : 28)), - MarkdownBody(data: bio!, styleSheet: MarkdownStyleSheet(textScaleFactor: 1.5, textAlign: WrapAlignment.center)) // Text(bio!, style: const TextStyle(fontSize: 18)), - ], - ), - ), - if (zen != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), - child: Column( - children: [ - Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Text("${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), - Text("${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: const TextStyle(fontSize: 18)), - ], - ), - ), - if (newsletter != null && newsletter!.isNotEmpty) - Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ], - ) : getNewsTile(newsletter![index-1]); + return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter![index-1]); }, ); + } }); } } diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 839457a..06b215d 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -33,11 +33,11 @@ class StatCellNum extends StatelessWidget { Color getStatColor(){ if (averageStat == null) return Colors.white; num percentile = (higherIsBetter ? playerStat / averageStat! : averageStat! / playerStat).abs(); - if (percentile > 1.50) return Colors.purpleAccent; - else if (percentile > 1.20) return Colors.blueAccent; - else if (percentile > 0.90) return Colors.greenAccent; - else if (percentile > 0.70) return Colors.yellowAccent; - else return Colors.redAccent; + if (percentile > 1.50) return Colors.purpleAccent; + if (percentile > 1.20) return Colors.blueAccent; + if (percentile > 0.90) return Colors.greenAccent; + if (percentile > 0.70) return Colors.yellowAccent; + return Colors.redAccent; } @override diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index ab786ce..afd8bec 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -161,7 +161,7 @@ class _TLThingyState extends State { overflow: TextOverflow.visible, )), Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), + padding: const EdgeInsets.fromLTRB(8, 16, 8, 48), child: Wrap( direction: Axis.horizontal, alignment: WrapAlignment.center, @@ -214,77 +214,80 @@ class _TLThingyState extends State { averageStat: widget.averages?.nerdStats?.vsapm) ]), ), - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - pos: widget.lbPositions?.dss, - averageStat: widget.averages?.nerdStats?.dss, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dss,), - StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - pos: widget.lbPositions?.dsp, - averageStat: widget.averages?.nerdStats?.dsp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dsp,), - StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - pos: widget.lbPositions?.appdsp, - averageStat: widget.averages?.nerdStats?.appdsp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.appdsp,), - StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - pos: widget.lbPositions?.cheese, - //averageStat: rankAverages?.nerdStats?.cheese, TODO: questonable - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: false, - oldPlayerStat: oldTl?.nerdStats?.cheese,), - StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - pos: widget.lbPositions?.gbe, - averageStat: widget.averages?.nerdStats?.gbe, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.gbe,), - StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - pos: widget.lbPositions?.nyaapp, - averageStat: widget.averages?.nerdStats?.nyaapp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.nyaapp,), - StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - pos: widget.lbPositions?.area, - averageStat: widget.averages?.nerdStats?.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.area,) - ]) + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, + pos: widget.lbPositions?.dss, + averageStat: widget.averages?.nerdStats?.dss, smallDecimal: false, + alertWidgets: [Text(t.statCellNum.dssDescription), + Text("${t.formula}: (VS / 100) - (APM / 60)"), + Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.dss,), + StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, + pos: widget.lbPositions?.dsp, + averageStat: widget.averages?.nerdStats?.dsp, smallDecimal: false, + alertWidgets: [Text(t.statCellNum.dspDescription), + Text("${t.formula}: DS/S / PPS"), + Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.dsp,), + StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, + pos: widget.lbPositions?.appdsp, + averageStat: widget.averages?.nerdStats?.appdsp, smallDecimal: false, + alertWidgets: [Text(t.statCellNum.appdspDescription), + Text("${t.formula}: APP + DS/P"), + Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.appdsp,), + StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, + pos: widget.lbPositions?.cheese, + //averageStat: rankAverages?.nerdStats?.cheese, TODO: questonable + alertWidgets: [Text(t.statCellNum.cheeseDescription), + Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), + Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], + okText: t.popupActions.ok, + higherIsBetter: false, + oldPlayerStat: oldTl?.nerdStats?.cheese,), + StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, + pos: widget.lbPositions?.gbe, + averageStat: widget.averages?.nerdStats?.gbe, smallDecimal: false, + alertWidgets: [Text(t.statCellNum.gbeDescription), + Text("${t.formula}: APP * DS/P * 2"), + Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.gbe,), + StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, + pos: widget.lbPositions?.nyaapp, + averageStat: widget.averages?.nerdStats?.nyaapp, smallDecimal: false, + alertWidgets: [Text(t.statCellNum.nyaappDescription), + Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), + Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.nyaapp,), + StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, + pos: widget.lbPositions?.area, + averageStat: widget.averages?.nerdStats?.area, + alertWidgets: [Text(t.statCellNum.areaDescription), + Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), + Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.area,) + ]), + ) ], ), if (currentTl.estTr != null) @@ -330,14 +333,14 @@ class _TLThingyState extends State { Text(t.statCellNum.accOfEst, style: const TextStyle(height: 0.1),), RichText( text: TextSpan( - text: (currentTl.esttracc != null) ? intFDiff.format(currentTl.esttracc!.truncate()) : "-", + text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? intFDiff.format(currentTl.esttracc!.truncate()) : "---", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500), children: [ - TextSpan(text: (currentTl.esttracc != null) ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) + TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) ] ), ), - if (oldTl?.esttracc != null || widget.lbPositions != null) RichText(text: TextSpan( + if ((oldTl?.esttracc != null || widget.lbPositions != null) && currentTl.bestRank != "z") RichText(text: TextSpan( text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), children: [ diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 2c59eab..d971f7e 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -328,13 +328,29 @@ class UserThingy extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: Text( - "${player.country != null ? "${t.countries[player.country]} • " : ""}${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " ${t.botCreatedBy} ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? t.notSupporter : t.supporter(tier: player.supporterTier)}", - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - )), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan(text: "", style: const TextStyle( + fontFamily: "Eurostile Round", + fontSize: 16, + color: Colors.white, + ), + children: [ + if (player.country != null) TextSpan(text: "${t.countries[player.country]} • "), + TextSpan(text: "${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}"), + if (player.supporterTier > 0) TextSpan(text: " • "), + if (player.supporterTier > 0) WidgetSpan(child: Icon(player.supporterTier > 1 ? Icons.star : Icons.star_border, color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (player.supporterTier > 0) TextSpan(text: player.supporterTier.toString(), style: TextStyle(color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)) + ] + ) + ), + // Text( + // "${player.country != null ? "${t.countries[player.country]} • " : ""}${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " ${t.botCreatedBy} ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? t.notSupporter : t.supporter(tier: player.supporterTier)}", + // textAlign: TextAlign.center, + // style: const TextStyle( + // fontFamily: "Eurostile Round", + // fontSize: 16, + // )), ) ], ), From c1f0e85b4a2e30a69e8dcf915fd7f5ef739d60dd Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 19 Mar 2024 01:39:41 +0300 Subject: [PATCH 12/14] 40 Lines & Blitz tab design --- lib/gen/strings.g.dart | 108 ++++++++- lib/utils/numers_formats.dart | 5 +- lib/views/main_view.dart | 337 +++++++++++------------------ lib/widgets/finesse_thingy.dart | 47 ++++ lib/widgets/lineclears_thingy.dart | 52 +++++ lib/widgets/tl_thingy.dart | 4 +- res/i18n/strings.i18n.json | 26 ++- res/i18n/strings_ru.i18n.json | 26 ++- 8 files changed, 374 insertions(+), 231 deletions(-) create mode 100644 lib/widgets/finesse_thingy.dart create mode 100644 lib/widgets/lineclears_thingy.dart diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index b28bfe7..3a890e7 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1018 (509 per locale) +/// Strings: 1050 (525 per locale) /// -/// Built on 2024-02-08 at 20:30 UTC +/// Built on 2024-03-18 at 17:41 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -675,8 +675,30 @@ class _StringsNumOfGameActionsEn { // Translations String get pc => 'All Clears'; String get hold => 'Holds'; - String get tspinsTotal => 'T-spins total'; - String get lineClears => 'Line clears'; + String inputs({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} key presses', + one: '${n} key press', + two: '${n} key presses', + few: '${n} key presses', + many: '${n} key presses', + other: '${n} key presses', + ); + String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} T-spins total', + one: '${n} T-spin total', + two: '${n} T-spins total', + few: '${n} T-spins total', + many: '${n} T-spins total', + other: '${n} T-spins total', + ); + String lineClears({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} lines cleared', + one: '${n} line cleared', + two: '${n} lines cleared', + few: '${n} lines cleared', + many: '${n} lines cleared', + other: '${n} lines cleared', + ); } // Path: popupActions @@ -1268,8 +1290,30 @@ class _StringsNumOfGameActionsRu implements _StringsNumOfGameActionsEn { // Translations @override String get pc => 'Все чисто'; @override String get hold => 'В запас'; - @override String get tspinsTotal => 'T-spins всего'; - @override String get lineClears => 'Линий очищено'; + @override String inputs({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} нажатий клавиш', + one: '${n} нажатие на клавишу', + two: '${n} нажатия на клавишы', + few: '${n} нажатия на клавишы', + many: '${n} нажатий на клавиш', + other: '${n} нажатий на клавиш', + ); + @override String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} T-спинов всего', + one: 'всего ${n} T-спин', + two: '${n} T-спина всего', + few: '${n} T-спина всего', + many: '${n} T-спинов всего', + other: '${n} T-спинов всего', + ); + @override String lineClears({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} линий очищено', + one: '${n} линия очищена', + two: '${n} линии очищено', + few: '${n} линии очищено', + many: '${n} линий очищено', + other: '${n} линий очищено', + ); } // Path: popupActions @@ -1546,8 +1590,30 @@ extension on Translations { case 'playerRole.anon': return 'Anonymous'; case 'numOfGameActions.pc': return 'All Clears'; case 'numOfGameActions.hold': return 'Holds'; - case 'numOfGameActions.tspinsTotal': return 'T-spins total'; - case 'numOfGameActions.lineClears': return 'Line clears'; + case 'numOfGameActions.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} key presses', + one: '${n} key press', + two: '${n} key presses', + few: '${n} key presses', + many: '${n} key presses', + other: '${n} key presses', + ); + case 'numOfGameActions.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} T-spins total', + one: '${n} T-spin total', + two: '${n} T-spins total', + few: '${n} T-spins total', + many: '${n} T-spins total', + other: '${n} T-spins total', + ); + case 'numOfGameActions.lineClears': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} lines cleared', + one: '${n} line cleared', + two: '${n} lines cleared', + few: '${n} lines cleared', + many: '${n} lines cleared', + other: '${n} lines cleared', + ); case 'popupActions.cancel': return 'Cancel'; case 'popupActions.submit': return 'Submit'; case 'popupActions.ok': return 'OK'; @@ -2065,8 +2131,30 @@ extension on _StringsRu { case 'playerRole.anon': return 'Аноним'; case 'numOfGameActions.pc': return 'Все чисто'; case 'numOfGameActions.hold': return 'В запас'; - case 'numOfGameActions.tspinsTotal': return 'T-spins всего'; - case 'numOfGameActions.lineClears': return 'Линий очищено'; + case 'numOfGameActions.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} нажатий клавиш', + one: '${n} нажатие на клавишу', + two: '${n} нажатия на клавишы', + few: '${n} нажатия на клавишы', + many: '${n} нажатий на клавиш', + other: '${n} нажатий на клавиш', + ); + case 'numOfGameActions.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} T-спинов всего', + one: 'всего ${n} T-спин', + two: '${n} T-спина всего', + few: '${n} T-спина всего', + many: '${n} T-спинов всего', + other: '${n} T-спинов всего', + ); + case 'numOfGameActions.lineClears': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} линий очищено', + one: '${n} линия очищена', + two: '${n} линии очищено', + few: '${n} линии очищено', + many: '${n} линий очищено', + other: '${n} линий очищено', + ); case 'popupActions.cancel': return 'Отменить'; case 'popupActions.submit': return 'Подтвердить'; case 'popupActions.ok': return 'OK'; diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index b437749..af7bf36 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -4,5 +4,6 @@ import 'package:tetra_stats/gen/strings.g.dart'; final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); -final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); -final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); \ No newline at end of file +final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); +final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); +final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..minimumFractionDigits = 0; \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 23bb17b..ec0c77f 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -21,6 +21,8 @@ import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/ranks_averages_view.dart' show RankAveragesView; import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView; 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/search_box.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; @@ -437,7 +439,7 @@ class _MainState extends State with TickerProviderStateMixin { child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0) ), ],), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _History(chartsData: chartsData, states: snapshot.data![2], changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,), // Row(children: [ // Container( @@ -464,7 +466,7 @@ class _MainState extends State with TickerProviderStateMixin { lbPositions: meAmongEveryone ), _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _History(chartsData: chartsData, states: snapshot.data![2], 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), _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) @@ -696,6 +698,7 @@ class _TLRecords extends StatelessWidget { class _History extends StatelessWidget{ final List>> chartsData; + final List states; final String userID; final Function update; final Function changePlayer; @@ -703,7 +706,7 @@ class _History extends StatelessWidget{ /// Widget, that can show history of some stat of the player on the graph. /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); + const _History({required this.chartsData, required this.states, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); @override Widget build(BuildContext context) { @@ -1017,6 +1020,23 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { } } +class _HistoryTableThingy extends StatelessWidget{ + final List states; + + const _HistoryTableThingy(this.states); + // :tf: + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints){ + return SingleChildScrollView(child: Table( + children: [ + TableRow(children: [Text("Date & Time"), Text("Tr")]), + ], + )); + }); + } +} + class _TwoRecordsThingy extends StatelessWidget { final RecordSingle? sprint; final RecordSingle? blitz; @@ -1081,6 +1101,9 @@ class _TwoRecordsThingy extends StatelessWidget { children: [ if (rank != null && rank != "z") TextSpan(text: "${readableTimeDifference(sprint!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else TextSpan(text: "${readableTimeDifference(sprint!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key.toUpperCase()} rank average\n", style: TextStyle( + color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent )), if (sprint!.rank != null) TextSpan(text: "№${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank!))), if (sprint!.rank != null) const TextSpan(text: " • "), @@ -1096,17 +1119,20 @@ class _TwoRecordsThingy extends StatelessWidget { alignment: WrapAlignment.spaceBetween, spacing: 20, children: [ - StatCellNum(playerStat: sprint!.endContext!.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true), - StatCellNum(playerStat: sprint!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true), - StatCellNum(playerStat: sprint!.endContext!.kps, playerStatLabel: t.statCellNum.kps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true,) + StatCellNum(playerStat: sprint!.endContext!.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: sprint!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: sprint!.endContext!.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), ], ), + if (sprint != null) FinesseThingy(sprint?.endContext?.finesse, sprint?.endContext?.finessePercentage), + if (sprint != null) LineclearsThingy(sprint!.endContext!.clears, sprint!.endContext!.lines, sprint!.endContext!.holds, sprint!.endContext!.tSpins), + if (sprint != null) Text("${sprint!.endContext!.inputs} KP • ${f2.format(sprint!.endContext!.kps)} KpS") ] ), Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisSize: MainAxisSize.min, @@ -1133,6 +1159,9 @@ class _TwoRecordsThingy extends StatelessWidget { children: [ if (rank != null && rank != "z") TextSpan(text: "${readableIntDifference(blitz!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else TextSpan(text: "${readableIntDifference(blitz!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average\n", style: TextStyle( + color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent )), TextSpan(text: _dateFormat.format(blitz!.timestamp!)), if (blitz!.rank != null) const TextSpan(text: " • "), @@ -1151,11 +1180,14 @@ class _TwoRecordsThingy extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.start, spacing: 20, children: [ - StatCellNum(playerStat: blitz!.endContext!.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true), - StatCellNum(playerStat: blitz!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true), - StatCellNum(playerStat: blitz!.endContext!.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true,) + StatCellNum(playerStat: blitz!.endContext!.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: blitz!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: blitz!.endContext!.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true) ], ), + if (blitz != null) FinesseThingy(blitz?.endContext?.finesse, blitz?.endContext?.finessePercentage), + if (blitz != null) LineclearsThingy(blitz!.endContext!.clears, blitz!.endContext!.lines, blitz!.endContext!.holds, blitz!.endContext!.tSpins), + if (blitz != null) Text("${blitz!.endContext!.piecesPlaced} P • ${blitz!.endContext!.inputs} KP • ${f2.format(blitz!.endContext!.kpp)} KpP • ${f2.format(blitz!.endContext!.kps)} KpS") ], ), ]), @@ -1170,6 +1202,15 @@ class _RecordThingy extends StatelessWidget { /// Widget that displays data from [record] const _RecordThingy({required this.record, 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; + } + @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))); @@ -1186,219 +1227,89 @@ class _RecordThingy extends StatelessWidget { 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 ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: 1, - itemBuilder: (BuildContext context, int index) { - return Column( + + 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, children: [ - // show mode title - if (record!.stream.contains("40l")) Text(t.sprint, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - else if (record!.stream.contains("blitz")) Text(t.blitz, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - - // show main metric Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - textBaseline: TextBaseline.alphabetic, + mainAxisSize: MainAxisSize.min, children: [ - // Show grade based on closest rank average - if (record!.stream.contains("40l")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 48) - else if (record!.stream.contains("blitz")) Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 48), - - // Show result - if (record!.stream.contains("40l")) Text(get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - else if (record!.stream.contains("blitz")) Text(NumberFormat.decimalPattern().format(record!.endContext!.score), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (record!.stream.contains("40l")) Padding(padding: EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) + ), + if (record!.stream.contains("blitz")) Padding(padding: 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!.stream.contains("40l")) Text(t.sprint, style: TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + if (record!.stream.contains("blitz")) Text(t.blitz, style: TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: record!.stream.contains("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!.stream.contains("40l") && (rank != null && rank != "z")) TextSpan(text: "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if (record!.stream.contains("40l") && (rank == null || rank == "z")) TextSpan(text: "${readableTimeDifference(record!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key.toUpperCase()} rank average\n", style: TextStyle( + color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if (record!.stream.contains("blitz") && (rank != null && rank != "z")) TextSpan(text: "${readableIntDifference(record!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if (record!.stream.contains("blitz") && (rank == null || rank == "z")) TextSpan(text: "${readableIntDifference(record!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average\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: _dateFormat.format(record!.timestamp!)), + ] + ), + ) + ],), ], ), - - // Show difference between rank average - if (record!.stream.contains("40l") && (rank != null && rank != "z")) Text( - "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: sprintBetterThanRankAverage??false ? - Colors.greenAccent : - Colors.redAccent - ) - ) - else if (record!.stream.contains("40l") && (rank == null || rank == "z")) Text( - "${readableTimeDifference(record!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: sprintBetterThanClosestAverage ? - Colors.greenAccent : - Colors.redAccent - ) - ) - else if (record!.stream.contains("blitz") && (rank != null && rank != "z")) Text( - "${readableIntDifference(record!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: blitzBetterThanRankAverage??false ? - Colors.greenAccent : - Colors.redAccent - ) - ) - else if (record!.stream.contains("blitz") && (rank == null || rank == "z")) Text( - "${readableIntDifference(record!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average", - textAlign: TextAlign.center, - style: TextStyle( - color: blitzBetterThanClosestAverage ? - Colors.greenAccent : - Colors.redAccent - ) - ), - - // Show rank if presented - if (record!.rank != null) StatCellNum(playerStat: record!.rank!, playerStatLabel: "Leaderboard Placement", isScreenBig: bigScreen, higherIsBetter: false), - - // Show when this record was obtained - Text(t.obtainDate(date: _dateFormat.format(record!.timestamp!)), textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 16)), - - // Show metrics - Padding(padding: const EdgeInsets.fromLTRB(0, 48, 0, 48), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceAround, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - spacing: 25, + if (record!.stream.contains("40l")) Wrap( + alignment: WrapAlignment.spaceBetween, + spacing: 20, children: [ - if (record!.stream.contains("blitz")) StatCellNum(playerStat: record!.endContext!.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true), - if (record!.stream.contains("blitz")) StatCellNum(playerStat: record!.endContext!.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.endContext!.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true), - if (record!.endContext!.finesse != null) StatCellNum(playerStat: record!.endContext!.finesse!.faults, playerStatLabel: t.statCellNum.finesseFaults, isScreenBig: bigScreen, higherIsBetter: false), - if (record!.endContext!.finesse != null) StatCellNum(playerStat: record!.endContext!.finessePercentage * 100, playerStatLabel: t.statCellNum.finessePercentage, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.endContext!.inputs, playerStatLabel: t.statCellNum.keys, isScreenBig: bigScreen, higherIsBetter: false), - StatCellNum(playerStat: record!.endContext!.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: false), - StatCellNum(playerStat: record!.endContext!.kps, playerStatLabel: t.statCellNum.kps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true,), + 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), ], - ), ), - - // List of actions - Padding(padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), - child: Container(width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, - constraints: BoxConstraints(maxWidth: 452), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${t.numOfGameActions.pc}:", style: const TextStyle(fontSize: 24)), - Text(record!.endContext!.clears.allClears.toString(), style: const TextStyle(fontSize: 24)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${t.numOfGameActions.hold}:", style: const TextStyle(fontSize: 24)), - Text(record!.endContext!.holds.toString(), style: const TextStyle(fontSize: 24)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${t.numOfGameActions.tspinsTotal}:", style: const TextStyle(fontSize: 24)), - Text(record!.endContext!.tSpins.toString(), style: const TextStyle(fontSize: 24)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin zero:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.tSpinZeros.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin singles:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.tSpinSingles.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin doubles:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.tSpinDoubles.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin triples:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.tSpinTriples.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin mini zero:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.tSpinMiniZeros.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin mini singles:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.tSpinMiniSingles.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin mini doubles:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.tSpinMiniDoubles.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${t.numOfGameActions.lineClears}:", style: const TextStyle(fontSize: 24)), - Text(record!.endContext!.lines.toString(), style: const TextStyle(fontSize: 24)), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Singles:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.singles.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Doubles:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.doubles.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Triples:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.triples.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Quads:", style: TextStyle(fontSize: 18)), - Text(record!.endContext!.clears.quads.toString(), style: const TextStyle(fontSize: 18)), - ], - ), - ], - ), - ), + if (record!.stream.contains("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!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KpS"), + if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KpP • ${f2.format(record!.endContext!.kps)} KpS") ] - ); - }); - }); + ), + ), + ); + } + ); } } diff --git a/lib/widgets/finesse_thingy.dart b/lib/widgets/finesse_thingy.dart new file mode 100644 index 0000000..e913572 --- /dev/null +++ b/lib/widgets/finesse_thingy.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; + +class FinesseThingy extends StatelessWidget{ + final Finesse? finesse; + final double? finessePercentage; + + const FinesseThingy(this.finesse, this.finessePercentage, {super.key}); + + Color getFinesseColor(){ + if (finesse == null) return Colors.grey; + if (finesse!.faults == 0) return Colors.purpleAccent; + if (finessePercentage! > 0.4) return Colors.white; + else return Colors.redAccent; + } + + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + Text("f", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + Positioned(child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended")), left: 25, top: 20), + Positioned( + child: Text("${finesse != null ? finesse!.faults : "---"}F", style: TextStyle( + color: getFinesseColor() + )), right: 0, top: 20), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text("${finesse != null ? f2.format(finessePercentage! * 100) : "---.--"}%", style: TextStyle( + shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: getFinesseColor() + )), + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/lineclears_thingy.dart b/lib/widgets/lineclears_thingy.dart new file mode 100644 index 0000000..303b607 --- /dev/null +++ b/lib/widgets/lineclears_thingy.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; + +class LineclearsThingy extends StatelessWidget{ + final Clears clears; + final int lines; + final int holds; + final int tSpins; + + const LineclearsThingy(this.clears, this.lines, this.holds, this.tSpins, {super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 20, + children: [ + Container( + width: 150, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.numOfGameActions.lineClears(n: lines), style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended")), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Quads"), Text(clears.quads.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Triples"), Text(clears.triples.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Doubles"), Text(clears.doubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Singles"), Text(clears.singles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("\n${t.numOfGameActions.pc}"), Text("\n${clears.allClears.toString()}")]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.numOfGameActions.hold), Text(holds.toString())]), + ], + ), + ), + Container( + width: 150, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.numOfGameActions.tspinsTotal(n: tSpins), style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended")), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins triples"), Text(clears.tSpinTriples.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins doubles"), Text(clears.tSpinDoubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins singles"), Text(clears.tSpinSingles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins zeros"), Text(clears.tSpinZeros.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Mini T-spins doubles"), Text(clears.tSpinMiniDoubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Mini T-spins singles"), Text(clears.tSpinMiniSingles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]), + ], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index afd8bec..5b04061 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -308,7 +308,7 @@ class _TLThingyState extends State { RichText( text: TextSpan( text: intf.format(currentTl.estTr!.esttr.truncate()), - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), children: [TextSpan(text: fractionfEstTR.format(currentTl.estTr!.esttr - currentTl.estTr!.esttr.truncate()).substring(1), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] ), ), @@ -334,7 +334,7 @@ class _TLThingyState extends State { RichText( text: TextSpan( text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? intFDiff.format(currentTl.esttracc!.truncate()) : "---", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500), + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), children: [ TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) ] diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 4145f76..79ec395 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -238,8 +238,30 @@ "numOfGameActions":{ "pc": "All Clears", "hold": "Holds", - "tspinsTotal": "T-spins total", - "lineClears": "Line clears" + "inputs": { + "zero": "$n key presses", + "one": "$n key press", + "two": "$n key presses", + "few": "$n key presses", + "many": "$n key presses", + "other": "$n key presses" + }, + "tspinsTotal": { + "zero": "$n T-spins total", + "one": "$n T-spin total", + "two": "$n T-spins total", + "few": "$n T-spins total", + "many": "$n T-spins total", + "other": "$n T-spins total" + }, + "lineClears": { + "zero": "$n lines cleared", + "one": "$n line cleared", + "two": "$n lines cleared", + "few": "$n lines cleared", + "many": "$n lines cleared", + "other": "$n lines cleared" + } }, "popupActions":{ "cancel": "Cancel", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index f8dba3b..7afcbbd 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -238,8 +238,30 @@ "numOfGameActions":{ "pc": "Все чисто", "hold": "В запас", - "tspinsTotal": "T-spins всего", - "lineClears": "Линий очищено" + "inputs": { + "zero": "$n нажатий клавиш", + "one": "$n нажатие на клавишу", + "two": "$n нажатия на клавишы", + "few": "$n нажатия на клавишы", + "many": "$n нажатий на клавиш", + "other": "$n нажатий на клавиш" + }, + "tspinsTotal": { + "zero": "$n T-спинов всего", + "one": "всего $n T-спин", + "two": "$n T-спина всего", + "few": "$n T-спина всего", + "many": "$n T-спинов всего", + "other": "$n T-спинов всего" + }, + "lineClears": { + "zero": "$n линий очищено", + "one": "$n линия очищена", + "two": "$n линии очищено", + "few": "$n линии очищено", + "many": "$n линий очищено", + "other": "$n линий очищено" + } }, "popupActions":{ "cancel": "Отменить", From 8ab4a4db35e779b218e8d5041f317461bc7d932b Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 21 Mar 2024 01:56:13 +0300 Subject: [PATCH 13/14] Failed attempt into Table + preparing for 1.5.0 --- lib/data_objects/freyhoe_test.dart | 8 +- lib/gen/strings.g.dart | 152 +++++++++++++++--- lib/services/crud_exceptions.dart | 2 + lib/services/tetrio_crud.dart | 2 +- lib/utils/numers_formats.dart | 3 +- lib/views/calc_view.dart | 2 +- lib/views/compare_view.dart | 4 +- lib/views/customization_view.dart | 103 ++++++------- lib/views/main_view.dart | 239 ++++++++++++++++++----------- lib/views/settings_view.dart | 12 +- lib/widgets/gauget_num.dart | 2 +- lib/widgets/lineclears_thingy.dart | 26 ++-- lib/widgets/stat_sell_num.dart | 2 +- lib/widgets/tl_thingy.dart | 125 ++++++--------- lib/widgets/user_thingy.dart | 1 + res/i18n/strings.i18n.json | 36 ++++- res/i18n/strings_ru.i18n.json | 38 ++++- test/api_test.dart | 1 - 18 files changed, 474 insertions(+), 284 deletions(-) diff --git a/lib/data_objects/freyhoe_test.dart b/lib/data_objects/freyhoe_test.dart index 7a6eeb3..5ca2a05 100644 --- a/lib/data_objects/freyhoe_test.dart +++ b/lib/data_objects/freyhoe_test.dart @@ -1,9 +1,9 @@ -import 'dart:convert'; +//import 'dart:convert'; import 'dart:io'; -import 'package:path_provider/path_provider.dart'; +//import 'package:path_provider/path_provider.dart'; -import 'tetrio_multiplayer_replay.dart'; +//import 'tetrio_multiplayer_replay.dart'; /// That thing allows me to test my new staff i'm trying to implement void main() async { @@ -20,6 +20,6 @@ void main() async { // List> board = [for (var i = 0 ; i < 40; i++) [for (var i = 0 ; i < 10; i++) Tetromino.empty]]; // print(replay.rawJson); - print(""); + //print(""); exit(0); } \ No newline at end of file diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 3a890e7..ee03a73 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1050 (525 per locale) +/// Strings: 1098 (549 per locale) /// -/// Built on 2024-03-18 at 17:41 UTC +/// Built on 2024-03-20 at 22:41 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -181,6 +181,14 @@ class Translations implements BaseTranslations { String get stoppedBeingTracked => 'Removed from tracking list!'; String get tlLeaderboard => 'Tetra League leaderboard'; String get noRecords => 'No records'; + String noOldRecords({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'No records', + one: '${n} record', + two: '${n} records', + few: '${n} records', + many: '${n} records', + other: '${n} records', + ); String get noRecord => 'No record'; String get botRecord => 'Bots are not allowed to set records'; String get anonRecord => 'Guests are not allowed to set records'; @@ -205,6 +213,9 @@ class Translations implements BaseTranslations { String comparingWith({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; String get top => 'Top'; String get topRank => 'Top Rank'; + String verdictGeneral({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; + String get verdictBetter => 'better'; + String get verdictWorse => 'worse'; String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; String get nerdStats => 'Nerd Stats'; String get playersYouTrack => 'Players you track'; @@ -228,8 +239,14 @@ class Translations implements BaseTranslations { String get yourIDAlertTitle => 'Your nickname in TETR.IO'; String get yourIDText => 'When app loads, it will retrieve data for this account'; String get language => 'Language'; + String get customization => 'Customization'; + String get customizationDescription => 'There is only one toggle, planned to add more settings'; + String get lbStats => 'Show leaderboard based stats'; + String get lbStatsDescription => 'That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values'; String get aboutApp => 'About app'; String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; + String get oskKagari => 'Osk Kagari gimmick'; + String get oskKagariDescription => 'If on, osk\'s rank on main view will be rendered as :kagari:'; String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}'; String statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; String matchesViewTitle({required Object nickname}) => '${nickname} TL matches'; @@ -722,21 +739,30 @@ class _StringsErrorsEn { // Translations String connection({required Object code, required Object message}) => 'Some issue with connection: ${code} ${message}'; String get noSuchUser => 'No such user'; + String get noSuchUserSub => 'Either you mistyped something, or the account no longer exists'; + String get discordNotAssigned => 'No user assigned to given Discord ID'; + String get discordNotAssignedSub => 'Make sure you provided valid ID'; String get history => 'History for that player is missing'; + String get actionSuggestion => 'Perhaps, you want to'; String get p1nkl0bst3rTLmatches => 'No Tetra League matches was found'; String get clientException => 'No internet connection'; - String get forbidden => 'Your IP address is blocked.\nChange IP address or reach out to osk'; - String get tooManyRequests => 'You have been rate limited. Try again later'; + String get forbidden => 'Your IP address is blocked'; + String forbiddenSub({required Object nickname}) => 'If you are using VPN or Proxy, turn it off. If this does not help, reach out to ${nickname}'; + String get tooManyRequests => 'You have been rate limited.'; + String get tooManyRequestsSub => 'Wait a few moments and try again'; String get internal => 'Something happend on the tetr.io side'; + String get internalSub => 'osk, probably, already aware about it'; String get internalWebVersion => 'Something happend on the tetr.io side (or on oskware_bridge, idk honestly)'; - String get oskwareBridge => 'Something happend with oskware_bridge. Let dan63047 know'; - String get p1nkl0bst3rForbidden => 'Third party API blocked your IP address.\nChange IP address or reach out to p1nkl0bst3r'; + String get internalWebVersionSub => 'If osk status page says that everything is ok, let dan63047 know about this issue'; + String get oskwareBridge => 'Something happend with oskware_bridge'; + String get oskwareBridgeSub => 'Let dan63047 know'; + String get p1nkl0bst3rForbidden => 'Third party API blocked your IP address'; String get p1nkl0bst3rTooManyRequests => 'Too many requests to third party API. Try again later'; String get p1nkl0bst3rinternal => 'Something happend on the p1nkl0bst3r side'; String get p1nkl0bst3rinternalWebVersion => 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; String get replayAlreadySaved => 'Replay already saved'; String get replayExpired => 'Replay expired and not available anymore'; - String get replayRejected => 'Third party API blocked your IP address.\nChange IP address or reach out to szy'; + String get replayRejected => 'Third party API blocked your IP address'; } // Path: @@ -796,6 +822,14 @@ class _StringsRu implements Translations { @override String get compare => 'Сравнить'; @override String get tlLeaderboard => 'Рейтинговая таблица'; @override String get noRecords => 'Нет записей'; + @override String noOldRecords({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: 'Нет записей', + one: 'Всего один матч', + two: 'Всего ${n} матча', + few: 'Всего ${n} матча', + many: 'Всего ${n} матчей', + other: '${n} матчей', + ); @override String get noRecord => 'Нет рекорда'; @override String get botRecord => 'Ботам нельзя ставить рекорды'; @override String get anonRecord => 'Гостям нельзя ставить рекорды'; @@ -820,6 +854,9 @@ class _StringsRu implements Translations { @override String comparingWith({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; @override String get top => 'Топ'; @override String get topRank => 'Топ Ранг'; + @override String verdictGeneral({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; + @override String get verdictBetter => 'Лучше'; + @override String get verdictWorse => 'Хуже'; @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; @override String get nerdStats => 'Для задротов'; @override String get playersYouTrack => 'Отслеживаемые игроки'; @@ -843,8 +880,14 @@ class _StringsRu implements Translations { @override String get yourIDAlertTitle => 'Ваш ник в TETR.IO'; @override String get yourIDText => 'При запуске приложения оно будет получать статистику этого игрока.'; @override String get language => 'Язык (Language)'; + @override String get customization => 'Кастомизация'; + @override String get customizationDescription => 'Здесь только один переключатель, в планах добавить больше'; + @override String get lbStats => 'Показывать статистику, основанную на рейтинговой таблице'; + @override String get lbStatsDescription => 'Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате'; @override String get aboutApp => 'О приложении'; @override String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; + @override String get oskKagari => '"Оск Кагари" прикол'; + @override String get oskKagariDescription => 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:'; @override String stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; @override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; @override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${nickname}'; @@ -1300,7 +1343,7 @@ class _StringsNumOfGameActionsRu implements _StringsNumOfGameActionsEn { ); @override String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, zero: '${n} T-спинов всего', - one: 'всего ${n} T-спин', + one: 'Всего ${n} T-спин', two: '${n} T-спина всего', few: '${n} T-спина всего', many: '${n} T-спинов всего', @@ -1337,21 +1380,30 @@ class _StringsErrorsRu implements _StringsErrorsEn { // Translations @override String connection({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; @override String get noSuchUser => 'Нет такого пользователя'; + @override String get noSuchUserSub => 'Либо вы ошиблись при вводе, либо аккаунта больше не существует'; + @override String get discordNotAssigned => 'К данному Discord ID не привязан аккаунт'; + @override String get discordNotAssignedSub => 'Убедитесь в том, что вы вставили правильный ID'; @override String get history => 'История данного игрока отсутствует'; + @override String get actionSuggestion => 'Возможно, вы хотите'; @override String get p1nkl0bst3rTLmatches => 'Старых матчей Тетра Лиги не было найдено'; @override String get clientException => 'Нет соединения с интернетом'; - @override String get forbidden => 'Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом'; - @override String get tooManyRequests => 'Слишком много запросов. Попробуйте позже'; + @override String get forbidden => 'Ваш IP адрес заблокирован'; + @override String forbiddenSub({required Object nickname}) => 'Если у вас работает VPN или прокси, выключите его. Если это не помогло, свяжитесь с ${nickname}'; + @override String get tooManyRequests => 'Слишком много запросов'; + @override String get tooManyRequestsSub => 'Подождите немного и попробуйте снова'; @override String get internal => 'Что-то случилось на стороне tetr.io'; + @override String get internalSub => 'Скорее всего, osk уже в курсе об этом'; @override String get internalWebVersion => 'Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)'; - @override String get oskwareBridge => 'Что-то случилось с oskware_bridge. Дайте dan63047 знать'; - @override String get p1nkl0bst3rForbidden => 'Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с p1nkl0bst3r-ом'; + @override String get internalWebVersionSub => 'Если статус страница osk-а говорит, что всё ок - свяжитесь с dan63047'; + @override String get oskwareBridge => 'Что-то случилось с oskware_bridge'; + @override String get oskwareBridgeSub => 'Дайте dan63047 знать'; + @override String get p1nkl0bst3rForbidden => 'Стороннее API заблокировало ваш IP адрес'; @override String get p1nkl0bst3rTooManyRequests => 'Слишком много запросов к стороннему API. Попробуйте позже'; @override String get p1nkl0bst3rinternal => 'Что-то случилось на стороне p1nkl0bst3r-а'; @override String get p1nkl0bst3rinternalWebVersion => 'Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)'; @override String get replayAlreadySaved => 'Повтор уже сохранён'; @override String get replayExpired => 'Повтор истёк и больше недоступен'; - @override String get replayRejected => 'Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с szy'; + @override String get replayRejected => 'Стороннее API заблокировало ваш IP адрес'; } /// Flat map(s) containing all translations. @@ -1403,6 +1455,14 @@ extension on Translations { case 'stoppedBeingTracked': return 'Removed from tracking list!'; case 'tlLeaderboard': return 'Tetra League leaderboard'; case 'noRecords': return 'No records'; + case 'noOldRecords': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'No records', + one: '${n} record', + two: '${n} records', + few: '${n} records', + many: '${n} records', + other: '${n} records', + ); case 'noRecord': return 'No record'; case 'botRecord': return 'Bots are not allowed to set records'; case 'anonRecord': return 'Guests are not allowed to set records'; @@ -1427,6 +1487,9 @@ extension on Translations { case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; case 'top': return 'Top'; case 'topRank': return 'Top Rank'; + case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; + case 'verdictBetter': return 'better'; + case 'verdictWorse': return 'worse'; case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; case 'nerdStats': return 'Nerd Stats'; case 'playersYouTrack': return 'Players you track'; @@ -1450,8 +1513,14 @@ extension on Translations { case 'yourIDAlertTitle': return 'Your nickname in TETR.IO'; case 'yourIDText': return 'When app loads, it will retrieve data for this account'; case 'language': return 'Language'; + case 'customization': return 'Customization'; + case 'customizationDescription': return 'There is only one toggle, planned to add more settings'; + case 'lbStats': return 'Show leaderboard based stats'; + case 'lbStatsDescription': return 'That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values'; case 'aboutApp': return 'About app'; case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; + case 'oskKagari': return 'Osk Kagari gimmick'; + case 'oskKagariDescription': return 'If on, osk\'s rank on main view will be rendered as :kagari:'; case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} account on ${date}'; case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches'; @@ -1619,21 +1688,30 @@ extension on Translations { case 'popupActions.ok': return 'OK'; case 'errors.connection': return ({required Object code, required Object message}) => 'Some issue with connection: ${code} ${message}'; case 'errors.noSuchUser': return 'No such user'; + case 'errors.noSuchUserSub': return 'Either you mistyped something, or the account no longer exists'; + case 'errors.discordNotAssigned': return 'No user assigned to given Discord ID'; + case 'errors.discordNotAssignedSub': return 'Make sure you provided valid ID'; case 'errors.history': return 'History for that player is missing'; + case 'errors.actionSuggestion': return 'Perhaps, you want to'; case 'errors.p1nkl0bst3rTLmatches': return 'No Tetra League matches was found'; case 'errors.clientException': return 'No internet connection'; - case 'errors.forbidden': return 'Your IP address is blocked.\nChange IP address or reach out to osk'; - case 'errors.tooManyRequests': return 'You have been rate limited. Try again later'; + case 'errors.forbidden': return 'Your IP address is blocked'; + case 'errors.forbiddenSub': return ({required Object nickname}) => 'If you are using VPN or Proxy, turn it off. If this does not help, reach out to ${nickname}'; + case 'errors.tooManyRequests': return 'You have been rate limited.'; + case 'errors.tooManyRequestsSub': return 'Wait a few moments and try again'; case 'errors.internal': return 'Something happend on the tetr.io side'; + case 'errors.internalSub': return 'osk, probably, already aware about it'; case 'errors.internalWebVersion': return 'Something happend on the tetr.io side (or on oskware_bridge, idk honestly)'; - case 'errors.oskwareBridge': return 'Something happend with oskware_bridge. Let dan63047 know'; - case 'errors.p1nkl0bst3rForbidden': return 'Third party API blocked your IP address.\nChange IP address or reach out to p1nkl0bst3r'; + case 'errors.internalWebVersionSub': return 'If osk status page says that everything is ok, let dan63047 know about this issue'; + case 'errors.oskwareBridge': return 'Something happend with oskware_bridge'; + case 'errors.oskwareBridgeSub': return 'Let dan63047 know'; + case 'errors.p1nkl0bst3rForbidden': return 'Third party API blocked your IP address'; case 'errors.p1nkl0bst3rTooManyRequests': return 'Too many requests to third party API. Try again later'; case 'errors.p1nkl0bst3rinternal': return 'Something happend on the p1nkl0bst3r side'; case 'errors.p1nkl0bst3rinternalWebVersion': return 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; case 'errors.replayAlreadySaved': return 'Replay already saved'; case 'errors.replayExpired': return 'Replay expired and not available anymore'; - case 'errors.replayRejected': return 'Third party API blocked your IP address.\nChange IP address or reach out to szy'; + case 'errors.replayRejected': return 'Third party API blocked your IP address'; case 'countries.': return 'Not selected'; case 'countries.AF': return 'Afghanistan'; case 'countries.AX': return 'Åland Islands'; @@ -1944,6 +2022,14 @@ extension on _StringsRu { case 'compare': return 'Сравнить'; case 'tlLeaderboard': return 'Рейтинговая таблица'; case 'noRecords': return 'Нет записей'; + case 'noOldRecords': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: 'Нет записей', + one: 'Всего один матч', + two: 'Всего ${n} матча', + few: 'Всего ${n} матча', + many: 'Всего ${n} матчей', + other: '${n} матчей', + ); case 'noRecord': return 'Нет рекорда'; case 'botRecord': return 'Ботам нельзя ставить рекорды'; case 'anonRecord': return 'Гостям нельзя ставить рекорды'; @@ -1968,6 +2054,9 @@ extension on _StringsRu { case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; case 'top': return 'Топ'; case 'topRank': return 'Топ Ранг'; + case 'verdictGeneral': return ({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; + case 'verdictBetter': return 'Лучше'; + case 'verdictWorse': return 'Хуже'; case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; case 'nerdStats': return 'Для задротов'; case 'playersYouTrack': return 'Отслеживаемые игроки'; @@ -1991,8 +2080,14 @@ extension on _StringsRu { case 'yourIDAlertTitle': return 'Ваш ник в TETR.IO'; case 'yourIDText': return 'При запуске приложения оно будет получать статистику этого игрока.'; case 'language': return 'Язык (Language)'; + case 'customization': return 'Кастомизация'; + case 'customizationDescription': return 'Здесь только один переключатель, в планах добавить больше'; + case 'lbStats': return 'Показывать статистику, основанную на рейтинговой таблице'; + case 'lbStatsDescription': return 'Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате'; case 'aboutApp': return 'О приложении'; case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; + case 'oskKagari': return '"Оск Кагари" прикол'; + case 'oskKagariDescription': return 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:'; case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${nickname}'; @@ -2141,7 +2236,7 @@ extension on _StringsRu { ); case 'numOfGameActions.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, zero: '${n} T-спинов всего', - one: 'всего ${n} T-спин', + one: 'Всего ${n} T-спин', two: '${n} T-спина всего', few: '${n} T-спина всего', many: '${n} T-спинов всего', @@ -2160,21 +2255,30 @@ extension on _StringsRu { case 'popupActions.ok': return 'OK'; case 'errors.connection': return ({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; case 'errors.noSuchUser': return 'Нет такого пользователя'; + case 'errors.noSuchUserSub': return 'Либо вы ошиблись при вводе, либо аккаунта больше не существует'; + case 'errors.discordNotAssigned': return 'К данному Discord ID не привязан аккаунт'; + case 'errors.discordNotAssignedSub': return 'Убедитесь в том, что вы вставили правильный ID'; case 'errors.history': return 'История данного игрока отсутствует'; + case 'errors.actionSuggestion': return 'Возможно, вы хотите'; case 'errors.p1nkl0bst3rTLmatches': return 'Старых матчей Тетра Лиги не было найдено'; case 'errors.clientException': return 'Нет соединения с интернетом'; - case 'errors.forbidden': return 'Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом'; - case 'errors.tooManyRequests': return 'Слишком много запросов. Попробуйте позже'; + case 'errors.forbidden': return 'Ваш IP адрес заблокирован'; + case 'errors.forbiddenSub': return ({required Object nickname}) => 'Если у вас работает VPN или прокси, выключите его. Если это не помогло, свяжитесь с ${nickname}'; + case 'errors.tooManyRequests': return 'Слишком много запросов'; + case 'errors.tooManyRequestsSub': return 'Подождите немного и попробуйте снова'; case 'errors.internal': return 'Что-то случилось на стороне tetr.io'; + case 'errors.internalSub': return 'Скорее всего, osk уже в курсе об этом'; case 'errors.internalWebVersion': return 'Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)'; - case 'errors.oskwareBridge': return 'Что-то случилось с oskware_bridge. Дайте dan63047 знать'; - case 'errors.p1nkl0bst3rForbidden': return 'Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с p1nkl0bst3r-ом'; + case 'errors.internalWebVersionSub': return 'Если статус страница osk-а говорит, что всё ок - свяжитесь с dan63047'; + case 'errors.oskwareBridge': return 'Что-то случилось с oskware_bridge'; + case 'errors.oskwareBridgeSub': return 'Дайте dan63047 знать'; + case 'errors.p1nkl0bst3rForbidden': return 'Стороннее API заблокировало ваш IP адрес'; case 'errors.p1nkl0bst3rTooManyRequests': return 'Слишком много запросов к стороннему API. Попробуйте позже'; case 'errors.p1nkl0bst3rinternal': return 'Что-то случилось на стороне p1nkl0bst3r-а'; case 'errors.p1nkl0bst3rinternalWebVersion': return 'Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)'; case 'errors.replayAlreadySaved': return 'Повтор уже сохранён'; case 'errors.replayExpired': return 'Повтор истёк и больше недоступен'; - case 'errors.replayRejected': return 'Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с szy'; + case 'errors.replayRejected': return 'Стороннее API заблокировало ваш IP адрес'; case 'countries.': return 'Не выбрана'; case 'countries.AF': return 'Афганистан'; case 'countries.AX': return 'Аландские острова'; diff --git a/lib/services/crud_exceptions.dart b/lib/services/crud_exceptions.dart index 303eaa0..3f34f91 100644 --- a/lib/services/crud_exceptions.dart +++ b/lib/services/crud_exceptions.dart @@ -14,6 +14,8 @@ class TetrioPlayerAlreadyExist implements Exception {} class TetrioPlayerNotExist implements Exception {} +class TetrioDiscordNotExist implements Exception {} + class TetrioHistoryNotExist implements Exception {} class TetrioTooManyRequests implements Exception {} diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 4090a38..947ae12 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -974,7 +974,7 @@ class TetrioService extends DB { user = json['data']['user']['_id']; } else { // fail - throw an exception developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); - throw TetrioPlayerNotExist(); + throw TetrioDiscordNotExist(); } break; // more exceptions to god of exceptions diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index af7bf36..3f626fc 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -6,4 +6,5 @@ final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettin final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); -final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..minimumFractionDigits = 0; \ No newline at end of file +final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..minimumFractionDigits = 0; +final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode); \ No newline at end of file diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart index 5c9334e..7762c13 100644 --- a/lib/views/calc_view.dart +++ b/lib/views/calc_view.dart @@ -69,7 +69,7 @@ class CalcState extends State { body: SafeArea( child: Center( child: Container( - constraints: BoxConstraints(maxWidth: 768), + constraints: const BoxConstraints(maxWidth: 768), child: NestedScrollView( controller: _scrollController, headerSliverBuilder: (context, value) { diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index c3755fb..8ac89d9 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -258,7 +258,7 @@ class CompareState extends State { body: SafeArea( child: Center( child: Container( - constraints: BoxConstraints(maxWidth: 768), + constraints: const BoxConstraints(maxWidth: 768), child: NestedScrollView( controller: _scrollController, headerSliverBuilder: (context, value) { @@ -327,7 +327,7 @@ class CompareState extends State { }, body: Center( child: Container( - constraints: BoxConstraints(maxWidth: 768), + constraints: const BoxConstraints(maxWidth: 768), child: ListView( children: !listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])? [ if (theGreenSide[0] != null && diff --git a/lib/views/customization_view.dart b/lib/views/customization_view.dart index 71806ba..e7c22a4 100644 --- a/lib/views/customization_view.dart +++ b/lib/views/customization_view.dart @@ -20,6 +20,7 @@ class CustomizationView extends StatefulWidget { class CustomizationState extends State { late SharedPreferences prefs; + late bool oskKagariGimmick; void changeColor(Color color) { setState(() => pickerColor = color); @@ -31,7 +32,7 @@ class CustomizationState extends State { windowManager.getTitle().then((value) => oldWindowTitle = value); windowManager.setTitle("Tetra Stats: ${t.settings}"); } - _getPreferences(); + _getPreferences().then((value) => setState((){})); super.initState(); } @@ -43,6 +44,11 @@ class CustomizationState extends State { Future _getPreferences() async { prefs = await SharedPreferences.getInstance(); + if (prefs.getBool("oskKagariGimmick") != null) { + oskKagariGimmick = prefs.getBool("oskKagariGimmick")!; + } else { + oskKagariGimmick = true; + } } ThemeData getTheme(BuildContext context, Color color){ @@ -66,59 +72,48 @@ class CustomizationState extends State { body: SafeArea( child: ListView( children: [ - ListTile( - title: const Text("Accent Color"), - trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary)), - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: const Text('Pick a color!'), - content: SingleChildScrollView( - child: ColorPicker( - pickerColor: pickerColor, - onColorChanged: changeColor, - ), - // Use Material color picker: - // - // child: MaterialPicker( - // pickerColor: pickerColor, - // onColorChanged: changeColor, - // showLabel: true, // only on portrait mode - // ), - // - // Use Block color picker: - // - // child: BlockPicker( - // pickerColor: currentColor, - // onColorChanged: changeColor, - // ), - // - // child: MultipleChoiceBlockPicker( - // pickerColors: currentColors, - // onColorsChanged: changeColors, - // ), - ), - actions: [ - ElevatedButton( - child: const Text('Got it'), - onPressed: () { - setState(() { - setAccentColor(pickerColor); - }); - Navigator.of(context).pop(); - }, - ), - ])); - }), - const ListTile( - title: Text("Font"), - subtitle: Text("Not implemented"), - ), - const ListTile( - title: Text("Stats Table in TL mathes list"), - subtitle: Text("Not implemented"), - ), + // ListTile( + // title: const Text("Accent color"), + // trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary)), + // onTap: () { + // showDialog( + // context: context, + // builder: (BuildContext context) => AlertDialog( + // title: const Text('Pick an accent color'), + // content: SingleChildScrollView( + // child: ColorPicker( + // pickerColor: pickerColor, + // onColorChanged: changeColor, + // ), + // ), + // actions: [ + // ElevatedButton( + // child: const Text('Set'), + // onPressed: () { + // setState(() { + // setAccentColor(pickerColor); + // }); + // Navigator.of(context).pop(); + // }, + // ), + // ])); + // }), + // const ListTile( + // title: Text("Font"), + // subtitle: Text("Not implemented"), + // ), + // const ListTile( + // title: Text("Stats Table in TL mathes list"), + // subtitle: Text("Not implemented"), + // ), + ListTile(title: Text(t.oskKagari), + subtitle: Text(t.oskKagariDescription), + trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){ + prefs.setBool("oskKagariGimmick", value); + setState(() { + oskKagariGimmick = value; + }); + }),) ], )), ); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index ec0c77f..fb890d6 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -34,6 +34,7 @@ import 'package:go_router/go_router.dart'; final TetrioService teto = TetrioService(); // thing, that manadge our local DB int _chartsIndex = 0; +bool _showHistoryAsTable = 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); @@ -84,8 +85,10 @@ class _MainState extends State with TickerProviderStateMixin { String _titleNickname = "dan63047"; /// Each dropdown menu item contains list of dots for the graph var chartsData = >>[]; + //var tableData = []; final bodyGlobalKey = GlobalKey(); bool _showSearchBar = false; + bool _TLHistoryWasFetched = false; late TabController _tabController; late TabController _wideScreenTabController; late bool fixedScroll; @@ -143,6 +146,7 @@ class _MainState extends State with TickerProviderStateMixin { /// If at least one request to Tetra Channel API fails, whole function will throw an exception. Future fetch(String nickOrID, {bool fetchHistory = false, bool fetchTLmatches = false}) async { TetrioPlayer me; + _TLHistoryWasFetched = false; // If user trying to search with discord id if (nickOrID.startsWith("ds:")){ @@ -211,8 +215,11 @@ class _MainState extends State with TickerProviderStateMixin { if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); }on P1nkl0bst3rTooManyRequests{ if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); + }finally{ + _TLHistoryWasFetched = true; } - } + } + if (storedRecords.isNotEmpty) _TLHistoryWasFetched = true; for (var match in storedRecords) { // add stored match to list only if it missing from retrived ones if (!tlMatches.contains(match)) tlMatches.add(match); @@ -246,6 +253,11 @@ class _MainState extends State with TickerProviderStateMixin { if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1); } // Also i need previous Tetra League State for comparison if avaliable + // tableData = [ + // TableRow(children: [ Text("Date & Time"), Text("Tr"), Text("Glicko"), Text("RD"), Text("GP"), Text("GW"), Text("APM"), Text("PPS"), Text("VS"), Text("APP"), Text("VS/APM"), Text("DS/S"), Text("DS/P"), Text("APP+DS/P"), Text("Cheese"), Text("GbE"), Text("wAPP"), Text("Area"), Text("eTR"), Text("±eTR"), Text("Opener"), Text("Plonk"), Text("Inf. DS"), Text("Stride")], + // decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.white)))), + // for (var state in states) TableRow(children: [Text(dateFormat.format(state.tlSeason1.timestamp)), Text(f4.format(state.tlSeason1.rating)), Text(f4.format(state.tlSeason1.glicko)), Text(f4.format(state.tlSeason1.rd)), Text(f0.format(state.tlSeason1.gamesPlayed)), Text(f0.format(state.tlSeason1.gamesWon)), Text(f2.format(state.tlSeason1.apm)), Text(f2.format(state.tlSeason1.pps)), Text(state.tlSeason1.vs != null ? f2.format(state.tlSeason1.vs) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.app) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.vsapm) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.dss) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.dsp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.appdsp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.cheese) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.gbe) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.nyaapp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.area) : "---"), Text(state.tlSeason1.estTr != null ? f4.format(state.tlSeason1.estTr?.esttr) : "---"), Text(state.tlSeason1.esttracc != null ? f4.format(state.tlSeason1.esttracc) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.opener) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.plonk) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.infds) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.stride) : "---")]), + // ]; if (uniqueTL.length >= 2){ compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid @@ -381,9 +393,9 @@ class _MainState extends State with TickerProviderStateMixin { return notification.depth == 0; }, child: NestedScrollView( + scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), physics: const AlwaysScrollableScrollPhysics(), controller: _scrollController, - scrollBehavior: const MaterialScrollBehavior(), headerSliverBuilder: (context, value) { return [ SliverToBoxAdapter( @@ -422,7 +434,7 @@ class _MainState extends State with TickerProviderStateMixin { children: [ Container( width: MediaQuery.of(context).size.width-450, - constraints: BoxConstraints(maxWidth: 1024), + constraints: const BoxConstraints(maxWidth: 1024), child: TLThingy( tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, @@ -436,23 +448,11 @@ class _MainState extends State with TickerProviderStateMixin { ), SizedBox( width: 450, - child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0) + child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched) ), ],), - _History(chartsData: chartsData, states: snapshot.data![2], changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,), - // Row(children: [ - // Container( - // width: MediaQuery.of(context).size.width/2, - // padding: EdgeInsets.only(right: 8), - // child: _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank) - // ), - // Container( - // width: MediaQuery.of(context).size.width/2, - // padding: EdgeInsets.only(left: 8), - // child: _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank) - // ), - // ],), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) ] : [ TLThingy( @@ -465,8 +465,8 @@ class _MainState extends State with TickerProviderStateMixin { averages: rankAverages, lbPositions: meAmongEveryone ), - _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), - _History(chartsData: chartsData, states: snapshot.data![2], changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _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), _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) @@ -476,28 +476,34 @@ class _MainState extends State with TickerProviderStateMixin { ); } else if (snapshot.hasError) { String errText = ""; + String? subText; switch (snapshot.error.runtimeType){ case TetrioPlayerNotExist: errText = t.errors.noSuchUser; + subText = t.errors.noSuchUserSub; break; + case TetrioDiscordNotExist: + errText = t.errors.discordNotAssigned; + subText = t.errors.discordNotAssignedSub; case ConnectionIssue: var err = snapshot.error as ConnectionIssue; errText = t.errors.connection(code: err.code, message: err.message); break; - case TetrioHistoryNotExist: - errText = t.errors.history; - break; case TetrioForbidden: errText = t.errors.forbidden; + subText = t.errors.forbiddenSub(nickname: 'osk'); break; case TetrioTooManyRequests: errText = t.errors.tooManyRequests; + subText = t.errors.tooManyRequestsSub; break; case TetrioOskwareBridgeProblem: errText = t.errors.oskwareBridge; + subText = t.errors.oskwareBridgeSub; break; case TetrioInternalProblem: errText = kIsWeb ? t.errors.internalWebVersion : t.errors.internal; + subText = kIsWeb ? t.errors.internalWebVersionSub : t.errors.internalSub; break; case ClientException: errText = t.errors.clientException; @@ -505,7 +511,18 @@ class _MainState extends State with TickerProviderStateMixin { default: errText = snapshot.error.toString(); } - return Center(child: Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center)); + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + if (subText != null) Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18)), + ), + ], + ) + ); } break; } @@ -646,27 +663,42 @@ class _TLRecords extends StatelessWidget { final Function changePlayer; final List data; final bool wasActiveInTL; + final bool oldMathcesHere; /// Widget, that displays Tetra League records. /// Accepts list of TL records ([data]) and [userID] of player from the view - const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL}); + const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere}); @override Widget build(BuildContext context) { - if (data.isEmpty) return Center(child: Column( + if (data.isEmpty) { + return Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text("Perhaps, you want to"), + if (wasActiveInTL) Text(t.errors.actionSuggestion), if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) ], )); + } bool bigScreen = MediaQuery.of(context).size.width >= 768; + int length = data.length; return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), controller: ScrollController(), - itemCount: data.length, + itemCount: oldMathcesHere ? length : length + 1, itemBuilder: (BuildContext context, int index) { + if (index == length) { + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text(t.errors.actionSuggestion), + if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) + ], + )); + } + var accentColor = data[index].endContext.firstWhere((element) => element.userId == userID).success ? Colors.green : Colors.red; return Container( decoration: BoxDecoration( @@ -698,7 +730,6 @@ class _TLRecords extends StatelessWidget { class _History extends StatelessWidget{ final List>> chartsData; - final List states; final String userID; final Function update; final Function changePlayer; @@ -706,41 +737,67 @@ class _History extends StatelessWidget{ /// Widget, that can show history of some stat of the player on the graph. /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.chartsData, required this.states, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); + const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); @override Widget build(BuildContext context) { - bool bigScreen = MediaQuery.of(context).size.width > 768; - return chartsData.isNotEmpty ? - Column( - children: [ - DropdownButton( - items: chartsData, - value: chartsData[_chartsIndex].value, - onChanged: (value) { - _chartsIndex = chartsData.indexWhere((element) => element.value == value); - update(); - } - ), - if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),) - else Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text("Perhaps, you want"), - if (wasActiveInTL)TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) - ], - )) - ], - ) - : Center(child: Column( + if (chartsData.isEmpty) { + return Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text("Perhaps, you want"), - if (wasActiveInTL)TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) + if (wasActiveInTL) Text(t.errors.actionSuggestion), + if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) ], )); + } + bool bigScreen = MediaQuery.of(context).size.width > 768; + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + primary: true, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 20, + children: [ + // DropdownButton( + // items: [DropdownMenuItem(child: Text("Chart"), value: false), DropdownMenuItem(child: Text("Table"), value: true)], + // value: _showHistoryAsTable, + // onChanged: (value) { + // _showHistoryAsTable = value!; + // update(); + // } + // ), + DropdownButton( + items: chartsData, + value: chartsData[_chartsIndex].value, + onChanged: (value) { + _chartsIndex = chartsData.indexWhere((element) => element.value == value); + update(); + } + ), + ], + ), + if(chartsData[_chartsIndex].value!.length > 1 && !_showHistoryAsTable) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),) + else if (chartsData[_chartsIndex].value!.length <= 1 && !_showHistoryAsTable) Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text(t.errors.actionSuggestion), + if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) + ], + )) + // else if (_showHistoryAsTable) Padding( + // padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + // child: _HistoryTableThingy(tableData), + // ) + ], + ), + ), + ); } } @@ -1020,22 +1077,28 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { } } -class _HistoryTableThingy extends StatelessWidget{ - final List states; +// class _HistoryTableThingy extends StatelessWidget{ +// final List tableData; - const _HistoryTableThingy(this.states); - // :tf: - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints){ - return SingleChildScrollView(child: Table( - children: [ - TableRow(children: [Text("Date & Time"), Text("Tr")]), - ], - )); - }); - } -} +// const _HistoryTableThingy(this.tableData); +// // :tf: +// @override +// Widget build(BuildContext context) { +// return LayoutBuilder(builder: (context, constraints){ +// return Table( +// defaultColumnWidth: FixedColumnWidth(75), +// columnWidths: { +// 0: FixedColumnWidth(170), +// 1: FixedColumnWidth(100), +// 2: FixedColumnWidth(90), +// 18: FixedColumnWidth(100), +// 19: FixedColumnWidth(90), +// }, +// children: tableData, +// ); +// }); +// } +// } class _TwoRecordsThingy extends StatelessWidget { final RecordSingle? sprint; @@ -1082,13 +1145,13 @@ class _TwoRecordsThingy extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Padding(padding: EdgeInsets.only(right: 8.0), + Padding(padding: const EdgeInsets.only(right: 8.0), child: sprint != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(t.sprint, style: TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), RichText(text: TextSpan( text: sprint != null ? get40lTime(sprint!.endContext!.finalTime.inMicroseconds) : "---", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey), @@ -1099,11 +1162,11 @@ class _TwoRecordsThingy extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (rank != null && rank != "z") TextSpan(text: "${readableTimeDifference(sprint!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.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 TextSpan(text: "${readableTimeDifference(sprint!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key.toUpperCase()} rank average\n", style: TextStyle( - color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext!.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( + color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent )), if (sprint!.rank != null) TextSpan(text: "№${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank!))), if (sprint!.rank != null) const TextSpan(text: " • "), @@ -1157,10 +1220,10 @@ class _TwoRecordsThingy extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (rank != null && rank != "z") TextSpan(text: "${readableIntDifference(blitz!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.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 TextSpan(text: "${readableIntDifference(blitz!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average\n", style: TextStyle( + else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext!.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent )), TextSpan(text: _dateFormat.format(blitz!.timestamp!)), @@ -1170,7 +1233,7 @@ class _TwoRecordsThingy extends StatelessWidget { ), ), ],), - Padding(padding: EdgeInsets.only(left: 8.0), + Padding(padding: const EdgeInsets.only(left: 8.0), child: blitz != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96)), ], ), @@ -1240,17 +1303,17 @@ class _RecordThingy extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - if (record!.stream.contains("40l")) Padding(padding: EdgeInsets.only(right: 8.0), + if (record!.stream.contains("40l")) Padding(padding: const EdgeInsets.only(right: 8.0), child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) ), - if (record!.stream.contains("blitz")) Padding(padding: EdgeInsets.only(right: 8.0), + if (record!.stream.contains("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!.stream.contains("40l")) Text(t.sprint, style: TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - if (record!.stream.contains("blitz")) Text(t.blitz, style: TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + if (record!.stream.contains("40l")) Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + if (record!.stream.contains("blitz")) Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), RichText(text: TextSpan( text: record!.stream.contains("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), @@ -1260,16 +1323,16 @@ class _RecordThingy extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (record!.stream.contains("40l") && (rank != null && rank != "z")) TextSpan(text: "${readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!)} ${sprintBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + if (record!.stream.contains("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!.stream.contains("40l") && (rank == null || rank == "z")) TextSpan(text: "${readableTimeDifference(record!.endContext!.finalTime, closestAverageSprint.value)} ${sprintBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageSprint.key.toUpperCase()} rank average\n", style: TextStyle( - color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + else if (record!.stream.contains("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!.stream.contains("blitz") && (rank != null && rank != "z")) TextSpan(text: "${readableIntDifference(record!.endContext!.score, blitzAverages[rank]!)} ${blitzBetterThanRankAverage??false ? "better" : "worse"} than ${rank!.toUpperCase()} rank average\n", style: TextStyle( + else if (record!.stream.contains("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!.stream.contains("blitz") && (rank == null || rank == "z")) TextSpan(text: "${readableIntDifference(record!.endContext!.score, closestAverageBlitz.value)} ${blitzBetterThanClosestAverage ? "better" : "worse"} than ${closestAverageBlitz.key!.toUpperCase()} rank average\n", style: TextStyle( + else if (record!.stream.contains("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!))), @@ -1566,7 +1629,7 @@ class _OtherThingy extends StatelessWidget { physics: const AlwaysScrollableScrollPhysics(), itemCount: newsletter!.length+1, itemBuilder: (BuildContext context, int index) { - return index == 0 ? Center(child: Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter![index-1]); + return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter![index-1]); } )) ] diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 3aa1c11..979293e 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:go_router/go_router.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/main.dart' show packageInfo; import 'package:file_selector/file_selector.dart'; @@ -260,14 +261,14 @@ class SettingsState extends State { }, ), ), - ListTile(title: const Text("Customization"), - subtitle: const Text("I don't want to implement this"), + ListTile(title: Text(t.customization), + subtitle: Text(t.customizationDescription), trailing: const Icon(Icons.arrow_right), onTap: () { - Navigator.pushNamed(context, "/customization"); + context.go("/customization"); },), - ListTile(title: Text("Show leaderboard based stats"), - subtitle: Text("That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values"), + ListTile(title: Text(t.lbStats), + subtitle: Text(t.lbStatsDescription), trailing: Switch(value: showPositions, onChanged: (bool value){ prefs.setBool("showPositions", value); setState(() { @@ -281,6 +282,7 @@ class SettingsState extends State { }, title: Text(t.aboutApp), subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)), + trailing: const Icon(Icons.arrow_right) ), ], )), diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart index ef83515..b33362d 100644 --- a/lib/widgets/gauget_num.dart +++ b/lib/widgets/gauget_num.dart @@ -98,7 +98,7 @@ class GaugetNum extends StatelessWidget { oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent ),), if ((oldTl != null && oldTl!.gamesPlayed > 0) && pos != null) const TextSpan(text: " • "), - if (pos != null) TextSpan(text: pos!.position >= 1000 ? "Top ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") + if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") ] ), ), diff --git a/lib/widgets/lineclears_thingy.dart b/lib/widgets/lineclears_thingy.dart index 303b607..35b8d3f 100644 --- a/lib/widgets/lineclears_thingy.dart +++ b/lib/widgets/lineclears_thingy.dart @@ -20,11 +20,11 @@ class LineclearsThingy extends StatelessWidget{ child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(t.numOfGameActions.lineClears(n: lines), style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended")), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Quads"), Text(clears.quads.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Triples"), Text(clears.triples.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Doubles"), Text(clears.doubles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Singles"), Text(clears.singles.toString())]), + Text(t.numOfGameActions.lineClears(n: lines), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Quads"), Text(clears.quads.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Triples"), Text(clears.triples.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Doubles"), Text(clears.doubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Singles"), Text(clears.singles.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("\n${t.numOfGameActions.pc}"), Text("\n${clears.allClears.toString()}")]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.numOfGameActions.hold), Text(holds.toString())]), ], @@ -35,14 +35,14 @@ class LineclearsThingy extends StatelessWidget{ child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(t.numOfGameActions.tspinsTotal(n: tSpins), style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended")), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins triples"), Text(clears.tSpinTriples.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins doubles"), Text(clears.tSpinDoubles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins singles"), Text(clears.tSpinSingles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("T-spins zeros"), Text(clears.tSpinZeros.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Mini T-spins doubles"), Text(clears.tSpinMiniDoubles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Mini T-spins singles"), Text(clears.tSpinMiniSingles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]), + Text(t.numOfGameActions.tspinsTotal(n: tSpins), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins triples"), Text(clears.tSpinTriples.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins doubles"), Text(clears.tSpinDoubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins singles"), Text(clears.tSpinSingles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins zeros"), Text(clears.tSpinZeros.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins doubles"), Text(clears.tSpinMiniDoubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins singles"), Text(clears.tSpinMiniSingles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]), ], ), ), diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 06b215d..8ea21a6 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -70,7 +70,7 @@ class StatCellNum extends StatelessWidget { oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent ),), if (oldPlayerStat != null && pos != null) const TextSpan(text: " • "), - if (pos != null) TextSpan(text: pos!.position >= 1000 ? "Top ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") + if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") ] ), ), diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 5b04061..fbcda33 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -3,8 +3,8 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; @@ -38,11 +38,13 @@ class TLThingy extends StatefulWidget { } class _TLThingyState extends State { + late bool oskKagariGimmick; @override void initState() { _currentRangeValues = const RangeValues(0, 1); sortedStates = widget.states.reversed.toList(); + oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; try{ oldTl = sortedStates[1].tlSeason1; }on RangeError{ @@ -97,7 +99,7 @@ class _TLThingyState extends State { crossAxisAlignment: WrapCrossAlignment.center, clipBehavior: Clip.hardEdge, children: [ - widget.userID == "5e32fc85ab319c2ab1beb07c" // he love her so much, you can't even imagine + (widget.userID == "5e32fc85ab319c2ab1beb07c" && oskKagariGimmick) // he love her so much, you can't even imagine ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? : Image.asset("res/tetrio_tl_alpha_ranks/${currentTl.rank}.png", height: 128), Column( @@ -292,27 +294,28 @@ class _TLThingyState extends State { ), if (currentTl.estTr != null) Padding( - padding: const EdgeInsets.fromLTRB(0, 48, 0, 48), + padding: const EdgeInsets.fromLTRB(0, 20, 0, 20), child: Container( //alignment: Alignment.center, width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, + height: 70, constraints: BoxConstraints(maxWidth: 768), - child: Wrap( - alignment: WrapAlignment.spaceBetween, - spacing: 20, + child: Stack( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.statCellNum.estOfTR, style: TextStyle(height: 0.1),), - RichText( - text: TextSpan( - text: intf.format(currentTl.estTr!.esttr.truncate()), - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), - children: [TextSpan(text: fractionfEstTR.format(currentTl.estTr!.esttr - currentTl.estTr!.esttr.truncate()).substring(1), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + Positioned( + left: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.statCellNum.estOfTR, style: TextStyle(height: 0.1),), + RichText( + text: TextSpan( + text: intf.format(currentTl.estTr!.esttr.truncate()), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), + children: [TextSpan(text: fractionfEstTR.format(currentTl.estTr!.esttr - currentTl.estTr!.esttr.truncate()).substring(1), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), ), - ), - if (oldTl?.estTr?.esttr != null || widget.lbPositions != null) RichText(text: TextSpan( + RichText(text: TextSpan( text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), children: [ @@ -320,78 +323,46 @@ class _TLThingyState extends State { color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent ),), if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), - if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}"), + if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}"), if (widget.lbPositions?.estTr != null) const TextSpan(text: " • "), TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") ] ), ), - ],), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(t.statCellNum.accOfEst, style: const TextStyle(height: 0.1),), - RichText( - text: TextSpan( - text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? intFDiff.format(currentTl.esttracc!.truncate()) : "---", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + ],), + ), + Positioned( + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(t.statCellNum.accOfEst, style: const TextStyle(height: 0.1),), + RichText( + text: TextSpan( + text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? intFDiff.format(currentTl.esttracc!.truncate()) : "---", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), + children: [ + TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) + ] + ), + ), + if ((oldTl?.esttracc != null || widget.lbPositions != null) && currentTl.bestRank != "z") RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), children: [ - TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) + if (oldTl?.esttracc != null) TextSpan(text: comparef.format(currentTl.esttracc! - oldTl!.esttracc!), style: TextStyle( + color: oldTl!.esttracc! > currentTl.esttracc! ? Colors.redAccent : Colors.greenAccent + ),), + if (oldTl?.esttracc != null && widget.lbPositions?.accOfEst != null) const TextSpan(text: " • "), + if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.accOfEst!.percentage*100)}%" : "№${widget.lbPositions!.accOfEst!.position}") ] ), ), - if ((oldTl?.esttracc != null || widget.lbPositions != null) && currentTl.bestRank != "z") RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), - children: [ - if (oldTl?.esttracc != null) TextSpan(text: comparef.format(currentTl.esttracc! - oldTl!.esttracc!), style: TextStyle( - color: oldTl!.esttracc! > currentTl.esttracc! ? Colors.redAccent : Colors.greenAccent - ),), - if (oldTl?.esttracc != null && widget.lbPositions?.accOfEst != null) const TextSpan(text: " • "), - if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst!.position >= 1000 ? "Top ${f2.format(widget.lbPositions!.accOfEst!.percentage*100)}%" : "№${widget.lbPositions!.accOfEst!.position}") - ] - ), - ), - ],) + ],), + ) ], ), ) - // child: Container( - // width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, - // constraints: BoxConstraints(maxWidth: 452), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "${bigScreen ? t.statCellNum.estOfTR : t.statCellNum.estOfTRShort}:", - // style: const TextStyle(fontSize: 24), - // ), - // Text( - // f2.format(currentTl.estTr!.esttr), - // style: const TextStyle(fontSize: 24), - // ), - // ], - // ), - // if (currentTl.rating >= 0) - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "${bigScreen ? t.statCellNum.accOfEst : t.statCellNum.accOfEstShort}:", - // style: const TextStyle(fontSize: 24), - // ), - // Text( - // fDiff.format(currentTl.esttracc!), - // style: const TextStyle(fontSize: 24), - // ), - // ], - // ), - // ], - // ), - // ), ), if (currentTl.nerdStats != null) Graphs(currentTl.apm!, currentTl.pps!, currentTl.vs!, currentTl.nerdStats!, currentTl.playstyle!) ] diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index d971f7e..653b33e 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -91,6 +91,7 @@ class UserThingy extends StatelessWidget { ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) : player.avatarRevision != null ? Image.network("https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + // TODO: osk banner can cause memory leak fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace); return Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight); diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 79ec395..63ec0f8 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -46,6 +46,14 @@ "stoppedBeingTracked": "Removed from tracking list!", "tlLeaderboard": "Tetra League leaderboard", "noRecords": "No records", + "noOldRecords": { + "zero": "No records", + "one": "$n record", + "two": "$n records", + "few": "$n records", + "many": "$n records", + "other": "$n records" + }, "noRecord": "No record", "botRecord": "Bots are not allowed to set records", "anonRecord": "Guests are not allowed to set records", @@ -70,6 +78,9 @@ "comparingWith": "Data from ${newDate} comparing with ${oldDate}", "top": "Top", "topRank": "Top Rank", + "verdictGeneral": "$n $verdict than $rank rank average", + "verdictBetter": "better", + "verdictWorse": "worse", "gamesUntilRanked": "${left} games until being ranked", "nerdStats": "Nerd Stats", "playersYouTrack": "Players you track", @@ -93,8 +104,14 @@ "yourIDAlertTitle": "Your nickname in TETR.IO", "yourIDText": "When app loads, it will retrieve data for this account", "language": "Language", + "customization": "Customization", + "customizationDescription": "There is only one toggle, planned to add more settings", + "lbStats": "Show leaderboard based stats", + "lbStatsDescription": "That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values", "aboutApp": "About app", "aboutAppText": "${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy", + "oskKagari": "Osk Kagari gimmick", + "oskKagariDescription": "If on, osk's rank on main view will be rendered as :kagari:", "stateViewTitle": "${nickname} account on ${date}", "statesViewTitle": "${number} states of ${nickname} account", "matchesViewTitle": "${nickname} TL matches", @@ -271,21 +288,30 @@ "errors":{ "connection": "Some issue with connection: ${code} ${message}", "noSuchUser": "No such user", + "noSuchUserSub": "Either you mistyped something, or the account no longer exists", + "discordNotAssigned": "No user assigned to given Discord ID", + "discordNotAssignedSub": "Make sure you provided valid ID", "history": "History for that player is missing", + "actionSuggestion": "Perhaps, you want to", "p1nkl0bst3rTLmatches": "No Tetra League matches was found", "clientException": "No internet connection", - "forbidden": "Your IP address is blocked.\nChange IP address or reach out to osk", - "tooManyRequests": "You have been rate limited. Try again later", + "forbidden": "Your IP address is blocked", + "forbiddenSub": "If you are using VPN or Proxy, turn it off. If this does not help, reach out to $nickname", + "tooManyRequests": "You have been rate limited.", + "tooManyRequestsSub": "Wait a few moments and try again", "internal": "Something happend on the tetr.io side", + "internalSub": "osk, probably, already aware about it", "internalWebVersion": "Something happend on the tetr.io side (or on oskware_bridge, idk honestly)", - "oskwareBridge": "Something happend with oskware_bridge. Let dan63047 know", - "p1nkl0bst3rForbidden": "Third party API blocked your IP address.\nChange IP address or reach out to p1nkl0bst3r", + "internalWebVersionSub": "If osk status page says that everything is ok, let dan63047 know about this issue", + "oskwareBridge": "Something happend with oskware_bridge", + "oskwareBridgeSub": "Let dan63047 know", + "p1nkl0bst3rForbidden": "Third party API blocked your IP address", "p1nkl0bst3rTooManyRequests": "Too many requests to third party API. Try again later", "p1nkl0bst3rinternal": "Something happend on the p1nkl0bst3r side", "p1nkl0bst3rinternalWebVersion": "Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)", "replayAlreadySaved": "Replay already saved", "replayExpired": "Replay expired and not available anymore", - "replayRejected": "Third party API blocked your IP address.\nChange IP address or reach out to szy" + "replayRejected": "Third party API blocked your IP address" }, "countries(map)": { "": "Not selected", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 7afcbbd..6911667 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -46,6 +46,14 @@ "compare": "Сравнить", "tlLeaderboard": "Рейтинговая таблица", "noRecords": "Нет записей", + "noOldRecords": { + "zero": "Нет записей", + "one": "Всего один матч", + "two": "Всего $n матча", + "few": "Всего $n матча", + "many": "Всего $n матчей", + "other": "$n матчей" + }, "noRecord": "Нет рекорда", "botRecord": "Ботам нельзя ставить рекорды", "anonRecord": "Гостям нельзя ставить рекорды", @@ -70,6 +78,9 @@ "comparingWith": "Данные от ${newDate} в сравнении с данными от ${oldDate}", "top": "Топ", "topRank": "Топ Ранг", + "verdictGeneral": "$verdict среднего $rank ранга на $n", + "verdictBetter": "Лучше", + "verdictWorse": "Хуже", "gamesUntilRanked": "${left} матчей до получения рейтинга", "nerdStats": "Для задротов", "playersYouTrack": "Отслеживаемые игроки", @@ -93,8 +104,14 @@ "yourIDAlertTitle": "Ваш ник в TETR.IO", "yourIDText": "При запуске приложения оно будет получать статистику этого игрока.", "language": "Язык (Language)", + "customization": "Кастомизация", + "customizationDescription": "Здесь только один переключатель, в планах добавить больше", + "lbStats": "Показывать статистику, основанную на рейтинговой таблице", + "lbStatsDescription": "Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате", "aboutApp": "О приложении", "aboutAppText": "${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy", + "oskKagari": "\"Оск Кагари\" прикол", + "oskKagariDescription": "Если включено, вместо настоящего ранга оска будет рендерится :kagari:", "stateViewTitle": "Аккаунт ${nickname} ${date}", "statesViewTitle": "${number} состояний аккаунта ${nickname}", "matchesViewTitle": "Матчи аккаунта ${nickname}", @@ -248,7 +265,7 @@ }, "tspinsTotal": { "zero": "$n T-спинов всего", - "one": "всего $n T-спин", + "one": "Всего $n T-спин", "two": "$n T-спина всего", "few": "$n T-спина всего", "many": "$n T-спинов всего", @@ -271,21 +288,30 @@ "errors":{ "connection": "Проблема с подключением: ${code} ${message}", "noSuchUser": "Нет такого пользователя", + "noSuchUserSub": "Либо вы ошиблись при вводе, либо аккаунта больше не существует", + "discordNotAssigned": "К данному Discord ID не привязан аккаунт", + "discordNotAssignedSub": "Убедитесь в том, что вы вставили правильный ID", "history": "История данного игрока отсутствует", + "actionSuggestion": "Возможно, вы хотите", "p1nkl0bst3rTLmatches": "Старых матчей Тетра Лиги не было найдено", "clientException": "Нет соединения с интернетом", - "forbidden": "Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом", - "tooManyRequests": "Слишком много запросов. Попробуйте позже", + "forbidden": "Ваш IP адрес заблокирован", + "forbiddenSub": "Если у вас работает VPN или прокси, выключите его. Если это не помогло, свяжитесь с $nickname", + "tooManyRequests": "Слишком много запросов", + "tooManyRequestsSub": "Подождите немного и попробуйте снова", "internal": "Что-то случилось на стороне tetr.io", + "internalSub": "Скорее всего, osk уже в курсе об этом", "internalWebVersion": "Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)", - "oskwareBridge": "Что-то случилось с oskware_bridge. Дайте dan63047 знать", - "p1nkl0bst3rForbidden": "Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с p1nkl0bst3r-ом", + "internalWebVersionSub": "Если статус страница osk-а говорит, что всё ок - свяжитесь с dan63047", + "oskwareBridge": "Что-то случилось с oskware_bridge", + "oskwareBridgeSub": "Дайте dan63047 знать", + "p1nkl0bst3rForbidden": "Стороннее API заблокировало ваш IP адрес", "p1nkl0bst3rTooManyRequests": "Слишком много запросов к стороннему API. Попробуйте позже", "p1nkl0bst3rinternal": "Что-то случилось на стороне p1nkl0bst3r-а", "p1nkl0bst3rinternalWebVersion": "Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)", "replayAlreadySaved": "Повтор уже сохранён", "replayExpired": "Повтор истёк и больше недоступен", - "replayRejected": "Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с szy" + "replayRejected": "Стороннее API заблокировало ваш IP адрес" }, "countries(map)": { "": "Не выбрана", diff --git a/test/api_test.dart b/test/api_test.dart index 3075be0..52f3e87 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; From 3b16822c1f81805dbab5334dc9022ba46a642409 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 24 Mar 2024 19:38:06 +0300 Subject: [PATCH 14/14] 1.5.0 --- README.md | 1 + android/app/build.gradle | 18 +- lib/data_objects/freyhoe_test.dart | 8 +- .../tetrio_multiplayer_replay.dart | 724 +++++++-------- lib/gen/strings.g.dart | 84 +- lib/services/tetrio_crud.dart | 8 +- lib/utils/colors_functions.dart | 10 + lib/utils/numers_formats.dart | 3 +- lib/views/calc_view.dart | 119 ++- lib/views/compare_view.dart | 857 +++++++++--------- lib/views/main_view.dart | 110 ++- lib/views/ranks_averages_view.dart | 3 +- lib/views/tl_leaderboard_view.dart | 3 +- lib/views/tl_match_view.dart | 52 +- lib/widgets/gauget_num.dart | 3 +- lib/widgets/graphs.dart | 13 +- lib/widgets/lineclears_thingy.dart | 4 +- lib/widgets/stat_sell_num.dart | 3 +- lib/widgets/tl_thingy.dart | 13 +- lib/widgets/user_thingy.dart | 2 +- pubspec.lock | 244 ++--- pubspec.yaml | 2 +- res/i18n/strings.i18n.json | 25 +- res/i18n/strings_ru.i18n.json | 15 +- res/tetrio_badges/mts_1.png | Bin 0 -> 45779 bytes res/tetrio_badges/mts_2.png | Bin 0 -> 46077 bytes res/tetrio_badges/mts_3.png | Bin 0 -> 42267 bytes res/tetrio_badges/mts_participation.png | Bin 0 -> 49519 bytes res/tetrio_badges/stride_1.png | Bin 0 -> 29369 bytes res/tetrio_badges/stride_2.png | Bin 0 -> 30197 bytes res/tetrio_badges/stride_3.png | Bin 0 -> 28903 bytes res/tetrio_badges/stride_participation.png | Bin 0 -> 15449 bytes res/tetrio_badges/uoftflag_1.png | Bin 0 -> 43347 bytes res/tetrio_badges/uoftflag_2.png | Bin 0 -> 25582 bytes res/tetrio_badges/uoftflag_3.png | Bin 0 -> 28203 bytes 35 files changed, 1219 insertions(+), 1105 deletions(-) create mode 100644 lib/utils/colors_functions.dart create mode 100644 res/tetrio_badges/mts_1.png create mode 100644 res/tetrio_badges/mts_2.png create mode 100644 res/tetrio_badges/mts_3.png create mode 100644 res/tetrio_badges/mts_participation.png create mode 100644 res/tetrio_badges/stride_1.png create mode 100644 res/tetrio_badges/stride_2.png create mode 100644 res/tetrio_badges/stride_3.png create mode 100644 res/tetrio_badges/stride_participation.png create mode 100644 res/tetrio_badges/uoftflag_1.png create mode 100644 res/tetrio_badges/uoftflag_2.png create mode 100644 res/tetrio_badges/uoftflag_3.png diff --git a/README.md b/README.md index d7506ff..093c477 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ You can [download an app](https://github.com/dan63047/TetraStats/releases), or [ - Stats Calculator - Player history in charts - Tetra League matches history +- Time-weighted stats in Tetra League matches # Special thanks - **kerrmunism** — formulas diff --git a/android/app/build.gradle b/android/app/build.gradle index 3863f27..ebd2bb0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,6 +25,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +def keystoreProperties = new Properties() + def keystorePropertiesFile = rootProject.file('key.properties') + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + } + android { compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion @@ -53,11 +59,17 @@ android { versionName flutterVersionName } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } } diff --git a/lib/data_objects/freyhoe_test.dart b/lib/data_objects/freyhoe_test.dart index 5ca2a05..5275e86 100644 --- a/lib/data_objects/freyhoe_test.dart +++ b/lib/data_objects/freyhoe_test.dart @@ -1,12 +1,12 @@ //import 'dart:convert'; -import 'dart:io'; +//import 'dart:io'; //import 'package:path_provider/path_provider.dart'; //import 'tetrio_multiplayer_replay.dart'; /// That thing allows me to test my new staff i'm trying to implement -void main() async { +//void main() async { // List queue = List.from(tetrominoes); // TetrioRNG rng = TetrioRNG(0); // queue = rng.shuffleList(queue); @@ -21,5 +21,5 @@ void main() async { // print(replay.rawJson); //print(""); - exit(0); -} \ No newline at end of file +// exit(0); +//} \ No newline at end of file diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 03207c9..e453dd3 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -261,399 +261,399 @@ class ReplayData{ // can't belive i have to implement that difficult shit -List readEventList(Map json){ - List events = []; - int id = 0; - for (var event in json['data'][0]['replays'][0]['events']){ - int frame = event["frame"]; - EventType type = EventType.values.byName(event['type']); - switch (type) { - case EventType.start: - events.add(Event(id, frame, type)); - break; - case EventType.full: - events.add(EventFull(id, frame, type, DataFull.fromJson(event["data"]))); - break; - case EventType.targets: - // TODO - break; - case EventType.keydown: - events.add(EventKeyPress(id, frame, type, - Keypress( - KeyType.values.byName(event['data']['key']), - event['data']['subframe'], - false) - )); - break; - case EventType.keyup: - events.add(EventKeyPress(id, frame, type, - Keypress( - KeyType.values.byName(event['data']['key']), - event['data']['subframe'], - true) - )); - break; - case EventType.end: - // TODO: Handle this case. - case EventType.ige: - // TODO: Handle this case. - case EventType.exit: - // TODO: Handle this case. - } - id++; - } - return []; -} +// List readEventList(Map json){ +// List events = []; +// int id = 0; +// for (var event in json['data'][0]['replays'][0]['events']){ +// int frame = event["frame"]; +// EventType type = EventType.values.byName(event['type']); +// switch (type) { +// case EventType.start: +// events.add(Event(id, frame, type)); +// break; +// case EventType.full: +// events.add(EventFull(id, frame, type, DataFull.fromJson(event["data"]))); +// break; +// case EventType.targets: +// // TODO +// break; +// case EventType.keydown: +// events.add(EventKeyPress(id, frame, type, +// Keypress( +// KeyType.values.byName(event['data']['key']), +// event['data']['subframe'], +// false) +// )); +// break; +// case EventType.keyup: +// events.add(EventKeyPress(id, frame, type, +// Keypress( +// KeyType.values.byName(event['data']['key']), +// event['data']['subframe'], +// true) +// )); +// break; +// case EventType.end: +// // TODO: Handle this case. +// case EventType.ige: +// // TODO: Handle this case. +// case EventType.exit: +// // TODO: Handle this case. +// } +// id++; +// } +// return []; +// } -enum EventType -{ - start, - end, - full, - keydown, - keyup, - targets, - ige, - exit -} +// enum EventType +// { +// start, +// end, +// full, +// keydown, +// keyup, +// targets, +// ige, +// exit +// } -enum KeyType -{ - moveLeft, - moveRight, - softDrop, - rotateCCW, - rotateCW, - rotate180, - hardDrop, - hold, - chat, - exit, - retry -} +// enum KeyType +// { +// moveLeft, +// moveRight, +// softDrop, +// rotateCCW, +// rotateCW, +// rotate180, +// hardDrop, +// hold, +// chat, +// exit, +// retry +// } -class Event{ - int id; - int frame; - EventType type; - //dynamic data; +// class Event{ +// int id; +// int frame; +// EventType type; +// //dynamic data; - Event(this.id, this.frame, this.type); +// Event(this.id, this.frame, this.type); - @override - String toString(){ - return "E#$id f#$frame: $type"; - } -} +// @override +// String toString(){ +// return "E#$id f#$frame: $type"; +// } +// } -class Keypress{ - KeyType key; - double subframe; - bool released; +// class Keypress{ +// KeyType key; +// double subframe; +// bool released; - Keypress(this.key, this.subframe, this.released); -} +// Keypress(this.key, this.subframe, this.released); +// } -class EventKeyPress extends Event{ - Keypress data; +// class EventKeyPress extends Event{ +// Keypress data; - EventKeyPress(super.id, super.frame, super.type, this.data); -} +// EventKeyPress(super.id, super.frame, super.type, this.data); +// } -class IGE{ - int id; - int frame; - String type; - int amount; +// class IGE{ +// int id; +// int frame; +// String type; +// int amount; - IGE(this.id, this.frame, this.type, this.amount); -} +// IGE(this.id, this.frame, this.type, this.amount); +// } -class EventIGE extends Event{ - IGE data; +// class EventIGE extends Event{ +// IGE data; - EventIGE(super.id, super.frame, super.type, this.data); -} +// EventIGE(super.id, super.frame, super.type, this.data); +// } -class Hold -{ - String? piece; - bool locked; +// class Hold +// { +// String? piece; +// bool locked; - Hold(this.piece, this.locked); -} +// Hold(this.piece, this.locked); +// } -class DataFullOptions{ - int? version; - bool? seedRandom; - int? seed; - double? g; - int? stock; - int? gMargin; - double? gIncrease; - double? garbageMultiplier; - int? garbageMargin; - double? garbageIncrease; - int? garbageCap; - double? garbageCapIncrease; - int? garbageCapMax; - int? garbageHoleSize; - String? garbageBlocking; // TODO: enum - bool? hasGarbage; - int? locktime; - int? garbageSpeed; - int? forfeitTime; - int? are; - int? areLineclear; - bool? infiniteMovement; - int? lockresets; - bool? allow180; - bool? btbChaining; - bool? allclears; - bool? clutch; - bool? noLockout; - String? passthrough; - int? boardwidth; - int? boardheight; - Handling? handling; - int? boardbuffer; +// class DataFullOptions{ +// int? version; +// bool? seedRandom; +// int? seed; +// double? g; +// int? stock; +// int? gMargin; +// double? gIncrease; +// double? garbageMultiplier; +// int? garbageMargin; +// double? garbageIncrease; +// int? garbageCap; +// double? garbageCapIncrease; +// int? garbageCapMax; +// int? garbageHoleSize; +// String? garbageBlocking; // TODO: enum +// bool? hasGarbage; +// int? locktime; +// int? garbageSpeed; +// int? forfeitTime; +// int? are; +// int? areLineclear; +// bool? infiniteMovement; +// int? lockresets; +// bool? allow180; +// bool? btbChaining; +// bool? allclears; +// bool? clutch; +// bool? noLockout; +// String? passthrough; +// int? boardwidth; +// int? boardheight; +// Handling? handling; +// int? boardbuffer; - DataFullOptions.fromJson(Map json){ - version = json["version"]; - seedRandom = json["seed_random"]; - seed = json["seed"]; - g = json["g"]; - stock = json["stock"]; - gMargin = json["gmargin"]; - gIncrease = json["gincrease"]; - garbageMultiplier = json["garbagemultiplier"]; - garbageCapIncrease = json["garbagecapincrease"]; - garbageCapMax = json["garbagecapmax"]; - garbageHoleSize = json["garbageholesize"]; - garbageBlocking = json["garbageblocking"]; - hasGarbage = json["hasgarbage"]; - locktime = json["locktime"]; - garbageSpeed = json["garbagespeed"]; - forfeitTime = json["forfeit_time"]; - are = json["are"]; - areLineclear = json["lineclear_are"]; - infiniteMovement = json["infinitemovement"]; - lockresets = json["lockresets"]; - allow180 = json["allow180"]; - btbChaining = json["b2bchaining"]; - allclears = json["allclears"]; - clutch = json["clutch"]; - noLockout = json["nolockout"]; - passthrough = json["passthrough"]; - boardwidth = json["boardwidth"]; - boardheight = json["boardheight"]; - handling = Handling.fromJson(json["handling"]); - boardbuffer = json["boardbuffer"]; - } -} +// DataFullOptions.fromJson(Map json){ +// version = json["version"]; +// seedRandom = json["seed_random"]; +// seed = json["seed"]; +// g = json["g"]; +// stock = json["stock"]; +// gMargin = json["gmargin"]; +// gIncrease = json["gincrease"]; +// garbageMultiplier = json["garbagemultiplier"]; +// garbageCapIncrease = json["garbagecapincrease"]; +// garbageCapMax = json["garbagecapmax"]; +// garbageHoleSize = json["garbageholesize"]; +// garbageBlocking = json["garbageblocking"]; +// hasGarbage = json["hasgarbage"]; +// locktime = json["locktime"]; +// garbageSpeed = json["garbagespeed"]; +// forfeitTime = json["forfeit_time"]; +// are = json["are"]; +// areLineclear = json["lineclear_are"]; +// infiniteMovement = json["infinitemovement"]; +// lockresets = json["lockresets"]; +// allow180 = json["allow180"]; +// btbChaining = json["b2bchaining"]; +// allclears = json["allclears"]; +// clutch = json["clutch"]; +// noLockout = json["nolockout"]; +// passthrough = json["passthrough"]; +// boardwidth = json["boardwidth"]; +// boardheight = json["boardheight"]; +// handling = Handling.fromJson(json["handling"]); +// boardbuffer = json["boardbuffer"]; +// } +// } -class DataFullStats - { - double? seed; - int? lines; - int? levelLines; - int? levelLinesNeeded; - int? inputs; - int? holds; - int? score; - int? zenLevel; - int? zenProgress; - int? level; - int? combo; - int? currentComboPower; - int? topCombo; - int? btb; - int? topbtb; - int? tspins; - int? piecesPlaced; - Clears? clears; - Garbage? garbage; - int? kills; - Finesse? finesse; +// class DataFullStats +// { +// double? seed; +// int? lines; +// int? levelLines; +// int? levelLinesNeeded; +// int? inputs; +// int? holds; +// int? score; +// int? zenLevel; +// int? zenProgress; +// int? level; +// int? combo; +// int? currentComboPower; +// int? topCombo; +// int? btb; +// int? topbtb; +// int? tspins; +// int? piecesPlaced; +// Clears? clears; +// Garbage? garbage; +// int? kills; +// Finesse? finesse; - DataFullStats.fromJson(Map json){ - seed = json["seed"]; - lines = json["lines"]; - levelLines = json["level_lines"]; - levelLinesNeeded = json["level_lines_needed"]; - inputs = json["inputs"]; - holds = json["holds"]; - score = json["score"]; - zenLevel = json["zenlevel"]; - zenProgress = json["zenprogress"]; - level = json["level"]; - combo = json["combo"]; - currentComboPower = json["currentcombopower"]; - topCombo = json["topcombo"]; - btb = json["btb"]; - topbtb = json["topbtb"]; - tspins = json["tspins"]; - piecesPlaced = json["piecesplaced"]; - clears = Clears.fromJson(json["clears"]); - garbage = Garbage.fromJson(json["garbage"]); - kills = json["kills"]; - finesse = Finesse.fromJson(json["finesse"]); - } - } +// DataFullStats.fromJson(Map json){ +// seed = json["seed"]; +// lines = json["lines"]; +// levelLines = json["level_lines"]; +// levelLinesNeeded = json["level_lines_needed"]; +// inputs = json["inputs"]; +// holds = json["holds"]; +// score = json["score"]; +// zenLevel = json["zenlevel"]; +// zenProgress = json["zenprogress"]; +// level = json["level"]; +// combo = json["combo"]; +// currentComboPower = json["currentcombopower"]; +// topCombo = json["topcombo"]; +// btb = json["btb"]; +// topbtb = json["topbtb"]; +// tspins = json["tspins"]; +// piecesPlaced = json["piecesplaced"]; +// clears = Clears.fromJson(json["clears"]); +// garbage = Garbage.fromJson(json["garbage"]); +// kills = json["kills"]; +// finesse = Finesse.fromJson(json["finesse"]); +// } +// } -class DataFullGame - { - List>? board; - List? bag; - double? g; - bool? playing; - Hold? hold; - String? piece; - Handling? handling; +// class DataFullGame +// { +// List>? board; +// List? bag; +// double? g; +// bool? playing; +// Hold? hold; +// String? piece; +// Handling? handling; - DataFullGame.fromJson(Map json){ - board = json["board"]; - bag = json["bag"]; - hold = Hold(json["hold"]["piece"], json["hold"]["locked"]); - g = json["g"]; - handling = Handling.fromJson(json["handling"]); - } - } +// DataFullGame.fromJson(Map json){ +// board = json["board"]; +// bag = json["bag"]; +// hold = Hold(json["hold"]["piece"], json["hold"]["locked"]); +// g = json["g"]; +// handling = Handling.fromJson(json["handling"]); +// } +// } -class DataFull{ - bool? successful; - String? gameOverReason; - int? fire; - DataFullOptions? options; - DataFullStats? stats; - DataFullGame? game; +// class DataFull{ +// bool? successful; +// String? gameOverReason; +// int? fire; +// DataFullOptions? options; +// DataFullStats? stats; +// DataFullGame? game; - DataFull.fromJson(Map json){ - successful = json["successful"]; - gameOverReason = json["gameoverreason"]; - fire = json["fire"]; - options = DataFullOptions.fromJson(json["options"]); - stats = DataFullStats.fromJson(json["stats"]); - game = DataFullGame.fromJson(json["game"]); - } -} +// DataFull.fromJson(Map json){ +// successful = json["successful"]; +// gameOverReason = json["gameoverreason"]; +// fire = json["fire"]; +// options = DataFullOptions.fromJson(json["options"]); +// stats = DataFullStats.fromJson(json["stats"]); +// game = DataFullGame.fromJson(json["game"]); +// } +// } -class EventFull extends Event{ - DataFull data; +// class EventFull extends Event{ +// DataFull data; - EventFull(super.id, super.frame, super.type, this.data); -} +// EventFull(super.id, super.frame, super.type, this.data); +// } -class TetrioRNG{ - late double _t; +// class TetrioRNG{ +// late double _t; - TetrioRNG(int seed){ - _t = seed % 2147483647; - if (_t <= 0) _t += 2147483646; - } +// TetrioRNG(int seed){ +// _t = seed % 2147483647; +// if (_t <= 0) _t += 2147483646; +// } - int next(){ - _t = 16807 * _t % 2147483647; - return _t.toInt(); - } +// int next(){ +// _t = 16807 * _t % 2147483647; +// return _t.toInt(); +// } - double nextFloat(){ - return (next() - 1) / 2147483646; - } +// double nextFloat(){ +// return (next() - 1) / 2147483646; +// } - List shuffleList(List array){ - int length = array.length; - if (length == 0) return []; +// List shuffleList(List array){ +// int length = array.length; +// if (length == 0) return []; - for (; --length > 0;){ - int swapIndex = ((nextFloat()) * (length + 1)).toInt(); - Tetromino tmp = array[length]; - array[length] = array[swapIndex]; - array[swapIndex] = tmp; - } - return array; - } -} +// for (; --length > 0;){ +// int swapIndex = ((nextFloat()) * (length + 1)).toInt(); +// Tetromino tmp = array[length]; +// array[length] = array[swapIndex]; +// array[swapIndex] = tmp; +// } +// return array; +// } +// } -enum Tetromino{ - Z, - L, - O, - S, - I, - J, - T, - garbage, - empty -} +// enum Tetromino{ +// Z, +// L, +// O, +// S, +// I, +// J, +// T, +// garbage, +// empty +// } -List tetrominoes = [Tetromino.Z, Tetromino.L, Tetromino.O, Tetromino.S, Tetromino.I, Tetromino.J, Tetromino.T]; -List>> shapes = [ - [ // Z - [Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(2, 1)], - [Vector2(2, 0), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], - [Vector2(0, 1), Vector2(1, 1), Vector2(1, 2), Vector2(2, 2)], - [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(0, 2)] - ], - [ // L - [Vector2(2, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], - [Vector2(1, 0), Vector2(1, 1), Vector2(1, 2), Vector2(2, 2)], - [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(0, 2)], - [Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(1, 2)] - ], - [ // O - [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], - [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], - [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], - [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)] - ], - [ // S - [Vector2(1, 0), Vector2(2, 0), Vector2(0, 1), Vector2(1, 1)], - [Vector2(1, 0), Vector2(1, 1), Vector2(2, 1), Vector2(2, 2)], - [Vector2(1, 1), Vector2(2, 1), Vector2(0, 2), Vector2(1, 2)], - [Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 2)] - ], - [ // I - [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(3, 1)], - [Vector2(2, 0), Vector2(2, 1), Vector2(2, 2), Vector2(2, 3)], - [Vector2(0, 2), Vector2(1, 2), Vector2(2, 2), Vector2(3, 2)], - [Vector2(1, 0), Vector2(1, 1), Vector2(1, 2), Vector2(1, 3)] - ], - [ // J - [Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], - [Vector2(1, 0), Vector2(2, 0), Vector2(1, 1), Vector2(1, 2)], - [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(2, 2)], - [Vector2(1, 0), Vector2(1, 1), Vector2(0, 2), Vector2(1, 2)] - ], - [ // T - [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], - [Vector2(1, 0), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], - [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], - [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 2)] - ] -]; -List spawnPositionFixes = [Vector2(1, 1), Vector2(1, 1), Vector2(0, 1), Vector2(1, 1), Vector2(1, 1), Vector2(1, 1), Vector2(1, 1)]; +// List tetrominoes = [Tetromino.Z, Tetromino.L, Tetromino.O, Tetromino.S, Tetromino.I, Tetromino.J, Tetromino.T]; +// List>> shapes = [ +// [ // Z +// [Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(2, 1)], +// [Vector2(2, 0), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], +// [Vector2(0, 1), Vector2(1, 1), Vector2(1, 2), Vector2(2, 2)], +// [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(0, 2)] +// ], +// [ // L +// [Vector2(2, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], +// [Vector2(1, 0), Vector2(1, 1), Vector2(1, 2), Vector2(2, 2)], +// [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(0, 2)], +// [Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(1, 2)] +// ], +// [ // O +// [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], +// [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], +// [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)], +// [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)] +// ], +// [ // S +// [Vector2(1, 0), Vector2(2, 0), Vector2(0, 1), Vector2(1, 1)], +// [Vector2(1, 0), Vector2(1, 1), Vector2(2, 1), Vector2(2, 2)], +// [Vector2(1, 1), Vector2(2, 1), Vector2(0, 2), Vector2(1, 2)], +// [Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 2)] +// ], +// [ // I +// [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(3, 1)], +// [Vector2(2, 0), Vector2(2, 1), Vector2(2, 2), Vector2(2, 3)], +// [Vector2(0, 2), Vector2(1, 2), Vector2(2, 2), Vector2(3, 2)], +// [Vector2(1, 0), Vector2(1, 1), Vector2(1, 2), Vector2(1, 3)] +// ], +// [ // J +// [Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], +// [Vector2(1, 0), Vector2(2, 0), Vector2(1, 1), Vector2(1, 2)], +// [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(2, 2)], +// [Vector2(1, 0), Vector2(1, 1), Vector2(0, 2), Vector2(1, 2)] +// ], +// [ // T +// [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(2, 1)], +// [Vector2(1, 0), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], +// [Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(1, 2)], +// [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 2)] +// ] +// ]; +// List spawnPositionFixes = [Vector2(1, 1), Vector2(1, 1), Vector2(0, 1), Vector2(1, 1), Vector2(1, 1), Vector2(1, 1), Vector2(1, 1)]; -const Map garbage = { - "single": 0, - "double": 1, - "triple": 2, - "quad": 4, - "penta": 5, - "t-spin": 0, - "t-spin single": 2, - "t-spin double": 4, - "t-spin triple": 6, - "t-spin quad": 10, - "t-spin penta": 12, - "t-spin mini": 0, - "t-spin mini single": 0, - "t-spin mini double": 1, - "allclear": 10 -}; -int btbBonus = 1; -double btbLog = 0.8; -double comboBonus = 0.25; -int comboMinifier = 1; -double comboMinifierLog = 1.25; -List comboTable = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5]; +// const Map garbage = { +// "single": 0, +// "double": 1, +// "triple": 2, +// "quad": 4, +// "penta": 5, +// "t-spin": 0, +// "t-spin single": 2, +// "t-spin double": 4, +// "t-spin triple": 6, +// "t-spin quad": 10, +// "t-spin penta": 12, +// "t-spin mini": 0, +// "t-spin mini single": 0, +// "t-spin mini double": 1, +// "allclear": 10 +// }; +// int btbBonus = 1; +// double btbLog = 0.8; +// double comboBonus = 0.25; +// int comboMinifier = 1; +// double comboMinifierLog = 1.25; +// List comboTable = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5]; diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index ee03a73..1bed291 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1098 (549 per locale) +/// Strings: 1120 (560 per locale) /// -/// Built on 2024-03-20 at 22:41 UTC +/// Built on 2024-03-24 at 14:28 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -183,11 +183,11 @@ class Translations implements BaseTranslations { String get noRecords => 'No records'; String noOldRecords({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, zero: 'No records', - one: '${n} record', - two: '${n} records', - few: '${n} records', - many: '${n} records', - other: '${n} records', + one: 'Only ${n} record', + two: 'Only ${n} records', + few: 'Only ${n} records', + many: 'Only ${n} records', + other: 'Only ${n} records', ); String get noRecord => 'No record'; String get botRecord => 'Bots are not allowed to set records'; @@ -212,7 +212,7 @@ class Translations implements BaseTranslations { String supporter({required Object tier}) => 'Supporter tier ${tier}'; String comparingWith({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; String get top => 'Top'; - String get topRank => 'Top Rank'; + String get topRank => 'Top rank'; String verdictGeneral({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; String get verdictBetter => 'better'; String get verdictWorse => 'worse'; @@ -271,10 +271,14 @@ class Translations implements BaseTranslations { String get openReplay => 'Open replay in TETR.IO'; String replaySaved({required Object path}) => 'Replay saved to ${path}'; String get match => 'Match'; + String get timeWeightedmatch => 'Match (time-weighted)'; String roundNumber({required Object n}) => 'Round ${n}'; String get statsFor => 'Stats for'; + String get numberOfRounds => 'Number of rounds'; String get matchLength => 'Match Length'; String get roundLength => 'Round Length'; + String get matchStats => 'Match stats'; + String get timeWeightedmatchStats => 'Time-weighted match stats'; String get replayIssue => 'Can\'t process replay'; String get matchIsTooOld => 'Replay is not available'; String get winner => 'Winner'; @@ -311,6 +315,15 @@ class Translations implements BaseTranslations { many: '${n} players', other: '${n} players', ); + String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} games', + one: '${n} game', + two: '${n} games', + few: '${n} games', + many: '${n} games', + other: '${n} games', + ); + String gamesPlayed({required Object games}) => '${games} played'; String get chart => 'Chart'; String get entries => 'Entries'; String get minimums => 'Minimums'; @@ -853,7 +866,7 @@ class _StringsRu implements Translations { @override String get assignedManualy => 'Этот значок был присвоен вручную администрацией TETR.IO'; @override String comparingWith({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; @override String get top => 'Топ'; - @override String get topRank => 'Топ Ранг'; + @override String get topRank => 'Топ ранг'; @override String verdictGeneral({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; @override String get verdictBetter => 'Лучше'; @override String get verdictWorse => 'Хуже'; @@ -912,10 +925,14 @@ class _StringsRu implements Translations { @override String get openReplay => 'Открыть повтор в TETR.IO'; @override String replaySaved({required Object path}) => 'Повтор сохранён по пути ${path}'; @override String get match => 'Матч'; + @override String get timeWeightedmatch => 'Матч (взвешенная по времени)'; @override String roundNumber({required Object n}) => 'Раунд ${n}'; @override String get statsFor => 'Статистика за'; + @override String get numberOfRounds => 'Количество раундов'; @override String get matchLength => 'Продолжительность матча'; @override String get roundLength => 'Продолжительность раунда'; + @override String get matchStats => 'Статистика матча'; + @override String get timeWeightedmatchStats => 'Взвешенная по времени cтатистика матча'; @override String get replayIssue => 'Ошибка обработки повтора'; @override String get matchIsTooOld => 'Информация о повторе недоступна'; @override String get winner => 'Победитель'; @@ -952,6 +969,15 @@ class _StringsRu implements Translations { many: '${n} игроков', other: '${n} игроков', ); + @override String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} игр', + one: '${n} игра', + two: '${n} игры', + few: '${n} игры', + many: '${n} игр', + other: '${n} игр', + ); + @override String gamesPlayed({required Object games}) => '${games} сыграно'; @override String get chart => 'График'; @override String get entries => 'Список'; @override String get minimums => 'Минимумы'; @@ -1457,11 +1483,11 @@ extension on Translations { case 'noRecords': return 'No records'; case 'noOldRecords': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, zero: 'No records', - one: '${n} record', - two: '${n} records', - few: '${n} records', - many: '${n} records', - other: '${n} records', + one: 'Only ${n} record', + two: 'Only ${n} records', + few: 'Only ${n} records', + many: 'Only ${n} records', + other: 'Only ${n} records', ); case 'noRecord': return 'No record'; case 'botRecord': return 'Bots are not allowed to set records'; @@ -1486,7 +1512,7 @@ extension on Translations { case 'supporter': return ({required Object tier}) => 'Supporter tier ${tier}'; case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; case 'top': return 'Top'; - case 'topRank': return 'Top Rank'; + case 'topRank': return 'Top rank'; case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; case 'verdictBetter': return 'better'; case 'verdictWorse': return 'worse'; @@ -1545,10 +1571,14 @@ extension on Translations { case 'openReplay': return 'Open replay in TETR.IO'; case 'replaySaved': return ({required Object path}) => 'Replay saved to ${path}'; case 'match': return 'Match'; + case 'timeWeightedmatch': return 'Match (time-weighted)'; case 'roundNumber': return ({required Object n}) => 'Round ${n}'; case 'statsFor': return 'Stats for'; + case 'numberOfRounds': return 'Number of rounds'; case 'matchLength': return 'Match Length'; case 'roundLength': return 'Round Length'; + case 'matchStats': return 'Match stats'; + case 'timeWeightedmatchStats': return 'Time-weighted match stats'; case 'replayIssue': return 'Can\'t process replay'; case 'matchIsTooOld': return 'Replay is not available'; case 'winner': return 'Winner'; @@ -1585,6 +1615,15 @@ extension on Translations { many: '${n} players', other: '${n} players', ); + case 'games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} games', + one: '${n} game', + two: '${n} games', + few: '${n} games', + many: '${n} games', + other: '${n} games', + ); + case 'gamesPlayed': return ({required Object games}) => '${games} played'; case 'chart': return 'Chart'; case 'entries': return 'Entries'; case 'minimums': return 'Minimums'; @@ -2053,7 +2092,7 @@ extension on _StringsRu { case 'assignedManualy': return 'Этот значок был присвоен вручную администрацией TETR.IO'; case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; case 'top': return 'Топ'; - case 'topRank': return 'Топ Ранг'; + case 'topRank': return 'Топ ранг'; case 'verdictGeneral': return ({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; case 'verdictBetter': return 'Лучше'; case 'verdictWorse': return 'Хуже'; @@ -2112,10 +2151,14 @@ extension on _StringsRu { case 'openReplay': return 'Открыть повтор в TETR.IO'; case 'replaySaved': return ({required Object path}) => 'Повтор сохранён по пути ${path}'; case 'match': return 'Матч'; + case 'timeWeightedmatch': return 'Матч (взвешенная по времени)'; case 'roundNumber': return ({required Object n}) => 'Раунд ${n}'; case 'statsFor': return 'Статистика за'; + case 'numberOfRounds': return 'Количество раундов'; case 'matchLength': return 'Продолжительность матча'; case 'roundLength': return 'Продолжительность раунда'; + case 'matchStats': return 'Статистика матча'; + case 'timeWeightedmatchStats': return 'Взвешенная по времени cтатистика матча'; case 'replayIssue': return 'Ошибка обработки повтора'; case 'matchIsTooOld': return 'Информация о повторе недоступна'; case 'winner': return 'Победитель'; @@ -2152,6 +2195,15 @@ extension on _StringsRu { many: '${n} игроков', other: '${n} игроков', ); + case 'games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} игр', + one: '${n} игра', + two: '${n} игры', + few: '${n} игры', + many: '${n} игр', + other: '${n} игр', + ); + case 'gamesPlayed': return ({required Object games}) => '${games} сыграно'; case 'chart': return 'График'; case 'entries': return 'Список'; case 'minimums': return 'Минимумы'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 947ae12..a78532f 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -415,12 +415,12 @@ class TetrioService extends DB { // parsing data into TetraLeagueAlphaRecord objects for (var entry in csv){ TetraLeagueAlphaRecord match = TetraLeagueAlphaRecord( - replayId: entry[0], - ownId: entry[0], // i gonna disting p1nkl0bst3r entries with it + replayId: entry[0].toString(), + ownId: entry[0].toString(), // i gonna disting p1nkl0bst3r entries with it timestamp: DateTime.parse(entry[1]), endContext: [ EndContextMulti( - userId: entry[2], + userId: entry[2].toString(), username: entry[3].toString(), naturalOrder: 0, inputs: -1, @@ -437,7 +437,7 @@ class TetrioService extends DB { success: true ), EndContextMulti( - userId: entry[8], + userId: entry[8].toString(), username: entry[9].toString(), naturalOrder: 1, inputs: -1, diff --git a/lib/utils/colors_functions.dart b/lib/utils/colors_functions.dart new file mode 100644 index 0000000..70277d9 --- /dev/null +++ b/lib/utils/colors_functions.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +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; +} \ No newline at end of file diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index 3f626fc..412820d 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -7,4 +7,5 @@ final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..minimumFractionDigits = 0; -final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode); \ No newline at end of file +final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode); +final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2; \ No newline at end of file diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart index 7762c13..5688d26 100644 --- a/lib/views/calc_view.dart +++ b/lib/views/calc_view.dart @@ -66,76 +66,65 @@ class CalcState extends State { title: Text(t.statsCalc), ), backgroundColor: Colors.black, - body: SafeArea( + body: SingleChildScrollView( child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 768), - child: NestedScrollView( - controller: _scrollController, - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(14, 16, 16, 32), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: TextField( - onSubmitted: (value) => calc(), - controller: apmController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("APM"), alignLabelWithHint: true), - ), - )), - Expanded( - child: TextField( - onSubmitted: (value) => calc(), - controller: ppsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("PPS"), alignLabelWithHint: true), - )), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 12), - child: TextField( - onSubmitted: (value) => calc(), - controller: vsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("VS"), alignLabelWithHint: true), - ), - )), - TextButton( - onPressed: () => calc(), - child: Text(t.calc), - ), - ], - ), + child: Column(children: [ + Padding( + padding: const EdgeInsets.fromLTRB(14, 16, 16, 32), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: TextField( + onSubmitted: (value) => calc(), + controller: apmController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(label: Text("APM"), alignLabelWithHint: true), ), + )), + Expanded( + child: TextField( + onSubmitted: (value) => calc(), + controller: ppsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(label: Text("PPS"), alignLabelWithHint: true), + )), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: TextField( + onSubmitted: (value) => calc(), + controller: vsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(label: Text("VS"), alignLabelWithHint: true), + ), + )), + TextButton( + onPressed: () => calc(), + child: Text(t.calc), ), - const SliverToBoxAdapter( - child: Divider(), - ) - ]; - }, - body: nerdStats == null - ? Text(t.calcViewNoValues) - : ListView( - children: [ - _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.vsapm, label: "VS/APM", fractionDigits: 3), - _ListEntry(value: nerdStats!.dss, label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.dsp, label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.appdsp, label: "APP + DS/P", fractionDigits: 3), - _ListEntry(value: nerdStats!.cheese, label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.gbe, label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.nyaapp, label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.area, label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: estTr!.esttr, label: t.statCellNum.estOfTR, fractionDigits: 3), - Graphs(apm!, pps!, vs!, nerdStats!, playstyle!), - ], - )), + ], + ), + ), + Divider(), + if (nerdStats == null) Text(t.calcViewNoValues) + else Column(children: [ + _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.vsapm, label: "VS/APM", fractionDigits: 3), + _ListEntry(value: nerdStats!.dss, label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.dsp, label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.appdsp, label: "APP + DS/P", fractionDigits: 3), + _ListEntry(value: nerdStats!.cheese, label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.gbe, label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.nyaapp, label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: nerdStats!.area, label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), + _ListEntry(value: estTr!.esttr, label: t.statCellNum.estOfTR, fractionDigits: 3), + Graphs(apm!, pps!, vs!, nerdStats!, playstyle!) + ],) + ],), ), ), ), diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 8ac89d9..44d0156 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -255,452 +255,433 @@ class CompareState extends State { return Scaffold( appBar: AppBar(title: Text("$titleGreenSide ${t.vs} $titleRedSide")), backgroundColor: Colors.black, - body: SafeArea( + body: SingleChildScrollView( + controller: _scrollController, + physics: AlwaysScrollableScrollPhysics(), child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 768), - 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: PlayerSelector( - data: theGreenSide, - mode: greenSideMode, - fetch: fetchGreenSide, - change: changeGreenSide, - updateState: _justUpdate, - ), - ), - ), + child: Column(children: [ + 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: PlayerSelector( + data: theGreenSide, + mode: greenSideMode, + fetch: fetchGreenSide, + change: changeGreenSide, + updateState: _justUpdate, ), - 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: PlayerSelector( - data: theRedSide, - mode: redSideMode, - fetch: fetchRedSide, - change: changeRedSide, - updateState: _justUpdate, - ), - ), - ), - ), - ], + ), ), ), - ), - const SliverToBoxAdapter( - child: Divider(), - ) - ]; - }, - body: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 768), - child: ListView( - children: !listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])? [ - if (theGreenSide[0] != null && - theRedSide[0] != null && - theGreenSide[0]!.role != "banned" && - theRedSide[0]!.role != "banned") - Column( - children: [ - CompareRegTimeThingy( - greenSide: theGreenSide[0].registrationTime, - redSide: theRedSide[0].registrationTime, - label: t.registred), - CompareThingy( - label: t.statCellNum.level, - greenSide: theGreenSide[0].level, - redSide: theRedSide[0].level, - higherIsBetter: true, - fractionDigits: 2, - ), - if (!theGreenSide[0].gameTime.isNegative && - !theRedSide[0].gameTime.isNegative) - CompareThingy( - greenSide: theGreenSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - redSide: theRedSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - label: t.statCellNum.hoursPlayed.replaceAll(RegExp(r'\n'), " "), - higherIsBetter: true, - fractionDigits: 2, - ), - if (theGreenSide[0].gamesPlayed >= 0 && - theRedSide[0].gamesPlayed >= 0) - CompareThingy( - label: t.statCellNum.onlineGames.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesPlayed, - redSide: theRedSide[0].gamesPlayed, - higherIsBetter: true, - ), - if (theGreenSide[0].gamesWon >= 0 && - theRedSide[0].gamesWon >= 0) - CompareThingy( - label: t.statCellNum.gamesWon.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesWon, - redSide: theRedSide[0].gamesWon, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.friends, - greenSide: theGreenSide[0].friendCount, - redSide: theRedSide[0].friendCount, - higherIsBetter: true, - ), - const Divider(), - ], - ), - if (theGreenSide[0] != null && - theRedSide[0] != null && - (theGreenSide[0]!.role == "banned" || - theRedSide[0]!.role == "banned")) - CompareBoolThingy( - greenSide: theGreenSide[0].role == "banned", - redSide: theRedSide[0].role == "banned", - label: t.normalBanned, - trueIsBetter: false), - (theGreenSide[2].gamesPlayed > 0 || greenSideMode == Mode.stats) && - (theRedSide[2].gamesPlayed > 0 || redSideMode == Mode.stats) - ? Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.tetraLeague, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "TR", - greenSide: theGreenSide[2].rating, - redSide: theRedSide[2].rating, - fractionDigits: 2, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].gamesPlayed, - redSide: theRedSide[2].gamesPlayed, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].gamesWon, - redSide: theRedSide[2].gamesWon, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "WR %", - greenSide: - theGreenSide[2].winrate * 100, - redSide: theRedSide[2].winrate * 100, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "Glicko", - greenSide: theGreenSide[2].glicko!, - redSide: theRedSide[2].glicko!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "RD", - greenSide: theGreenSide[2].rd!, - redSide: theRedSide[2].rd!, - fractionDigits: 3, - higherIsBetter: false, - ), - if (theGreenSide[2].standing > 0 && - theRedSide[2].standing > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpShort, - greenSide: theGreenSide[2].standing, - redSide: theRedSide[2].standing, - higherIsBetter: false, - ), - if (theGreenSide[2].standingLocal > 0 && - theRedSide[2].standingLocal > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpcShort, - greenSide: - theGreenSide[2].standingLocal, - redSide: theRedSide[2].standingLocal, - higherIsBetter: false, - ), - if (theGreenSide[2].apm != null && - theRedSide[2].apm != null) - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].apm!, - redSide: theRedSide[2].apm!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].pps != null && - theRedSide[2].pps != null) - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].pps!, - redSide: theRedSide[2].pps!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].vs != null && - theRedSide[2].vs != null) - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].vs!, - redSide: theRedSide[2].vs!, - fractionDigits: 2, - higherIsBetter: true, - ), - ], - ) - : CompareBoolThingy( - greenSide: theGreenSide[2].gamesPlayed > 0, - redSide: theRedSide[2].gamesPlayed > 0, - label: t.playedTL, - trueIsBetter: false), - const Divider(), - if (theGreenSide[2].nerdStats != null && - theRedSide[2].nerdStats != null) - Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.nerdStats, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].nerdStats!.app, - redSide: theRedSide[2].nerdStats!.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].nerdStats!.vsapm, - redSide: theRedSide[2].nerdStats!.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].nerdStats!.dss, - redSide: theRedSide[2].nerdStats!.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].nerdStats!.dsp, - redSide: theRedSide[2].nerdStats!.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].nerdStats!.appdsp, - redSide: theRedSide[2].nerdStats!.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].nerdStats!.cheese, - redSide: theRedSide[2].nerdStats!.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].nerdStats!.gbe, - redSide: theRedSide[2].nerdStats!.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].nerdStats!.nyaapp, - redSide: theRedSide[2].nerdStats!.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].nerdStats!.area, - redSide: theRedSide[2].nerdStats!.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: theGreenSide[2].estTr!.esttr, - redSide: theRedSide[2].estTr!.esttr, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].gamesPlayed > 9 && - theGreenSide[2].gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.accOfEstShort, - greenSide: theGreenSide[2].esttracc!, - redSide: theRedSide[2].esttracc!, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].playstyle!.opener, - redSide: theRedSide[2].playstyle!.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].playstyle!.plonk, - redSide: theRedSide[2].playstyle!.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].playstyle!.stride, - redSide: theRedSide[2].playstyle!.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].playstyle!.infds, - redSide: theRedSide[2].playstyle!.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].apm!, theGreenSide[2].pps!, theGreenSide[2].vs!, theGreenSide[2].nerdStats!, theGreenSide[2].playstyle!, theRedSide[2].apm!, theRedSide[2].pps!, theRedSide[2].vs!, theRedSide[2].nerdStats!, theRedSide[2].playstyle!), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.winChance, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (greenSideMode != Mode.stats && redSideMode != Mode.stats && - theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9) - CompareThingy( - label: t.byGlicko, - greenSide: getWinrateByTR( - theGreenSide[2].glicko!, - theGreenSide[2].rd!, - theRedSide[2].glicko!, - theRedSide[2].rd!) * - 100, - redSide: getWinrateByTR( - theRedSide[2].glicko!, - theRedSide[2].rd!, - theGreenSide[2].glicko!, - theGreenSide[2].rd!) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - CompareThingy( - label: t.byEstTR, - greenSide: getWinrateByTR( - theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd ?? noTrRd, - theRedSide[2].estTr!.estglicko, - theRedSide[2].rd ?? noTrRd) * - 100, - redSide: getWinrateByTR( - theRedSide[2].estTr!.estglicko, - theRedSide[2].rd ?? noTrRd, - theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd ?? noTrRd) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - ], - ) - ] : [Padding( - padding: const EdgeInsets.all(8.0), - child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), - )], // This is so fucked up holy shit + 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: PlayerSelector( + data: theRedSide, + mode: redSideMode, + fetch: fetchRedSide, + change: changeRedSide, + updateState: _justUpdate, + ), ), + ), + ), + ], ), - ) + ), + Divider(), + if (!listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])) Column( + children: [ + if (theGreenSide[0] != null && theRedSide[0] != null && theGreenSide[0]!.role != "banned" && theRedSide[0]!.role != "banned") + Column( + children: [ + CompareRegTimeThingy( + greenSide: theGreenSide[0].registrationTime, + redSide: theRedSide[0].registrationTime, + label: t.registred), + CompareThingy( + label: t.statCellNum.level, + greenSide: theGreenSide[0].level, + redSide: theRedSide[0].level, + higherIsBetter: true, + fractionDigits: 2, + ), + if (!theGreenSide[0].gameTime.isNegative && !theRedSide[0].gameTime.isNegative) + CompareThingy( + greenSide: theGreenSide[0].gameTime.inMicroseconds / + 1000000 / + 60 / + 60, + redSide: theRedSide[0].gameTime.inMicroseconds / + 1000000 / + 60 / + 60, + label: t.statCellNum.hoursPlayed.replaceAll(RegExp(r'\n'), " "), + higherIsBetter: true, + fractionDigits: 2, + ), + if (theGreenSide[0].gamesPlayed >= 0 && theRedSide[0].gamesPlayed >= 0) + CompareThingy( + label: t.statCellNum.onlineGames.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[0].gamesPlayed, + redSide: theRedSide[0].gamesPlayed, + higherIsBetter: true, + ), + if (theGreenSide[0].gamesWon >= 0 && theRedSide[0].gamesWon >= 0) + CompareThingy( + label: t.statCellNum.gamesWon.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[0].gamesWon, + redSide: theRedSide[0].gamesWon, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.friends, + greenSide: theGreenSide[0].friendCount, + redSide: theRedSide[0].friendCount, + higherIsBetter: true, + ), + const Divider(), + ], + ), + if (theGreenSide[0] != null && theRedSide[0] != null && (theGreenSide[0]!.role == "banned" || theRedSide[0]!.role == "banned")) + CompareBoolThingy( + greenSide: theGreenSide[0].role == "banned", + redSide: theRedSide[0].role == "banned", + label: t.normalBanned, + trueIsBetter: false + ), + (theGreenSide[2].gamesPlayed > 0 || greenSideMode == Mode.stats) && (theRedSide[2].gamesPlayed > 0 || redSideMode == Mode.stats) + ? Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.tetraLeague, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + if (theGreenSide[2].gamesPlayed > 9 && + theRedSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "TR", + greenSide: theGreenSide[2].rating, + redSide: theRedSide[2].rating, + fractionDigits: 2, + higherIsBetter: true, + ), + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].gamesPlayed, + redSide: theRedSide[2].gamesPlayed, + higherIsBetter: true, + ), + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].gamesWon, + redSide: theRedSide[2].gamesWon, + higherIsBetter: true, + ), + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "WR %", + greenSide: + theGreenSide[2].winrate * 100, + redSide: theRedSide[2].winrate * 100, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].gamesPlayed > 9 && + theRedSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "Glicko", + greenSide: theGreenSide[2].glicko!, + redSide: theRedSide[2].glicko!, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].gamesPlayed > 9 && + theRedSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: "RD", + greenSide: theGreenSide[2].rd!, + redSide: theRedSide[2].rd!, + fractionDigits: 3, + higherIsBetter: false, + ), + if (theGreenSide[2].standing > 0 && + theRedSide[2].standing > 0 && + greenSideMode == Mode.player && + redSideMode == Mode.player) + CompareThingy( + label: t.statCellNum.lbpShort, + greenSide: theGreenSide[2].standing, + redSide: theRedSide[2].standing, + higherIsBetter: false, + ), + if (theGreenSide[2].standingLocal > 0 && + theRedSide[2].standingLocal > 0 && + greenSideMode == Mode.player && + redSideMode == Mode.player) + CompareThingy( + label: t.statCellNum.lbpcShort, + greenSide: + theGreenSide[2].standingLocal, + redSide: theRedSide[2].standingLocal, + higherIsBetter: false, + ), + if (theGreenSide[2].apm != null && + theRedSide[2].apm != null) + CompareThingy( + label: "APM", + greenSide: theGreenSide[2].apm!, + redSide: theRedSide[2].apm!, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].pps != null && + theRedSide[2].pps != null) + CompareThingy( + label: "PPS", + greenSide: theGreenSide[2].pps!, + redSide: theRedSide[2].pps!, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].vs != null && + theRedSide[2].vs != null) + CompareThingy( + label: "VS", + greenSide: theGreenSide[2].vs!, + redSide: theRedSide[2].vs!, + fractionDigits: 2, + higherIsBetter: true, + ), + ], + ) + : CompareBoolThingy( + greenSide: theGreenSide[2].gamesPlayed > 0, + redSide: theRedSide[2].gamesPlayed > 0, + label: t.playedTL, + trueIsBetter: false), + const Divider(), + if (theGreenSide[2].nerdStats != null && + theRedSide[2].nerdStats != null) + Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.nerdStats, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "APP", + greenSide: theGreenSide[2].nerdStats!.app, + redSide: theRedSide[2].nerdStats!.app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: theGreenSide[2].nerdStats!.vsapm, + redSide: theRedSide[2].nerdStats!.vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: theGreenSide[2].nerdStats!.dss, + redSide: theRedSide[2].nerdStats!.dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: theGreenSide[2].nerdStats!.dsp, + redSide: theRedSide[2].nerdStats!.dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: + theGreenSide[2].nerdStats!.appdsp, + redSide: theRedSide[2].nerdStats!.appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), + greenSide: + theGreenSide[2].nerdStats!.cheese, + redSide: theRedSide[2].nerdStats!.cheese, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Gb Eff.", + greenSide: theGreenSide[2].nerdStats!.gbe, + redSide: theRedSide[2].nerdStats!.gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "wAPP", + greenSide: + theGreenSide[2].nerdStats!.nyaapp, + redSide: theRedSide[2].nerdStats!.nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: theGreenSide[2].nerdStats!.area, + redSide: theRedSide[2].nerdStats!.area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.estOfTRShort, + greenSide: theGreenSide[2].estTr!.esttr, + redSide: theRedSide[2].estTr!.esttr, + fractionDigits: 2, + higherIsBetter: true, + ), + if (theGreenSide[2].gamesPlayed > 9 && + theGreenSide[2].gamesPlayed > 9 && + greenSideMode != Mode.stats && + redSideMode != Mode.stats) + CompareThingy( + label: t.statCellNum.accOfEstShort, + greenSide: theGreenSide[2].esttracc!, + redSide: theRedSide[2].esttracc!, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Opener", + greenSide: theGreenSide[2].playstyle!.opener, + redSide: theRedSide[2].playstyle!.opener, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Plonk", + greenSide: theGreenSide[2].playstyle!.plonk, + redSide: theRedSide[2].playstyle!.plonk, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Stride", + greenSide: theGreenSide[2].playstyle!.stride, + redSide: theRedSide[2].playstyle!.stride, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Inf. DS", + greenSide: theGreenSide[2].playstyle!.infds, + redSide: theRedSide[2].playstyle!.infds, + fractionDigits: 3, + higherIsBetter: true, + ), + VsGraphs(theGreenSide[2].apm!, theGreenSide[2].pps!, theGreenSide[2].vs!, theGreenSide[2].nerdStats!, theGreenSide[2].playstyle!, theRedSide[2].apm!, theRedSide[2].pps!, theRedSide[2].vs!, theRedSide[2].nerdStats!, theRedSide[2].playstyle!), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.winChance, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + if (greenSideMode != Mode.stats && redSideMode != Mode.stats && + theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9) + CompareThingy( + label: t.byGlicko, + greenSide: getWinrateByTR( + theGreenSide[2].glicko!, + theGreenSide[2].rd!, + theRedSide[2].glicko!, + theRedSide[2].rd!) * + 100, + redSide: getWinrateByTR( + theRedSide[2].glicko!, + theRedSide[2].rd!, + theGreenSide[2].glicko!, + theGreenSide[2].rd!) * + 100, + fractionDigits: 2, + higherIsBetter: true, + postfix: "%", + ), + CompareThingy( + label: t.byEstTR, + greenSide: getWinrateByTR( + theGreenSide[2].estTr!.estglicko, + theGreenSide[2].rd ?? noTrRd, + theRedSide[2].estTr!.estglicko, + theRedSide[2].rd ?? noTrRd) * + 100, + redSide: getWinrateByTR( + theRedSide[2].estTr!.estglicko, + theRedSide[2].rd ?? noTrRd, + theGreenSide[2].estTr!.estglicko, + theGreenSide[2].rd ?? noTrRd) * + 100, + fractionDigits: 2, + higherIsBetter: true, + postfix: "%", + ), + ], + ) + ], + ) + else Padding( + padding: const EdgeInsets.all(8.0), + child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), + ) + ], ), ), ), diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index fb890d6..c7e46e0 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -34,7 +34,7 @@ import 'package:go_router/go_router.dart'; final TetrioService teto = TetrioService(); // thing, that manadge our local DB int _chartsIndex = 0; -bool _showHistoryAsTable = false; +bool _gamesPlayedInsteadOfDateAndTime = 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); @@ -85,6 +85,7 @@ class _MainState extends State with TickerProviderStateMixin { String _titleNickname = "dan63047"; /// Each dropdown menu item contains list of dots for the graph var chartsData = >>[]; + var chartsDataGamesPlayed = >>[]; //var tableData = []; final bodyGlobalKey = GlobalKey(); bool _showSearchBar = false; @@ -283,6 +284,29 @@ class _MainState extends State with TickerProviderStateMixin { DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.infds)], child: const Text("Inf. DS")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")), ]; + chartsDataGamesPlayed = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.rating)], child: Text(t.statCellNum.tr)), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.glicko!)], child: const Text("Glicko")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.rd!)], child: const Text("Rating Deviation")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) FlSpot(tl.gamesPlayed.toDouble(), tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) FlSpot(tl.gamesPlayed.toDouble(), tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) FlSpot(tl.gamesPlayed.toDouble(), tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.vsapm)], child: const Text("VS/APM")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) FlSpot(tl.gamesPlayed.toDouble(), tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) FlSpot(tl.gamesPlayed.toDouble(), tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.opener)], child: const Text("Opener")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.plonk)], child: const Text("Plonk")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.infds)], child: const Text("Inf. DS")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")), + ]; }else{ compareWith = null; chartsData = []; @@ -393,8 +417,7 @@ class _MainState extends State with TickerProviderStateMixin { return notification.depth == 0; }, child: NestedScrollView( - scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - physics: const AlwaysScrollableScrollPhysics(), + scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false, physics: const AlwaysScrollableScrollPhysics()), controller: _scrollController, headerSliverBuilder: (context, value) { return [ @@ -448,10 +471,10 @@ class _MainState extends State with TickerProviderStateMixin { ), SizedBox( width: 450, - child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched) + child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true,) ), ],), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) ] : [ @@ -466,7 +489,7 @@ class _MainState extends State with TickerProviderStateMixin { lbPositions: meAmongEveryone ), _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), + _History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, 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), _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) @@ -664,10 +687,11 @@ class _TLRecords extends StatelessWidget { final List data; final bool wasActiveInTL; final bool oldMathcesHere; + final bool separateScrollController; /// Widget, that displays Tetra League records. /// Accepts list of TL records ([data]) and [userID] of player from the view - const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere}); + const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere, this.separateScrollController = false}); @override Widget build(BuildContext context) { @@ -685,7 +709,7 @@ class _TLRecords extends StatelessWidget { int length = data.length; return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), - controller: ScrollController(), + controller: separateScrollController ? ScrollController() : null, itemCount: oldMathcesHere ? length : length + 1, itemBuilder: (BuildContext context, int index) { if (index == length) { @@ -708,7 +732,6 @@ class _TLRecords extends StatelessWidget { ) ), child: ListTile( - // tileColor: data[index].endContext.firstWhere((element) => element.userId == userID).success ? Colors.green[900] : Colors.red[900], leading: Text("${data[index].endContext.firstWhere((element) => element.userId == userID).points} : ${data[index].endContext.firstWhere((element) => element.userId != userID).points}", style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), title: Text("vs. ${data[index].endContext.firstWhere((element) => element.userId != userID).username}"), @@ -730,6 +753,7 @@ class _TLRecords extends StatelessWidget { class _History extends StatelessWidget{ final List>> chartsData; + final List>> chartsDataGamesPlayed; final String userID; final Function update; final Function changePlayer; @@ -737,7 +761,7 @@ class _History extends StatelessWidget{ /// Widget, that can show history of some stat of the player on the graph. /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); + const _History({required this.chartsData, required this.chartsDataGamesPlayed, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); @override Widget build(BuildContext context) { @@ -763,15 +787,25 @@ class _History extends StatelessWidget{ Wrap( spacing: 20, children: [ - // DropdownButton( - // items: [DropdownMenuItem(child: Text("Chart"), value: false), DropdownMenuItem(child: Text("Table"), value: true)], - // value: _showHistoryAsTable, - // onChanged: (value) { - // _showHistoryAsTable = value!; - // update(); - // } - // ), - DropdownButton( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: const [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], + value: _gamesPlayedInsteadOfDateAndTime, + onChanged: (value) { + _gamesPlayedInsteadOfDateAndTime = value!; + update(); + } + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( items: chartsData, value: chartsData[_chartsIndex].value, onChanged: (value) { @@ -779,10 +813,12 @@ class _History extends StatelessWidget{ update(); } ), + ], + ), ], ), - if(chartsData[_chartsIndex].value!.length > 1 && !_showHistoryAsTable) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),) - else if (chartsData[_chartsIndex].value!.length <= 1 && !_showHistoryAsTable) Center(child: Column( + if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: _gamesPlayedInsteadOfDateAndTime ? chartsDataGamesPlayed[_chartsIndex].value! : chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact()) + else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), @@ -790,10 +826,6 @@ class _History extends StatelessWidget{ if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) ], )) - // else if (_showHistoryAsTable) Padding( - // padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - // child: _HistoryTableThingy(tableData), - // ) ], ), ), @@ -807,11 +839,12 @@ class _HistoryChartThigy extends StatefulWidget{ final bool bigScreen; final double leftSpace; final NumberFormat yFormat; + final NumberFormat? xFormat; /// Implements graph for the _History widget. Requires [data] which is a list of dots for the graph. [yAxisTitle] used to keep track of changes. /// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets number format /// for left titles - const _HistoryChartThigy({required this.data, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat}); + const _HistoryChartThigy({required this.data, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat}); @override State<_HistoryChartThigy> createState() => _HistoryChartThigyState(); @@ -819,6 +852,7 @@ class _HistoryChartThigy extends StatefulWidget{ class _HistoryChartThigyState extends State<_HistoryChartThigy> { late String previousAxisTitle; + late bool previousGamesPlayedInsteadOfDateAndTime; late double minX; late double maxX; late double minY; @@ -840,6 +874,7 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { maxX = widget.data.last.x; setMinMaxY(); previousAxisTitle = widget.yAxisTitle; + previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; actualMaxY = maxY; actualMinY = minY; recalculateScales(); @@ -945,15 +980,19 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { @override Widget build(BuildContext context) { GlobalKey graphKey = GlobalKey(); - double xInterval = widget.bigScreen ? max(1, xScale / 6) : max(1, xScale / 3); // how far away xTitles should be between each other + if ((previousAxisTitle != widget.yAxisTitle) || (previousGamesPlayedInsteadOfDateAndTime != _gamesPlayedInsteadOfDateAndTime)) { + minX = widget.data.first.x; + maxX = widget.data.last.x; + recalculateScales(); + setMinMaxY(); + previousAxisTitle = widget.yAxisTitle; + previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; + setState((){}); + } + double xInterval = widget.bigScreen ? max(1, xScale / 8) : max(1, xScale / 4); // how far away xTitles should be between each other EdgeInsets padding = widget.bigScreen ? const EdgeInsets.fromLTRB(40, 30, 40, 30) : const EdgeInsets.fromLTRB(0, 40, 16, 48); double graphStartX = padding.left+widget.leftSpace; double graphEndX = MediaQuery.sizeOf(context).width - padding.right; - if (previousAxisTitle != widget.yAxisTitle) { - setMinMaxY(); - recalculateScales(); - previousAxisTitle = widget.yAxisTitle; - } return SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height - 104, @@ -1025,7 +1064,7 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){ return value != meta.min && value != meta.max ? SideTitleWidget( axisSide: meta.axisSide, - child: Text(DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))), + child: Text(widget.xFormat != null && _gamesPlayedInsteadOfDateAndTime ? widget.xFormat!.format(value.round()) : DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))), ) : Container(); })), leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: widget.leftSpace, getTitlesWidget: (double value, TitleMeta meta){ @@ -1048,7 +1087,7 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { } else { hoveredPointId = touchResponse!.lineBarSpots!.first.spotIndex; headerTooltip = "${f4.format(touchResponse.lineBarSpots!.first.y)} ${widget.yAxisTitle}"; - footerTooltip = _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(touchResponse.lineBarSpots!.first.x.floor())); + footerTooltip = _gamesPlayedInsteadOfDateAndTime ? "${f0.format(touchResponse.lineBarSpots!.first.x)} games played" : _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(touchResponse.lineBarSpots!.first.x.floor())); } }); } @@ -1207,7 +1246,7 @@ class _TwoRecordsThingy extends StatelessWidget { RichText( text: TextSpan( text: "", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500), + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), children: [ TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.endContext!.score) : "---"), //WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48)) @@ -1295,6 +1334,7 @@ class _RecordThingy extends StatelessWidget { builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; return SingleChildScrollView( + controller: _scrollController, child: Padding( padding: const EdgeInsets.only(top: 8.0), child: Column( diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index 02f4e56..b82d8f3 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -54,7 +54,8 @@ class RanksAverages extends State { return ListTile( leading: Image.asset("res/tetrio_tl_alpha_ranks/${keys[index]}.png", height: 48), title: Text(t.players(n: averages[keys[index]]?[1]["players"]), style: const TextStyle(fontFamily: "Eurostile Round Extended")), - subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM"), + subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM", + style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), onTap: (){ if (averages[keys[index]]?[1]["players"] > 0) { diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index 7eb9f29..7fb2422 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -175,7 +175,8 @@ class TLLeaderboardState extends State { return ListTile( leading: Text((index+1).toString(), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : null), title: Text(allPlayers[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - subtitle: Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}"), + subtitle: Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", + style: TextStyle(fontFamily: "Eurostile Round Condensed", color: _sortBy == Stats.tr ? Colors.grey : null)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 660dc44..202bfb0 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -70,7 +70,7 @@ class TlMatchResultState extends State { timeWeightedStatsAvaliable = true; if (snapshot.connectionState != ConnectionState.done) return const LinearProgressIndicator(); if (!snapshot.hasError){ - if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, const DropdownMenuItem(value: -2, child: Text("timeWeightedStats"))); + if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch))); greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); if (roundSelector.isNegative){ @@ -81,7 +81,6 @@ class TlMatchResultState extends State { readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}"; } }else{ - timeWeightedStatsAvaliable = false; switch (snapshot.error.runtimeType){ case ReplayNotAvalable: reason = t.matchIsTooOld; @@ -509,7 +508,7 @@ class TlMatchResultState extends State { RichText( text: TextSpan( text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500), + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white), children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] ), ) @@ -533,6 +532,7 @@ class TlMatchResultState extends State { reason = snapshot.error.toString(); break; } + timeWeightedStatsAvaliable = false; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -553,15 +553,15 @@ class TlMatchResultState extends State { if (widget.record.ownId != widget.record.replayId) Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - const Text("Number of rounds"), + Text(t.numberOfRounds), RichText( text: TextSpan( - text: widget.record.endContext.first.secondaryTracking.length > 0 ? widget.record.endContext.first.secondaryTracking.length.toString() : "---", + text: widget.record.endContext.first.secondaryTracking.isNotEmpty ? widget.record.endContext.first.secondaryTracking.length.toString() : "---", style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, - color: widget.record.endContext.first.secondaryTracking.length == 0 ? Colors.grey : null + color: widget.record.endContext.first.secondaryTracking.isEmpty ? Colors.grey : Colors.white ), ), ) @@ -570,18 +570,16 @@ class TlMatchResultState extends State { OverflowBar( alignment: MainAxisAlignment.spaceEvenly, children: [ - TextButton( child: const Text('Match stats'), - style: roundSelector == -1 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, + TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, onPressed: () { roundSelector = -1; setState(() {}); - }), - TextButton( child: const Text('Time-weighted match stats'), - style: roundSelector == -2 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, + }, child: Text(t.matchStats)), + TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, onPressed: timeWeightedStatsAvaliable ? () { roundSelector = -2; setState(() {}); - } : null) , + } : null, child: Text(t.timeWeightedmatchStats)) , //TextButton( child: const Text('Button 3'), onPressed: () {}), ], ) @@ -624,7 +622,7 @@ class TlMatchResultState extends State { leading:RichText( text: TextSpan( text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500), + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.white), children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] ), ), @@ -682,7 +680,7 @@ class TlMatchResultState extends State { } Widget getMainWidget(double viewportWidth) { - if (viewportWidth <= 1024) { + if (viewportWidth <= 1200) { return Center( child: Container( constraints: const BoxConstraints(maxWidth: 768), @@ -692,9 +690,8 @@ class TlMatchResultState extends State { } else { return Row( mainAxisAlignment: MainAxisAlignment.center, - //mainAxisSize: MainAxisSize.min, children: [ - Container( + SizedBox( width: 768, child: buildComparison(true, false) ), @@ -729,28 +726,7 @@ class TlMatchResultState extends State { onSelected: (value) async { switch (value) { case 1: - if (kIsWeb){ - // final _base64 = base64Encode([1,2,3,4,5]); - // final anchor = AnchorElement(href: 'data:application/octet-stream;base64,$_base64')..target = 'blank'; - //final anchor = AnchorElement(href: 'https://inoue.szy.lol/api/replay/${widget.record.replayId}')..target = 'blank'; - //anchor.download = "${widget.record.replayId}.ttrm"; - //document.body!.append(anchor); - //anchor.click(); - //anchor.remove(); - } else{ - try{ - String path = await teto.saveReplay(widget.record.replayId); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.replaySaved(path: path)))); - } on TetrioReplayAlreadyExist{ - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.replayAlreadySaved))); - } on SzyNotFound { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.replayExpired))); - } on SzyForbidden { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.replayRejected))); - } on SzyTooManyRequests { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.tooManyRequests))); - } - } + await launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${widget.record.replayId}")); break; case 2: await launchInBrowser(Uri.parse("https://tetr.io/#r:${widget.record.replayId}")); diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart index b33362d..a255908 100644 --- a/lib/widgets/gauget_num.dart +++ b/lib/widgets/gauget_num.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; @@ -98,7 +99,7 @@ class GaugetNum extends StatelessWidget { oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent ),), if ((oldTl != null && oldTl!.gamesPlayed > 0) && pos != null) const TextSpan(text: " • "), - if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") + if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}", style: TextStyle(color: getColorOfRank(pos!.position))) ] ), ), diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index cadb397..caf65cb 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -1,10 +1,7 @@ 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'; -import 'package:tetra_stats/gen/strings.g.dart'; - -final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); +import 'package:tetra_stats/utils/numers_formats.dart'; class Graphs extends StatelessWidget{ const Graphs( @@ -125,13 +122,13 @@ class Graphs extends StatelessWidget{ getTitle: (index, angle) { switch (index) { case 0: - return RadarChartTitle(text: 'Opener\n${_f2.format(playstyle.opener)}', angle: 0, positionPercentageOffset: 0.05); + return RadarChartTitle(text: 'Opener\n${percentage.format(playstyle.opener)}', angle: 0, positionPercentageOffset: 0.05); case 1: - return RadarChartTitle(text: 'Stride\n${_f2.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); + return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); case 2: - return RadarChartTitle(text: 'Inf Ds\n${_f2.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: 'Inf Ds\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); case 3: - return RadarChartTitle(text: 'Plonk\n${_f2.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); + return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); default: return const RadarChartTitle(text: ''); } diff --git a/lib/widgets/lineclears_thingy.dart b/lib/widgets/lineclears_thingy.dart index 35b8d3f..2536891 100644 --- a/lib/widgets/lineclears_thingy.dart +++ b/lib/widgets/lineclears_thingy.dart @@ -15,7 +15,7 @@ class LineclearsThingy extends StatelessWidget{ return Wrap( spacing: 20, children: [ - Container( + SizedBox( width: 150, child: Column( mainAxisSize: MainAxisSize.min, @@ -30,7 +30,7 @@ class LineclearsThingy extends StatelessWidget{ ], ), ), - Container( + SizedBox( width: 150, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 8ea21a6..4a5ca7c 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -2,6 +2,7 @@ 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/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; class StatCellNum extends StatelessWidget { @@ -70,7 +71,7 @@ class StatCellNum extends StatelessWidget { oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent ),), if (oldPlayerStat != null && pos != null) const TextSpan(text: " • "), - if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}") + if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}", style: TextStyle(color: getColorOfRank(pos!.position))) ] ), ), diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index fbcda33..b5bb240 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -4,6 +4,7 @@ import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/graphs.dart'; @@ -299,7 +300,7 @@ class _TLThingyState extends State { //alignment: Alignment.center, width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, height: 70, - constraints: BoxConstraints(maxWidth: 768), + constraints: const BoxConstraints(maxWidth: 768), child: Stack( children: [ Positioned( @@ -307,12 +308,12 @@ class _TLThingyState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(t.statCellNum.estOfTR, style: TextStyle(height: 0.1),), + Text(t.statCellNum.estOfTR, style: const TextStyle(height: 0.1),), RichText( text: TextSpan( text: intf.format(currentTl.estTr!.esttr.truncate()), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), - children: [TextSpan(text: fractionfEstTR.format(currentTl.estTr!.esttr - currentTl.estTr!.esttr.truncate()).substring(1), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + children: [TextSpan(text: fractionfEstTR.format(currentTl.estTr!.esttr - currentTl.estTr!.esttr.truncate()).substring(1), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] ), ), RichText(text: TextSpan( @@ -323,7 +324,7 @@ class _TLThingyState extends State { color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent ),), if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), - if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}"), + if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.estTr!.position))), if (widget.lbPositions?.estTr != null) const TextSpan(text: " • "), TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") ] @@ -342,7 +343,7 @@ class _TLThingyState extends State { text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? intFDiff.format(currentTl.esttracc!.truncate()) : "---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), children: [ - TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) + TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) ] ), ), @@ -354,7 +355,7 @@ class _TLThingyState extends State { color: oldTl!.esttracc! > currentTl.esttracc! ? Colors.redAccent : Colors.greenAccent ),), if (oldTl?.esttracc != null && widget.lbPositions?.accOfEst != null) const TextSpan(text: " • "), - if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.accOfEst!.percentage*100)}%" : "№${widget.lbPositions!.accOfEst!.position}") + if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.accOfEst!.percentage*100)}%" : "№${widget.lbPositions!.accOfEst!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.accOfEst!.position))) ] ), ), diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 653b33e..a900051 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -339,7 +339,7 @@ class UserThingy extends StatelessWidget { children: [ if (player.country != null) TextSpan(text: "${t.countries[player.country]} • "), TextSpan(text: "${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}"), - if (player.supporterTier > 0) TextSpan(text: " • "), + if (player.supporterTier > 0) const TextSpan(text: " • "), if (player.supporterTier > 0) WidgetSpan(child: Icon(player.supporterTier > 1 ? Icons.star : Icons.star_border, color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), if (player.supporterTier > 0) TextSpan(text: player.supporterTier.toString(), style: TextStyle(color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)) ] diff --git a/pubspec.lock b/pubspec.lock index 43f3c9c..ff532e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" archive: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: dev_build - sha256: e476ac99174842cdb01e64c1d379d041d2150f9ad2cdd2713eb1ca1bdbf30507 + sha256: e5d575f3de4b0e5f004e065e1e2d98fa012d634b61b5855216b5698ed7f1e443 url: "https://pub.dev" source: hosted - version: "0.16.3+2" + version: "0.16.4+3" equatable: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -181,34 +181,34 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.2.1" file_selector: dependency: "direct main" description: name: file_selector - sha256: "84eaf3e034d647859167d1f01cfe7b6352488f34c1b4932635012b202014c25b" + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" file_selector_android: dependency: transitive description: name: file_selector_android - sha256: b7556052dbcc25ef88f6eba45ab98aa5600382af8dfdabc9d644a93d97b7be7f + sha256: "1cd66575f063b689e041aec836905ba7be18d76c9f0634d0d75daec825f67095" url: "https://pub.dev" source: hosted - version: "0.5.0+4" + version: "0.5.0+7" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: "2f48db7e338b2255101c35c604b7ca5ab588dce032db7fc418a2fe5f28da63f8" + sha256: b015154e6d9fddbc4d08916794df170b44531798c8dd709a026df162d07ad81d url: "https://pub.dev" source: hosted - version: "0.5.1+7" + version: "0.5.1+8" file_selector_linux: dependency: transitive description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: file_selector_platform_interface - sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" file_selector_web: dependency: transitive description: @@ -253,10 +253,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: fe6fec7d85975a99c73b9515a69a6e291364accfa0e4a5b3ce6de814d74b9a1c + sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" url: "https://pub.dev" source: hosted - version: "0.66.0" + version: "0.66.2" flutter: dependency: "direct main" description: flutter @@ -295,10 +295,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "35108526a233cc0755664d445f8a6b4b61e6f8fe993b3658b80b4a26827fc196" + sha256: "87e11b9df25a42e2db315b8b7a51fae8e66f57a4b2f50ec4b822d0fa155e6b52" url: "https://pub.dev" source: hosted - version: "0.6.18+2" + version: "0.6.22" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -311,10 +311,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -345,18 +345,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: ca7e4a2249f96773152f1853fa25933ac752495cdd7fdf5dafb9691bd05830fd + sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "13.2.1" http: dependency: "direct main" description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -377,10 +377,10 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.7" intl: dependency: "direct main" description: @@ -421,6 +421,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -441,34 +465,34 @@ packages: dependency: transitive description: name: markdown - sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "7.2.2" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -513,10 +537,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -529,26 +553,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -561,10 +585,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -585,26 +609,26 @@ packages: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -617,10 +641,10 @@ packages: dependency: transitive description: name: process_run - sha256: "3d5335d17003a7c2fd5148be2d313f5b84244ab2e625162fdd44b4aaa48bea66" + sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e" url: "https://pub.dev" source: hosted - version: "0.13.3+1" + version: "0.14.2" pub_semver: dependency: transitive description: @@ -657,10 +681,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: @@ -673,10 +697,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: @@ -734,18 +758,18 @@ packages: dependency: "direct main" description: name: slang - sha256: "77fd99f7b0da15e671ef0289b24a0a63e74f693c58a0ca54111388e4c0ddb1dd" + sha256: "5e08ac915ac27a3508863f37734280d30c3713d56746cd2e4a5da77413da4b95" url: "https://pub.dev" source: hosted - version: "3.28.0" + version: "3.30.1" slang_flutter: dependency: "direct main" description: name: slang_flutter - sha256: "57817bb15553bb5df37aed3bac497286bdd8c2eab6763f4de6815efe2c0becee" + sha256: "9ee040b0d364d3a4d692e4af536acff6ef513870689403494ebc6d59b0dccea6" url: "https://pub.dev" source: hosted - version: "3.28.0" + version: "3.30.0" source_map_stack_trace: dependency: transitive description: @@ -774,50 +798,50 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.0+2" + version: "2.5.4" sqflite_common_ffi: dependency: "direct main" description: name: sqflite_common_ffi - sha256: "873677ee78738a723d1ded4ccb23980581998d873d30ee9c331f6a81748663ff" + sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" sqflite_common_ffi_web: dependency: "direct main" description: name: sqflite_common_ffi_web - sha256: "3d4b550a09fda9eb0a9ce3bd98c8080f57b2cc1f4b19e844290e82843e73b63d" + sha256: "0c2921454d2e4a227675fb952be9fef916cf65fb9e9b606b54cfdf080d3e9450" url: "https://pub.dev" source: hosted - version: "0.4.2+2" + version: "0.4.2+3" sqlite3: dependency: transitive description: name: sqlite3 - sha256: c4a4c5a4b2a32e2d0f6837b33d7c91a67903891a5b7dbe706cf4b1f6b0c798c5 + sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "3e3583b77cf888a68eae2e49ee4f025f66b86623ef0d83c297c8d903daa14871" + sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 url: "https://pub.dev" source: hosted - version: "0.5.18" + version: "0.5.20" stack_trace: dependency: transitive description: @@ -846,18 +870,18 @@ packages: dependency: transitive description: name: syncfusion_flutter_core - sha256: "69c827931957d5b121ee9f0b9b0b8d7d0d1ac537b61bcdd5c3fbffc044bbe86e" + sha256: "7666506885ebc8f62bb928ad4588a73e20caaff2b2cf2b2b56f67d98f4113525" url: "https://pub.dev" source: hosted - version: "24.1.41" + version: "24.2.9" syncfusion_flutter_gauges: dependency: "direct main" description: name: syncfusion_flutter_gauges - sha256: "78515dcfbed952c36ab9ca4a1e6c99737beb7c6cf2312efe8beca2bd314a3955" + sha256: "87be13e520fc1676a725691446f411f549ea5b6ff2580234db0479799f213757" url: "https://pub.dev" source: hosted - version: "24.1.41" + version: "24.2.9" synchronized: dependency: transitive description: @@ -910,26 +934,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -950,18 +974,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" url_launcher_windows: dependency: transitive description: @@ -974,26 +998,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.11+1" vector_math: dependency: "direct main" description: @@ -1022,18 +1046,18 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.3" webkit_inspection_protocol: dependency: transitive description: @@ -1046,26 +1070,26 @@ packages: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.3.0" window_manager: dependency: "direct main" description: name: window_manager - sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7 + sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 url: "https://pub.dev" source: hosted - version: "0.3.7" + version: "0.3.8" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: @@ -1083,5 +1107,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index a2fd891..acb5c88 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.4.1+15 +version: 1.5.0+16 environment: sdk: '>=3.0.0' diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 63ec0f8..ba3cb2b 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -48,11 +48,11 @@ "noRecords": "No records", "noOldRecords": { "zero": "No records", - "one": "$n record", - "two": "$n records", - "few": "$n records", - "many": "$n records", - "other": "$n records" + "one": "Only $n record", + "two": "Only $n records", + "few": "Only $n records", + "many": "Only $n records", + "other": "Only $n records" }, "noRecord": "No record", "botRecord": "Bots are not allowed to set records", @@ -77,7 +77,7 @@ "supporter": "Supporter tier ${tier}", "comparingWith": "Data from ${newDate} comparing with ${oldDate}", "top": "Top", - "topRank": "Top Rank", + "topRank": "Top rank", "verdictGeneral": "$n $verdict than $rank rank average", "verdictBetter": "better", "verdictWorse": "worse", @@ -136,10 +136,14 @@ "openReplay": "Open replay in TETR.IO", "replaySaved": "Replay saved to ${path}", "match": "Match", + "timeWeightedmatch": "Match (time-weighted)", "roundNumber": "Round $n", "statsFor": "Stats for", + "numberOfRounds": "Number of rounds", "matchLength": "Match Length", "roundLength": "Round Length", + "matchStats": "Match stats", + "timeWeightedmatchStats": "Time-weighted match stats", "replayIssue": "Can't process replay", "matchIsTooOld": "Replay is not available", "winner": "Winner", @@ -176,6 +180,15 @@ "many": "$n players", "other": "$n players" }, + "games": { + "zero": "$n games", + "one": "$n game", + "two": "$n games", + "few": "$n games", + "many": "$n games", + "other": "$n games" + }, + "gamesPlayed": "$games played", "chart": "Chart", "entries": "Entries", "minimums": "Minimums", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 6911667..88dcfad 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -77,7 +77,7 @@ "assignedManualy": "Этот значок был присвоен вручную администрацией TETR.IO", "comparingWith": "Данные от ${newDate} в сравнении с данными от ${oldDate}", "top": "Топ", - "topRank": "Топ Ранг", + "topRank": "Топ ранг", "verdictGeneral": "$verdict среднего $rank ранга на $n", "verdictBetter": "Лучше", "verdictWorse": "Хуже", @@ -136,10 +136,14 @@ "openReplay": "Открыть повтор в TETR.IO", "replaySaved": "Повтор сохранён по пути ${path}", "match": "Матч", + "timeWeightedmatch": "Матч (взвешенная по времени)", "roundNumber": "Раунд $n", "statsFor": "Статистика за", + "numberOfRounds": "Количество раундов", "matchLength": "Продолжительность матча", "roundLength": "Продолжительность раунда", + "matchStats": "Статистика матча", + "timeWeightedmatchStats": "Взвешенная по времени cтатистика матча", "replayIssue": "Ошибка обработки повтора", "matchIsTooOld": "Информация о повторе недоступна", "winner": "Победитель", @@ -176,6 +180,15 @@ "many": "$n игроков", "other": "$n игроков" }, + "games": { + "zero": "$n игр", + "one": "$n игра", + "two": "$n игры", + "few": "$n игры", + "many": "$n игр", + "other": "$n игр" + }, + "gamesPlayed": "$games сыграно", "chart": "График", "entries": "Список", "minimums": "Минимумы", diff --git a/res/tetrio_badges/mts_1.png b/res/tetrio_badges/mts_1.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf0b285858666b6e009f688ff053215fefb599e GIT binary patch literal 45779 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 literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/mts_2.png b/res/tetrio_badges/mts_2.png new file mode 100644 index 0000000000000000000000000000000000000000..eb704f88ba1e4e8be333cb9978098f3a3c60c641 GIT binary patch literal 46077 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 SZKx6C&+s^H#(SN{i{K0Cnx literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/mts_3.png b/res/tetrio_badges/mts_3.png new file mode 100644 index 0000000000000000000000000000000000000000..8f706f3c53c459f74e8ab3156008a04663c647fb GIT binary patch literal 42267 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 F{tpRwsJs9G literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/mts_participation.png b/res/tetrio_badges/mts_participation.png new file mode 100644 index 0000000000000000000000000000000000000000..b48f4019dab3cfd9cf69954723f82695c3f33335 GIT binary patch literal 49519 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�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 z zZOiYp%R=Ln92)^M`YyVolzEr3By3`|A&#e4jp6G*UW+wb6*TH_SK18ox^3L}fw}1K ziFa7Hl{Lg@frzYNl+Za)3jw&QtKZ>}rx!4Ln{+|%zxLJ_}0+yFvg0DSS! z=8X{FJP@`0^XEkI`H%D<9(6FhZ5a82V^TeQ;|wOZ+OCMrPb2xe<1{=jhH)IXoZ_-1 z?z?vwU;I%9N@K5Bv&F@gA76v-ets@mn?q(VxpAkZ1lJGN6!rLZ2BJRYXlRbPE3i`K z?n2jIbqpSU?S3j2byPHTdd2*cT)DaSVUG%C()V`;40j;BNnS*dV z26r?IHdx>cM&E z&qcDa+{6QX6H<GH?~p7W1%ZQGQ6Ng9ZAPCg{_%!DR1rYu%tjo)umOL#XFKThQ^LGX ziGTjsJh*NVlm<0|RF%}9W}OpZIKFLp<@)$~E3`Onq7fpo$6As|v^65x(S&$Y+%zH$ zM{JDj%;Ae)8^E`Ik;ZVAZ-JIlIO$jmuUSxl?Rj{5FbqAxjZjyd6vOrJ>A?rDUW5}) z?n2b`?z12)-eTt5;%RQwjS44rjF;+cJFNUmnNrA1%OB ze}B)zt4>Sdx~uwN*)GNgvv7)B|FB2=^$kHeUP1$dkBRWJ>D)9^nOX-?6x;#Xf(of} z=7N9TU2AvXd*6N%O`UD{z-4iCwTCgc%YyKV`01U)xa~dW(46u6BJ%FUJHd4Jaiclmfx*8_(qm zxbpR3{PRyd(|UVvh}gh;FKomKFG(Rilm>zCd1o8iA>RYAc;?*Oj1SbN9F16u<#^ZiFSS)}`Blt2xa0N#eB(z{ z$vC9Z6xEp5V`J&DNu0E_0VgbJMr${TJ`!SM>DOOKA_TMcrXv+ z(S5-MK1A#Pv-cfva+T%Y|5N6iDLdOYyU8XSQb;8UB@l{$APBKUDT;z1@BvDRAYvB~ z@N#L&mCywQ0ga-FNJm6KB$NP2NJ4r~cC(u;v(r!e-g$n{J#WtJX0wFZ6mlPaf8KM> zOm=6^o~OJ|)%dzcVK=CdF>E;dp8Hj(iR*c~bQozcNn zs3@bKOGzgp&6pf(pd12beq$@`&wdAK@(0ZQo*_W~uXuWn?fILRV0e?e?WAoRcBh4W zHV=zn(fxA7b(fQ&{K6$5$V76eXmMNcp2tBeMUr3q#8D6MRtPFW5fjtDJUTE2tk=#) zDxdRlan~N%A3cnxt+rB`{;zY~KS+DC0N_?$$8uv2Wne}& z3+DgPa2)gH@vtn5_QT+Po~AQ%g#>Je$~(!#cNNh7R6r9T;lK~C13>lEfGHaY3(9IZ zvIH^(=1yj54e1*6NP&?7yQ`gN)XPE{?u0`_IYI(jG4r#A3v|>CkYx0tCY{BFMzV%L zHX@ZNzIkGLI_3<=;P(T9DZYHJO6?jn1^Q_H-5aL_Kfl~MsMJJMg{MlvBY(bJ<@SvI zGr{@<>D%EP4?E{z<%ey!_J)OcZ9x_TKpVM)J%wW0TLploY4u#se~mzRpfvxiA2bQg zKOP(KHxZv$MYt^$pnLbmPDF>47TPop|;+26wJIdPW@cIcR*0+t6* zwH!b~g&m4wWh;bc07CW6{%tV7suuen0-O_M_#l(h%8<4FoN8=K<|ojxAMaKbW( z$^6o(r^yEsn$5*3o&r^X;`%&Em(5-(VUt44~Wdf3znghSDiuI01&8= zB_+}LOl{dJ9kW*SyZKrf2dRXO%7zHIVvYoW%f%#|A0RcqrhxJW3NUnL{1E84XMCSc z=0KKRv8ms)ORr@3;A>96>=z(r=VNC6B9q?@^ua@~q%fqfkBfaw9pQKc(efA~u?SBs zCd;e1*I=Obfl2;OH^}#p|CU3DQsW1{KczyGkfm^-rfPWSwe`65o|TyMK?WWMloqj) z%VArzw+R4^+PxwxaOFTX^E~6Tah!GfD1=O%7>L4PY_A7A(lWZ8q_g{13dKJkghVrb8!$k_cZ7RF+8NHud}(|YiKXf z-_P}2SxLyu-J?4%Yd^jwwId%Fck%Uj8^D5%9ew>}er8&KRluHm`hJnJD9URpF!!BK zJoCaztX`9%-!r(W0tbwb;e_J{W7LE?c(ipQ!Q=ldrl8lJ$(7%uSH@>2Cyz*{xd?ds z^$ob|zU6preip8Wfu;_DncJ)M+v@}Xhx+zs=U>U`@Ck+(6Ey{)SQx4bV^IsEyqEbWv8YnB!G(!l-B6#hsG-f~Bg$-LgG&ShB>Y^%`Y7TkZ@KjF_ zGDB2EVuTHGTtLV-gL!YynQ}o!FV>FCoIT+%xF%n==L%Hm_158+Q8ebu1Dnpt zN5D}hppalQA|_&$Ww`gg)wut!?U1Bk(WV4mdNU!~Xl=rgQzH1mW#cjNz(Kyi-BwId zYJC=T!F@hE_oS)sBr|yWsa1IB(N%bVjfT5omI{ZSl0)p516Qc1$@AG)LoouDB*vHKY*JmQ}^Ea8Vkwe$|3Ct*-z3etG#I zm{AY8G!Rw*!!%&V!nBRpxY@z7WjSnYOQWTw1Faby-L2h7CJMqr2^77=O%LV6>roSR zQD3ErgqW@gE{BT#SbrN!a3(p0L}P!Tz)ms-kiaL zOM(O^_3r0Eogcih4u5^F1y8=%iY;jcJCR(TP<0zhK%cbfz_py`|9D9S01w8v>*u3D zz46VEA1-rc8!p(glMRC>?Fv}g0`lN?T*MGUi;)Sdgu<-kiq;#Ni5j}r=JBy;$shVRAGPiw!JpB<{S#VI)bNNCzw;J9xt?sq!$ zPro$`=Uy;@?tRZ9BNRFLD2hQZqzd+=6wTl-al|=ox|21-j55+KU8$ zAe|W#W8Mt z9Jc3DLksFzn9h=g;5w|~At}JnlF5Uy{oa|ME%LsZUkj1xO-Vbk@)#;=D{P3Y)$v5RQ2z!wBoqW`ql z2!JYqaRM(2#85&SK1jo3zc~=qLn`S3PARG%LUKz*ADyeH}7P+lJ@=|iS`}=Fht;|+~P|qB?H{{<0dhXa}0Eo z!g;}*Z`YL%jSO=IyY8}ZAnj^|$n1akp%u9G!Dcufdeolh9fcm|-G?ND4y*QX?7=1u zp9ttNjXnB-_k1Lh4n7&(C}w^RXL5wYK*(evV1_947q2SEtIw>&(w`qzu_^c!h79(EQzE^vlG7X7BZLjXir0K6j5 zh!WDF<4xRm*EB@pVW?Y=z((Mq5%7F}09yoszIGlw=plMwarfOzG3$35;Ck4tNTHek z9`c(fQ*1it)Lkfj7VRi}E=lI^6VuCdWrn5( znO`>~({qTEt{Em-0(4)O&u_tXw|t0y&(Cb@-jXx)_qR{M`28E8>S~~kI8*_4dP?g} zg3FZs?mnhzVE%#xZoG9F7Ok_f6UY*{N8tPY7Xo{S0B8_6j+=dGrDTW)t{;t0eP$ne z2)W#|=ppAM8Yu)Iw*+zzsp&dBFn8Rs7`HuCI_B3jARbc?(t)m|hkX8{Pb4~ctbxN1 ztHpu)#W1KY1VsZm0CVXaIWF@#i)RW@{Q`k==y_gX0X?S=Nv9xZ@&)(#3_z7D_e4-Q znMsGXHRB6}_*}f^hdZeuYQ&__Xg-z4`|mX4iDx(A&G);|)^1TmBxZV;G$x9R&l-kF zhc>Yv$JhQcVk3JT1RS}v(tRkvPr{uVJpGDX9|lT}O_ zt73Sgii$YUodk|QD}&8jynYF`i6c#%_W4>IeM|$w5g^l@Mkbk|y*+vRiQye8UAA2o zZWITn^kzb!d5Nr5;E*4mZ=ELq$?UQ*hBWy#UUk;|I_ES{enU6}5(FX6bC5L+Bn;Rh zpvVLdR3YJ&Y)>Jb^H3gD5UGk&^`qakLs}ul14Jna%JvJFNC=Ps$mjF4T3HeXh3`tg zJ6smQmd$zGbmuy}wKxxNFA@T~S^z{j{P%XL`~2PdgJJmm$zv$y?3aCd+nIx}^0D-E z(?j{v^J{SNl^-H+dHASQSFK>iaXL;uLBo{s8p38zC);9%wx3N7M)o9>*-x@Bu z_ta2`Pk8pJ9zW08p6Ux6H^BG;#1uNWC?hRRVCFBXU2uHAh)axw-aY-y{NVk5FpZhC zyjfs=6GnuY-|(5A1OaC`()(GX!*e)8F)%~GE1Dh@@K^?{01`?X9n*IZOwP zD3G<}y~nPL>&5|)7kTNzR(#{4chUY46GnzNDwz3A1E&bakC!Ri4h;Xx!P(!>!gc$$ z9=?8DIj;D@e$;y})g>9<&XJY_F}_=XM*@J4i8=KU_bm+65Wpvw>l4^=0Bw87iAT&0 zdH+y6(o0$Zs!H0QnA{A3G=Ic^VFm;MGe2v6u5D0&UyNFcmT5>t< zS3?QusA&~2V`g!DSgd?3A*nntEeWK%QuF{j)Qp2#lm({<2h?_eQ`o`@Fh4PVHk&7H z7%_F!RK`$MSA~!pf@_;l9Rrosm8cz5h3eW0#LFXyghMcRvXEcC%nYsRy#M#LJk>ME zRX~%>FB!kLZbBXyd_af(MbL3^mLlI!*`SpWsI26wO3n!Z9EV1Yqobt*5iN}yuNsas zPK-lU26Vo~1${-n_67kE6F3YdrlSrlgF;0+o~&ElStG>Gvz8 z0-5xIc4oFs6K#>rmFw`p z><(;bGf>x9i^{qRL}DgF1~IloNDH(n+ZMR%P=M)a<@4T1zAt!;o)!eR^j1M&0mK4< zORIGf3S=>`V98q!l8H1DtsOYyq)Oa)c>~JB-asb6T`O=-Z$jXcE&wL602ov1=AM(r z#{t&-cVGJolC53HB(un*{uddflNqGC3rH9K;9^Z7*_A?fXA+&_{jdFF75@CMGef8|^R9!ws4NUz_p=GeW-?^*+dPDH>xLS#5eQ_?Js%6Oc9Z=FESp!$ zCdSWO7RqBL7A{xt?Hd!g>+di}vaAT>PeGad*VN^t5TMn6Y|5@tdGBNye{i;?Z z+Pe!(PK=)}Aj#-X)AdA0H#%B7v2j&1e)Vu09Crt+4iC~031eF%LQWQbGuzs(xBn+I zN1}dc4B1qctKd8^(@W?DkaI8SkW&?cD$AB(u}3_YC#_G}am~@gL~E}Sg@3-l z*27W)TLZRKf*pwb3Ck@l8H{W+Fn|;nxK&^(dZAC$U=SYbt?=}d&z4#UjTsfe@kdre zQB`vGyzy=qUVcB1H{NJNV|fmdD7a!SPirolCy#nMl|^?Vg@yClaOdnUEN{jR(^WGK zj2f<@H~Qn#HhyqJe!ImeGmfsnCEwl`sbm^763mtHdkM(u#o~@aV1{CV$oH<(u~7(t zMPs<`o(`n*JJ!->&b$=HHJKPODo#EGc<{R^D+0_!y;uPtV#egl@;w2Dm#_Pl59I5; zT@#Xl@=#E{$wg z3TxLUG3T8G9{zh0e|g10OBZ&SChn`^s_z)kH1wd^PuaNax*Qw_ebT-|4cvayWZ0ff zLW~ku&?DY0z^i{poKdVNmHMh*Jolm=FC5)Rgp|iYv8?f&Q8uClXIpZoy-1)Hl5QbGk_u>(Rc)J z&rRdb$GWf+sZwByz*7Qp|M^Mi2cW8I8Gxj~Hw4ZasNb!?gDnesFnV~1bKO8D{hXx* zSO{r(=qkNH`TLHCkL`!iHQ?u$88AW!km~|>JYwM&x97LZ{FPDQ_8ay?D5@isNW$ah zp&n^Uc6MnhYc*9stc7#C@VZ0qbqb6i`}V61Ywvf`KE7+1q*B?^mydO&mE{M~R4>dqyar zameg{=$0@pJ~I@U5d7*}4o>)H7Jq!ort|GZFaCRGBaZk~Jw5OaWvKBW5E(<~kzg8w z1OeyUOF_U%Hw;d@B_Y5?I_xZj=Mhs~ap6$Z$35&QeRf(5N>~S#8RslUf%%!KQ#oV_ z|I6SRfhFrOpb*_Vdd~{F7KxS3s82TOb+segD(Ia zwo*LXA%VP$D8$P0j6Gfe060+KP=VV8$}zyy-kL+6q`t#U1bp_0Fe)NmNlI#O{Go|= z{uaew?hIr0ZDG9rSOhOU62XZ_X<(kh!lf=Q_(=|5y(EietKEKuF0a05C}wko1KGjU|(o0k0Vb zTNZf%witNz^F90yNp`LP3uG~J=41gDS&(^*9udJVq^Q8n0*CCe0zg&O1_0gM>8AlZ zfmUqMk?u$$N7A0G0F+gkIO8)4GzA}vYN`}W8L!fM^fV2}9id^&2o)3~U+>|OCv2R0 zQ5MIYnZaLQuwgs>>vD7RWkYcKxf2S^PZ|o~7(b7rW(0%)BY4i*o&^EH-wGH6em{HZ zIcGlInL>F5aMvy4am|JG7~81!eXqFf+03t1mvTf7M1Tw+%Ig6N$ zT9Xwl0D){W4H5u_W;tVkpnS8cQsj*Y0 zgmLDP7IY0eOl|ERjy*ktsb5Ot*fTQt(goSVO6Su~PLqfKmA4#NHu^<%WghOoWk1X~ za}tR8^ZtMmnX|7cfg~1QCDXG75VQ)I*-f3-5M3h}{2pEBntG5Koa#s-+0~7sKV6N7 z?wo`_-L@~T{Ptjc{qr?Aep(rh8Ry}PpQ#ot&Z}_ymBaAp?PKwU&sHGO-i2gGH*(DU zP64jx3j!$&Y+o1KU+1u`rYGK@4?3M>TJT9DVsVh{*!Co_&b&u(y%k!{PcA8C8PV2ba%0-939NkI+?1Ph)M!nOA%vA!Rr+J}q{;mRM5#d%jO zMoYU>>gmD%#iR1P-CY30`0xE=fhz{eQ)VI#>3`L1#hf2u`_5$C2-mK`{3lSjDiDzD$*=l zjuC@Ql94fdd98Rp0Q@2tmzlITA;6HMz5=uHKtd!acp#Zoj@R;=rMLul>vrLwm57!e zBf$jtpl9tuAmz8$_e$mu2nDu|Wc>jYh2r^+Z0gE04P~{YWPB2%6d|EG4P2o}^J$=v z(#RC7i41V}ACg$GUd6U367ukiZaDaLP~H);iq?J0idwA|I-3@ zl(xZV@Mhn1X&ojV)BsIW$>W>oN@CfPc0Bt^8gG8+VN?TzS3XJ%kk8xX40cOxlTRBBBO0RCTsCQaXZ!OBAhla?q&72WaFTByLV&M{ z)#caZbliX-Am#>N=Rq5E+{bVbI^PyahL`66dI$riZ3+c>Z{&m_BO0cr9!A*kr#*>1 zzrU@Zy*_tCa_BxujZchEnxDg~T*tseVCmWE=4{OO-p%0eZ#Y=jsloN+=Nn$_;+)Uv z7&bBjP19)G*|pimRS)El%qu7%Ef+XJVCC*A0LBRn7MLwCXrRpJ?|wCg&wsHAMmPk| z19^DUsSJ{Zy<$2`!!?#J&!e%%L$pdGjc<_vpsX;~wjgS21MYn>R)Rk9Qp*`Ss3u|b z*lHX&u?(L&aRfI&wKqDEL4es!|^C8>MF7H!zBJarxWd+7V2t3 zI6&O@Ne5IS)zyteTY^Tg({v^eAfX|k`;5>15(u=lAPd7!AhT>_GC3rZ8Msy+EiD$> z+H5$kf|@E7gBwj6ZW=F(6n+-HH<3tU!$t+y{x${ML8-{&*#ch?$n0(cV1huNC!S8k z0MYbm248b{Vmpx(pJUOubWt~!N5l(n$mO7 z_%KF~D#M6|5NaB$P+wMNJ38mK-YPMR!{)At~{1w!GfCJ>#6ZlrA zg9rc6ibtR6f`=dtE_dYuv4tBSv*>yW{Q*A_xOsO<1s)=>N#J3D@faW) zKTN>|r`6EI&n9lcvrKW`95cQ}V2v+8YhHk~Ay2^NDzs!bg!9n&$c%s1mnty*_y$zg zRZ`BlutfZZ6Hq{mmCMoq4hrizmfxPb59TKdFup9L;Z+xFNQD_TG4HKr{ONVoB4U|sr`F0eXm+S=;J_OufHwo zT~Km{+}GKm$c(?ON=8rh z=)JnB!*tA|c4e~g$YY0UIBbwI+HbOsZze-{@R>A9L^}J|<_NsDJ6Hfw`Jt%2Q3wM- zRWT2@Tv~}CBWp==lXu^;k;~;l%wJdq$lNDQ>x$La{Pccie*YnTW1fLmmxj?_nl#kI z4=);mrct$sSC&!3QO?Yxhl8w+TplchWC|*4V7F)(ClN1@dtJY+ zeZT{KkA}e}So!uzkG*E&u!%rLgU0t(<4I9Hi;kG+^KQdR;&YeJgH2UkayYV&2S9f! zhYSDT5NvE+hxu#0QfAm*&n?B=T_pep3k>1?NErr*PCZ(~xQTIqys{Q+f6}saPREv0 zRMZiCFiF6BvV62<6VR1W(I2Xfxw!nidejc9L9{%IaMa``5`@C<W0zi88rMb@nfYX7wsR!*l$+X>GIQZqcDg5&01-Sh3v6MLDM*?K-yASCH*#2H$ zp7-vc)l~(Cp(7fPA{GxLllGAJba5T1tkB?i9ofJXJ#u7}g_%BBJHTKeU%1BImu$LjI|{6H z8bANxNL+a38nm?ArAp%a28RWo+!X>K%1nRjK&C1lF~-GlN5_dNEXxj5``I?{^@EI0 zr0^_yfSXd#11`fzi`S|B_UxZ?QU#hu*O18>j+h|LPmD>7$c#^bog;w+?nzHzM_e%E zh9WKsg#KiFTde-~(lm{>7*j{Dh}T-jFYF=V8qx#=x$F?n2s_aUC&Q& zQt0%Z58i+{cfS<5R`y}(TZZwj>ROdmi*phDnBT(zWQ0BC5nhwqL zI#E=BwjVhvd4-i_Yb3{lfXid0w|%R9BP8|O-`=+Z{FGmcbH^&muwqF!zInw`F;AIFg@sINJCXk9J3xx;DtHqKKmwo<+GKz z==>&hZ|Ov)D@6jGg+Q^u;WhaA_^njlTow@6F{`%+jxZ!~oc%|#C zeV^P4>>L3wocotOI8dg4nc?Az(-kx}#VA<=yoY4QW-!xx%;a+Ahgv?gas@hCEZq1P z6J9^G|Lxza#UaOxL@XX9#}Vxtxgm$e4oHgxfb2U@%%3CXmy9M8YtVxf=^YPryCr|$%$KmBS1s_H{Xb#%kdNeh65WWdyy$Hk1#E7(Nt z>FgtmC~Ah#*&V_qH?{Zc;Xdtz3S4~tP;_kCLIpZ(jS$=-fx`_=1|)xNLcLDrO#J95LJad zS9uG^!%}D>a!olZstPk6-otZmrb^jg^Xmdn?hFCYAW+GBkC6kFId|R(4)#AF3d0CV z2M~k%xCgzn2+p%B9&62)>EFFMH;>t`>ewDl9vQ+T_a1_1MU+02>vCQ})^BFPnx7e; zJ^zj^1ym2MsT3u?&^fJ8gFWrI5lEnkC26imSz)Sf9PiF)#r3mRVfiKpz0pa>R^W%1 z4o6387qZ=H`g}c@-*p38Kj`@nG9YBwE+ABC_((wjL_%1;3b^|I?tXW0o_*VBMD#3G z1UiV02m>9KU2Wu zcev((nO`!!W&4i3AjA8|oTs4#{$8p-hk*We)M==!GLC%8!6|3Ig*BTUl!%rH94*i? zkODvzs1dkVU^H0%W#0J%ri-*3*)I8krgK|_>M`46x2zv%%^&_DhRWLjip?D39SOs8~ zgPZ|CaBiKG1MCd)$qu-=1YF&Lr&zEOo5bZUu){i>Tn3Kk!m%lez=cYAj%?U07(CLh z1kxrvdoLU!pMV(&!-yF4U}@ZmRA_oCiKX({?hFYn_WTp`2hw@Bt^MUOq;&#XxAxde zOChAGD33=l|ARbkcr*pu-F_+eA8#KGCz~Q?pd+(1gO17K&nJa|-!p+t?jG5N8o#cy zR#xddNFm32{eUBnnO|IUpnyV}%=oHYbyap~5-aM z)>0M>eO}<-10euLbAA7lrEl&b!)-Ia3RKq`bfefzlUi1hS}`#2fv*MjU4@m$PzjTd zG(gDI1w-WU#c#fcj;x9zqPcaALmDt~!Z=KtFcKG?GaW{#0(yA^plB#5*%??}8{y~{ ztj@Kt-45gv>yY<4k!xFuoMywyWs%ET$fOHv9^7@89>P$Fz85`AJbf1$&p{+cw`}rF zIJ7O1aepUh`WF@WFaRR1Kalrz>n_Aas=?#=f(2$WO&u$i=5YU$X{_zg&>O|Wz?J{6 z3X=~kN3uN$+mh+BL5n1K?79d{F9sn0<^psaP!Pdm0GxvmbQare_B*_{XEQ&*_`1*Z ze%}Q?mfBtlBsIYN>1Ti7hna>p z4#u&^9)(XIbp#HcdH@<424Qy~H`9T9dL!~(%aOHOkV~x+jIsh*JB3V|I^qxOd|_{IBv}#0l>@!c4Toe441@eyfR}EG}*FSVM|L|1w3Az)3Y;qSg9=D z|1gISmgcdk#e!!!Ks1C2O$v^g7DcSmplu1b^RdoM#Lmw^=ADeSQ>GmYGx?q5c(EP{ zRdmdnpUCsKK)e;BbC-PytYm0g!_bLod7K!n0wHF6Y4$UtOQ-^LEufTqK}DtNSsM%I zuE&|*U0kZlL7OvjZ!4t$s1i6+;O2oc{ZBs3#;2x*5aJo|Qri=_RFfH;A9h`rQ)X#3 zNU{>+n^787MV)rqo3~)grY%$!x@AKM|MN@?Gro8trk{8mCQq6`ziZDRkF14Uayc^T z6-c))LZWjSGIrpbK-{ki?}VWoe{%%f#;VvQhF zRs+>62UHV!DhE#w16mk*I0ns#f%s34K+#RUr*f(=92-ui3oRWvEPHnW7B5_ig)5rb z%r5TnuTtaHn1C8Fp_Ru_X1IvzuDE6+7SUis0ig@)Rg<(eyJCv>{$>H4%^B+ zREtNeNv-d9T+$4_N4(0hVm(cLX8z#H$2<>FW<}Z#O{x0b1B>v>-*=!yw1L};uP%iE zsN)*{zjNka$?1?$Hoh`lgJJ4?qqb&#W^S(CX7EGH$EFS=OiW+U_M|!5a4I@uLsA>%T>`w}Lb{5%=rAT)#LaJi{677qT&83h|W=Z&Y?5H%uBpAtW zK#We3JOZPt3Azd&_!Vi(nDlx*&^av|`!ulc0_kj!5!09e8FNGE3h14WVAnmh2_pDn_>%N>-69uPQR!2P%a zpq9Pi_vBSzC4H#Boqj6*<_NnnOY*++M!@?qB zq9IZp^9#1*Rsu4zqx()gUK!H-mjZ(szX#KM;MK1M;Vl}aG$YlO!s2W7d6i}#i?tiqZ;$oQsM38d{QCT@r|pUYw6 z%8h8<(1z;xAdKAqJX97KUo)`R6153)plCXPJ!P8{Aevshc<~ba>y_6DZ@=?Cve_(3 zK?-LjHk5l9Gct;?qhc7pZxs8Et-#>MGI)w)+CZ9i(7`A7^#Tf%z_@Z(Rt!o(K>uS0 zBsWYI)*4Nw{=F#i{_XR5l!#su_<}%oM+88ewf|EB69%HQ&zZ+qm@+8@P4}HOEMe84 z<_{6mO93EOGip+5Za8KlA+-MLg{$D&8pcjK4fSKcvDaw)y@?8p)7jaDS6_Pr&%N+Z zy!@}%(AL(Dk`iwzYRgqj62jo11FJD*d>O`1s7Aat2A_(XmI6L=OHCip_JP;^-ijP@ zv~m_TwCQN+z!f((W7X!8H4iyg;Ncw+05t;N<$Qk)C8lF0*!bdcI&?$ho7i*TAJS=M zXP>_vpnYsvJOayBv3hwM=FHuU5ktn{E9YGYvuZRxNtDl9*s`Uau&KEPn>V*%OM3^} z1-iNtNOX6T*_=*idL&1c$K!}ZBN#NO7W`-!1Cp*uxj;MY}(X} zR4Uyc#~3m8u%SaRe*750=wmT<%xH`rJpw}pHy{#;ARIRFNu};&Qe;EEhR6Q=Bwl&- zKgeV<7)Xkl5aG-a4nL>@haXypv17|&#-sGi1~a8tD999J76Q^4DTM(E0qQ4s&;1+m zj|B=!K#suA1a9cB00^T1b?+M8*^hD~_JQ4z|n z!ogE2P|;BC$NM>xy*M2_2$zHc_qfbq-D8{0R2+0j1*K-Xo)^fA7YFFvlE=IcZM?hG#flacSzFoG2n7HXMU}&5W=G`o6lR6&DP$U!F!8s!T1iAHmlU`st+O! z_tPJL7nfXg-fkPyFYHI3dG;T8`q>xp&fNFO#NQF(?<<^9lX1q^ze3xc>gp=|AAK}h zx^x8zf>#)9~WLChT z0?mC1fMFc^d$QC+Jw^=j@Qd%%6iRDJc)4+}YeYIeQ zQ+G$rNBj4Ce)ABXc;abvCAv#7V-Xjee-;V5`ayO7r|mx`wY0S2fd?NI_&r%+JCow3 zi$f-aar|L1OxeGhcp>DEZ)ahk`27wkH2B<;t8o3p-S7s)3J3}SjRnB3ORfFGA&9X_ zQ4AekhSrT4{9{fU|DI=kERMHO6hT-e6h-JxB#}y|sRA*Z%^{c1^@t{r zXt`MpRUkV^5%hh4!2KAI5zTJVN(jXVc&+ z;4@PrIPsVo3>#TV%|Z;GBjk2EF!0O323drG=b*c1V9z{^nan$+W(x- zc&Mx~@$y@F%wK~Y3UAcZRO6usZpWe1reZfDYDGTlj^7kO-uj(D6be1+$is2w8DGKi z#~(v|?j6UW2U++R1eQ^X?TQtvuueo4nwvLs6k&(AAoM#M>gzFl_)rXO9D>Hi!Dt*Z z7(<3MpuWBi)m2p_3=AWLJ&6io)nCuL2M_*kHic7nHt8yG@E8p&L6%Z5vBLuD#IJ=ade@y`f$8D@n=-5G`hi&$wzrlFn@!p+i z-uw?R^U@z<(c&f8Rn#=B5tAlO#3GSTzh>=v@~wOvMi&fCIRgR|LL50_IEFV3D_q|r z^9Q)ZnR+#fq#?3|T7~v+FQB7SSOSH7iY~nK?mR3Ij@n&GL#u&PkFUf@GlrpVST$J# zP=H)2hchmC3-edxOJxNdBG9t604Nuz7I<5r7P}7#rr#H{e}5loo85$n$?m-Sx48a> zTglwt6DbypV!w&w1;$~b0Ot)L9^dnFDGChU(z00yzzqVzX|{eN)~(-wjT@T_TOQi? z3zMJTw!voYMraLF>PSh*UjR;?*y1@18n-d_r79%dXK!+GBviG9Y_;`0`8o36Qa=wb`zq9?(B0fp>@5#1?9+GK zkm8}Ux%cd|_dX~Da(dZ7cY&y6Y>u4+|5vh<**miJtr5TkPym*NsgSJJ$2XMT z8~q{S13)r?0u%}>8>9Y=w$azWERFy0?}Xqa$qoiK9XxcDru}mcwe8T8N`_XV!hT`W zURx_H?5pedI|Bcp;Fn<7A37mfX^~>#^!fBTgorlD#QJZxP(Vyyt)~wT<5U?cK zxP3dGzWRCtb?-HZLNU2w@|X4+dZC%Ho`PTe;3`N~>K_zItH0Vn2(XbPh@WM_!sY6d zRMwm5wgJIWw0OxX%O_U^<8YtXQ|mU}$iGCSwAU-2xLUz4ufg9SS!GCg75ewCA4w)K zQ1CYLrTSJP>&?s4@(KY=LMtvZ5iybeeEbx>^X~iN)!x-a6<)3yRf?%k4Vt_umIJ-} z4W*&OKM>0q?7XPU1mVI(D`?T;mBN{`=ZoL_$JANk&kY+nj{4y1-fY*M>eO#ZQ59=b zP)IbtWk7fzu7WKU{1OVU7`k@xd-_GMSfw7J7aU2182l3_P4(I}Xp`pc=(8y^bE_C! z$eyDgcJCEWa(4#)fA${|gRp4vGGg!rm##>aF}UAo+l}5GK2}V!Yu9~Cd-ok6MD!^! zF-gQM^nMdW9snWrmo8qqLMRs|qyy8Kv6JZaH@i~Vs93L>=v|lr2SP-I0sco(7-!tz z52Ma+^?%}jYt*a_VZWAS0;#j=6C~e1Db#;|NLXcR+M+#;9y3v_)zPq%PP$o+fAqM}5_a?}C^Nf5$6Hn%bc;z1T{zqc4pT=NM zefZF$wX|S~NH1Hoo;IEL3 z5KS>iR?^cmsCVC?vfezi6iH5}PM;;lJu4J8*BnySN3YlfUX9`0mR<3Y$PY& zqZX|?N;?>>GPYJzLbDKJau+=Qa_$o3FqD_+$=?h{Deizj_$n*~??ckt>NT5?OkBNs zood!?F6|(+TI|t_xCoOwPMLx zs%z%`It;_$OG;kB7e~OIhHVFK|Bg&X!mt7n?{uA-` z#*he(qT4d9+pw8NqH)b%8@_JkvN9VUg)z`i@f0m#AOK#3WP~F}O)&h!g!WO?>Wwba z4k$Z`0IphUn{Ouov`Rue7G=rNRqDx-{F4Ev!w~qQq8IpPLGWYfFe`hQ$!i29<}@ECm@oI{DZurYKvd}O$Jq|q9M(gag(SV|mM8O0TUNc1?1)X6;Bo(u<2(;jwIw20;p!*LV z(D4(e=<7A#(0iy?uU)SN)?qy8jj~hMez^ADl?kX$W@s}mK4*=ZwvoMv=W&x^H~0!P z=XfS<68Hynm>qNqEmM^uqid;h6yIc zij7NwZ{3QgFXt_x`i)w9LqA6c+zo5M=M<|@(+XiBxDz@hy;!Vgihh8{(YMiVs7YlT zH*Hhf%-N|!k1HR-&!8`SL9rUOi6E>7<0ZXdtOpiD&P3eK@5wG`XAFo=#fTM2dtLLO z6JCbx;EV9jiVUdh59kAjzzj&TiJtQ^@7+o(0X;Tq_$v{!w26{_>smLf}Q`pkK>>-(SR*6nz7_vs|-&6QTQLek-oH_-=A zd$muYI4l$%UI$oUa4gxF)^8D&sj*7t2L1J+UcYdZi0mrJE6q`UV<%QvZ;~kkc#45x2J*B zU~ky|G2F6|vEtMli4Z@}2jeDFNLao}`nm-}6+5C$0##vmc;EbHm03@%|F2TLp{zGt z3X26s(4M{fNd~Zt6GdG81`bz$E@`is0X~FX6+44}z{{{I>}B#KAdY~38aQoM)|)Ox zY44&XE6HL+PX2*|N7SBq-Wbhyr2{U9K8oFWWD@8N??Ey_acb+1xbNu+AW#@5>y5YE zp~F6~w6qrkUmVNb>W!}XwR+P5H`{LA@5Urh1-6Irrk-VY?8Fo6x&up=m-Pc+nT?yb z6^&7?0F#aw_$^y^$Ot=;nRJdK36z_g;Dwbas*TgJ4C5q!oMi_9qnzv4@Se)FvF}pZ(o0}%^`Q0?6Y^@K{A@dM}DRz z&D)70KO;siNx zD~z^HUcXsrnS(`PI9vvEB)y4V;go5!%^3Wm`wj|rutq>i5+IQ?l)oL^roqrJDk*%8VH`osTL1ElSiwU%W zcV!(+b`zCQx_~|+aOrE0zJ8f-B@9rQJWT+9gr(qMm@4T_ zww3(`43qv8pSxJ^@BHouwQ-9G2o^74y9z)jya@-u|5q5#P%k1J57Q*Q;kFQmr--Mi zY#`Vt*6s@zucWpedLrU4bs*1`IdB&YR~TKF0ugW$PXc*=_1_VhzP#7 zG-^$Ti#Kw z4!#ZVN%~=s&azRll$e-A79#%s=`-e1h01lMy-s;F6;Aic@?BZM#SVil;APGeaP7uT z+sP^}U%o0@bq54{UGBdVo`;P%qt9ftW}|_yGTZ~RCH;_CF4}2X67@&(PO7#A3yHXH`0z!XV8EKd|iP@@z!%-}N;%F`u0fz9L=5_Cq4nt-jlNNKOV zhu(y5aZ-;YXw_83VMBP-E^h;~zE~iFVW^JtiW>iiX=ylpX|CFMMcQlbp;>SXETb@T zp`MHi{h&V_18>M48yGWg5&@LrCeqvQyr+9(j9PVDAlES98>-1AQGvrHR?3esn$1fnaBtM3;S5>02% zohPF>bM_ZCwjk@k)x&hS3>Hxs??j6e_`{Mg7&eEK;5K*x-iMjcVf33q-vPti?wMaI ztg_2o|5dBk8TQJ*VdEAGE?rU9f$OPGcoBBu?Y(@27Apvb1+q?68w@7cTGP+14A9wxxS@I{4TTqGN{H>x+^ zqfc%$%S2uL3Mv!f-cA4xHZi2FckZ=I&eKT2kwVe6^3#16r&jEFrrQ$z3oJ& z+wmX%9b160Zhvig>=OKroO`{w9e;=F)Nd*4;Pup0_%YY)Gj5~4BEkrmYS5No9u7RM zPNzpMDh_?YOGZ$|s>qsLBA zV9D~b4(8mL0}sGh-ri%(*a;Y*V;n$a@?~UXl9?nS%{az8I{2i*8Lg@W6jK<+gbAP^ zET{MBL|eASkx7U^VxRu+$~vIk(FyOu-LL^a^%>L9dJjgz!;ti{X^VEE>dp*Sf4xE7 zQj~RYyYd1Y3`2R~GnSyW8T&x*?Z7Qt-n&l`6>7>lz@McP&W6v!7Ziq(0c|%R2;PLGr;VGnrSyyp(vt}v z*)rQN8yE?l^T>_S_e)8~XY;V{nXW26Ni&R1YKybDQ3 zMFY+?Yd4Y(*lEu{8=i!tU>N+f!Z0$UWd<%b;4MIO0^YmtAbG;cQ)lha&DROf!am}O zuQgy%7)Ex~$TWXd4*x@inb#V4zEHoKyl<(+|KVQHw+mzM#fkDOn4M_fG=}r zUdAiX@&bmzEJ#M^>tB{;%$o03%|Jpzl1!2R9C!wfgVFGhu%N;)-h#FxPzdfZxMi3~ z=e=>`7CnpEa~8_*w7(5kz}8Sa={d5`crDs8pbkueWT2v;XW`@e0g{>e1(c(JpeQr{&O|r> zzM?P;^FJAa&0vzG`|zHa1Am2IaQkhBc?w!S`(XG`(t(^?GvF@Rh68&H<9Y&k4!#OM zftivHYIk(PORzKa;{!GsCU61SS1}hB17#0)ofurDk$%T7{*+*9fC0U1x&Rtvv0J* zFpT+V1w=ux4g4KCO&%yN5%o)gr zb6}{#FpNBCHT8vHN!SCPhB-P9{$*Uk%PmIlEv5H^C#VSFx@2l22T*YY!rjnT?-17IL5346os zFbO)I@#;Sj{-44yjCY^~3(LcDuq^BXSHKG}{gGwDSy)tI7{)u%0){>?5C*}@urr(j zJ3$|XVHod6ebtABVNn&Q5?tduT~UcG$iz(;SN?yLR3U!Ss;Odn1NP9_u&h?SGiYdaSU38h8{~+ zLrZh54Xr^C^nhq>X)0<7x!r@55*YNKKK#!4AI|w7a96UbZnchqZWPQ&Fd)U^D^mM2 z9{TB}l@VFB^0VP^h^F;%iwoe!FV5nB)ukmY6=;HQ|!5o-L{XfD7;> zjO8Vf1A_!vV39Bq3PmzxDc~oAfrLebAb}?R1Bm`GMHI4RP?4A?hmMTsNBqI>*;>ho zaR6MN0JJ^X_S+D&Z$q_g+nzT3x=UdSC zke5vvfEK`=Ov394bj6|P8ah|S+ftDwdZ?&UvpV< zr&!i2@c;mHKS@MERCwC#*gb0$VHkzsSBap4#&}llBudDNM^WP^8ZG>4RIEfT#Y*uH z*a_O%TiN*o1REmQs)57P4 zFAHB4zSglT!WV_l2%i)_B3u`)3GWv^AiPHSVEmtjx5n|_l2Tfd&^HSY2=_+Z2ycrh z&k0`_zAyYp__^?V;m^Xqg~x;^gr|g?!qdVn;h8zsisMb;sW|VS@E_r?!XJcR2|pHo zAbd;slJJSRuI<8mgf|I0uD!3MlolX@iKp(C*ib}uLHJI@_eS`8#NAW`y-b@Ho)rEo z{7v|s@H63u!Z%7vX?_v;uyDWd&iH&PVt*q1Nw^WA&8)J06y94>N)bF>C#(p4Afi7j zd{6j=@UM8HwpM7=i`f_9!zHCEi|G4eBk^+Ww(wKoA6*oD=Y9&;N=j7|(bppS;fVgO z@N3~lmj>UthH!sLsVdr{uL~cG=-&$euSoRm8yDV}?*UR*6J9Hf@SyN6;hVxQg~uuw zefuWkCRi^ibsOQ0VwRn=35hC>qWBBL7AYAwynqQ=g+Efc` zVXGDiMNm)#Erg6#ZK_2QebFum5rVKNTC{2*q3QSrOOnXAZBF1q$g=XFJ08)qd+I4q48IWqFfZ$MKSDU{){VtHc_ky94FHdMG1|+UKCxT z7|rsg-rs7RO-8$~HC*1h|boxIKMNZ&2DHyxO_PALlw{Z1#zbFn&Cq`wjh+@D?{aK9ws{+TFMW?-hbM=L6yyeo^1-#^9 znP@q6Fudp{~J-{2afXxqc7m*T#R}5Vs4KW z(&T$Z(asb3u_#`1);{h=PFsB!MRnjf(-=OF(bqDXy@aLvZl2`bqUdEv=9`z`8L2O# zC<>hHhNU~hFiZxD7x4P`s!tTR%uKW@xX~DY}yrzLRsFgn%vtfkRf8k(%8rrx$!#BFt~EG55(P1`+z6AeVMgr+a$Cfx~9JQYRKPx-k~ z(Q?@4$_i_)J7i}&PFwfYPJ7sM&-$PD*uaNZ_IYT~5{VHD!_bn+Bo$g1PNz{zj11e* z;3xa|uGd~XyKi@IT(Zj-&RKhFt?jG&#ooEaMsZ$o+z;(TUs_e70+$42Yz#O_3!*71 zNlBa%+JGt)B~l1&LrP21T#)QQnn06adyVb&?s_qpwb$NV->>l%-}gQ{d%xLhFH2CJ zqyl3b2vYjcioJ+)z{fmbd^W8rsR;9>x@0rxdTXDm~FV7Rb z@=Tw6?!PYsyLUW+!na^6Y<)nbf^sO)MuKC-lU$yiAu|($Y3y@3YUMqUb0*=3e-{Ubx*N1eX&I z!4A9A2Aji5*x+#3AqWDA-h-guk8m)EXgG|qNQ8*e=P1G?=3pQUzb^!jCje3O!Rd6t zYPB;q8~L<5c+Be!@HOOveFXynCK3uE8Xcq3z=KuyXDnvtpI1_3TlyxwEb_-hrbI3g zCjKYiR+~Ihf0}_^Pws%W;B7dEIw87Uu-Hd2Fx-dkGo9${X~+9LZRqOjK;KX|hOPZD zJBDF#j`DeCn;90H1rCP;qDzF|>qAHq?sz1E@n{sGV1xzk_lFRRO<-zj3YRWjnlBq> zXJ--h+7a=v4_Iv$@=bopHw7BbIK+`o{A9&#|GqK#C8A5*q$lRqOKE;KQ73O~k$fJ= z+<-U!_E!vc)Wc1pAGHpX*gMeJTn~e(2GuoH&=b`LJ!*{AFw&T*&Vc$x6Iwf(&@<4< z!NB6YAsATAJfr)4KJd&Pj&K^ENSwp;^fW#`7lC`Y4W`O+9Lqg`7oU3uPd&03n>KDi z=Gq6*c&ZRqyP5nBpl`4nXNG%VwvWK)^}lA)n`g#bX zz33h2V&64JJ^vrdJh%+Wd3pcs{MOHiYPt4bVkSK=pL)Zl7J0;n@_K#ph5uyscC@wj=$?&xlVv9<=4RXP&78pXxMw?wH_LakQg)W{uN#M-Q0pR$25TIF^vxWA~7T6pX40N^O zwHNmwb4}(Qeb*-*eH6Vt?NBOy|1+B~T+Pcf-0(I)SVP|#>af7X{!2~J=I1|uS zp5`WCNlD3kAW*ASsMPCtg}`Y!gQExkg0xkf=6@|bvhe{lH&&ytumA;$JmMHb0f0+@ z!M;{xtzEYeUsJ>nQe<0zC9lT+5%Jh}N%*ujjScDPczo+tJhyu%4j$Ntyd$}&R31US zu?i#YIylTdu-Pq~Hrwa1!r`>d;}ouAhf8$A>+>KK3LqMdAQl_PWPF0s_(_Qg#N#wJ z8AY5(B*t<6{3Jg5@B>`D_%F=O&5-y%A+GUyyVEHm5{h6f9N`;w1VNC30A~Wc)YjBe z&z*N^`41EbxGbQC9Ip^qY@=v4>ab<=FTeHxSz7oGG#V8YB>wz@V+02PKGvQngP~kO z=Pk%zbBb)=yh)9{G-;o_PU1gHRjhZy^3)7$d1yVJ-uYV`eCf~7|bM~$TILb0jXScR$t9G`wcDAT- zMg_U=LlA<3fP*?9Dk};qsEo=WI9`L4I3V{eFd#VG0^yV^_nl66(z*LcA4zw}K@u>4 zfIC~YJ5yV=GqwBf^FQ_F@9v*Jm_Rx*Tm4r()#<-G-G9>kJn!+n@B2wPNbgjn_xUw~ zYorfI&4qvjYpO~5swxPA&jI4QK>8|a1nDndz9?5f{A<^)paJI^n`%^(4cFdFO7Om= z4tfDf1dt0T5=*{Cg#$T~gPcH8`Z+oA^-+2R0A25_+zi$TEPa0-3x+!Re~M1QadPCy ze!Bi^HS(`n*(v}$mw-g$TkUfHiI3u9N7o%{taT4y2=LYar@iI1<*NztkI0*|o|E-U z-j##{JH?ip4C`DV<+RRD@%lUW*ut3muA5TM1c4tVhzZt;1`B^aob`uc!0HPwOqjf(s&wEQ=2To3QSW0hTVb2D83 z60H=M_4TOEV>nP=TBcR+(Hl@gzzV+{Yo=Cw!x{m*%_=WEH6a@RV`TOF%7GoL5N>QF z;Yof90IfgV{|%CU{;>4z+3r!_W8z{*^beKMzfWP^UwhSFH{js`{p7`|ljYNo-jM@e zZkDX{vtqU8&@G2cPSU$8T_h+-4Yymf%mB$ww>%s;-};;&0YMJJ$xc?M@RuQ;#=@QCt9T0+2*5fC zwbw{WtA2sx45(P#5BNw@9RV-T;W0BYAi<5^f=>E~*Jhj#^1DF(GP?b>7yQ!D5Y((c zNPQ6k(2Sb;Z^8uNTM(XJK!gZjHGmy;=*9!VOA!K42*l&25QxX+X6Y_E{2($xlm{P? z{{2Tp?K2oRCSFdRN{|g3)(I1TjQvB&zl|H#OJd4-dHV6k+WVR*aj_$+#Q%|s^CtiW zz<=MX1UCV**3%|EDvRIyl_c!mD5*)OBrPLZGBQ&oGb@ec&$DEQCFE>$_Yt>;C*pLX z21C|a!J|;gTY~3xF)(@u;a-G7c!JLhXz{)?lKk}nTL0$edb)06Dyv_P*I~>rb+uvS*R6jOv;Il|fGY+O(87URgn+<(y$cWE&o{4MkzK#rf+frT zQC$|@_}NEV6(Ng#VC_MC0zZ@dtUpLfIVZ2pe6b@Y04Y!cU@tCU*1ob5@wZks!|7XT9xFcE^A0G9{DM{!XguC_GEhDC44Pkt8N&uHPR&q9C`Wh-h6)~_My z2}OQX|E*;QV)EBt$xqwA>QEIc{q0=h|Gnza8wn)d>+AW0`wy14XHJpbn|~{b=TDOG zu=Y9>`NOS$gaBkI0l->!Laa5cpXu7;3_M zELku|_1Z@d&rh8ud3jlEB-(%&f6dAjvT6N#*|=_c0%VqVd6)gGv9PP&^Pal`$(+P5a`>p?9Vtf5{TGI}# z{xBf*9!&DlLOnWSh%9;kZ8>**H!U~-GN%wAHVICm8=;6#WYYQzk)?zHJ?Kdgk|VoN zjfNR`^WT~)v!+gxabw5Hh=Bv8&qIB}T~cXjVJ}2nhl}vJ>F~p(8C7*66EkvKB=?ug#hvkB%B8Js#|P`#JZ$2gvN{ z7@dU*Knt!T81Sitol|OS{o&bp5?GPfgtRR>jOTIN4vS8-NqoE~&`bf*#pE(YN4q|T zB!0hJUZ4GX)IYmPW8=l1nZ9QULikX=TU!j85&_*3mq~zPQ+NS1x}y zmFzosuTA3_*r&gI^wuo&vL6;J6Lg{4dAR3v)pIS`x)=ZygSG@A_1qEJxo#20P`x1W z!v-5-UQ^Pu3FYnAXUoAax3I~^=kx0I_=x>Meut6#Bp--qOw17~E;j=o`#val-Y5b( z@fGQL9q>Sa23{Au5{4#$fHs}iC9lnnzU7}bajcYBGQ?V#PuHG0a>kGv!g&>{>8Yce}=z& zWhSc$F-e$1MKL{y?*EP*+x5XCxFz|qB*@Qj>t`Q!^nEVGs{ihc{Qm&l4=e<3-Ydi( zH+r;e+qhavZ0TBK4eRYID_!CSP>sii8Z^x?GjAdW^a>yVB>&O<+hpGC88Wc%pmx81 zU%2=mzVl1TID3%qf%jttAeaACg`R52;ZhwNa%#=Wa$5|c z2xbDHlOI-JxA+huCLEDaWWNAb6hM3zmn(e^o6LGTy5xuZ4)K0Z{B8%ykHMboGDNE} z28ry(Y{8uzD~36PoI-@|L(UhlWTi{LK7%{@e(U05M~BJ(pqjaV;9gnYn=odye7R+{ zl$8~8&D~$cy7*8NO_fi4s)?r3QyC!!6au@AOFw*D z^0E@qFjLGWKd$=+s<`B*>hdF&{jk>0CA)gnhHFxm3y}O_#W?yJyczfyp#*_KphUnZ z07mk|;w$0@c}?(4xU`5G16Cnc6kFt(Nzo<$0Eq3m@}tH;n?I$ls!qAHu`;$I@bm_|>dPp{l=y1kedGlw-n%v|yOU+eyx7Fw z_7ndffqzjGdg^}YEdQvX!(`XmkLlhA0#$ld-R&A^h#G=|0FMBnLWZpQcpm2459shU z{2a-q^T+p!+wF`f?oot~=!hX=w37aH?-wK01TaZ|7IkAJ4Aq9fD(Xj z!>s=;a%oN@(>bDd~AHwKFi*{hsd5StE8c}hTDYM3T$jEPR&fz zvi_Vo_|>7103Z**WFY1Za36vk^knqyusQ_BV_&ZPsHP} z3#}=$=uTQImdNUL^Lnj3Ew8c7Hh|~t+_GLq4jOPrLp?ruf4(>!rAmBEy7|mN^0PUI zCBJOTt*xTvh5H>I|C@H@De|`q$_p?GfbROYv>3_nV?t6{eeM55QOiy|BJuH$N3AB~ z^_ee7m7@^T!$(WMo_$Q$l6&?VDo4KD43hXr6bcaI zWCz}lB1EW09>e>yW*_7)qm@mMG03qvtiQWd1 zeUao>Rv(@Vy`!WP`()IR!O>aUA3Z{nPaQ%j@ikn_Wo=VXwi06tFC_rkl9YCiRvu*M zyg_&Y#F2zUl9rXieF;9Ee^UqMiG+BXRu!=|>*C?k8sO&nU=WY-as15AIzmWlxaaP2n-mvXB z)YW)+JrFmBI5pK(EcVGd`Frl=mv7IJ%)C@?AI301c>)jwkUyCNNA_)9E%)7jM_q^4 zfnIU3(p!Tb@=;uoZEU)$z!AA<>zBYNb32s%mK6x8%@tik+X?kb7vpU z7tBkSq9VH_pE)VLdJeqPYuW?+O{zJU?B^ZJEM6K%rgo zteOA-@C0%TvN&)QI}y?H+b!8La>&p-y`DeD=Ki&h{KvYQ{M~@*Q>TctAe|&vR*t~W z;Ztb<<65_|*hMFA1dOG$=GApKbQp?hZd*_TA)p8#G+=#yu`OFZ!p^zF1`L#LZ4hJF z;9>Iqyf-92?F8}~RU{`~n}3rD`DyV%dd}*{piTm_`gk8xdME;6zoUl%xPCne2+^X~ z!t1PZX(GjC)L*|o1J~5`A>NV?-<50FdC0By^)D+e6-$1;Py}p*@X#Mw;zi9eR~g*{a#)v^jg8{<`jO;8p-xE|ihIftmwI>}Uvi28 z$zd)CN<4G|8*qb<;W;K1b$>8{+3PAnfLm<{M;IOi10G4(xdz2S?RTReM6=STA1;s! z{!)4Yc%KL&<8P*w2hsU|2nK^3$K`N1`M;^Jucr{e8_-3dEiV89!0W`aA6@?kXn=7K;Nh|K5kc^zF-S43*t>rnAE-2YlRdu$&Igzn`3G3WUwrLoq_K^yA&)C@jm z%Mr6*5Sz^=nVH#=n`;5`B`Yfjw|R)=ee?*FNSb&76aeNLQ0B|fBk}7589A6j)unjm zi3#E?u~3j9z{ov#AoLFLaOAieFzV#W0ILFK7n&ix`hw}z{A~X92G8j2IZVnUtV6pFHb|mMSk(a z5@EUs5G2zrS#mS9DGus1@>fGxugLs)^Y8Y({}UJ;7duQR_5KuC+Ld+v zlgE#j3R?!U;6b>2+;2|ut%&>bErbQI+H6b+m6nuH5a1fpB6ukXx_5xbTp&OJt%O<* z({{E|aeG8T4G2LJ(@h4k7*DH28t<#Dy3wo9VJNdzuJv_Y09@rZd8m7zyBkl|Z=kd^ z)lj%_og}o+=cmQT7F{4p1+1kL;^!jpGljrrvx~)&4>4@u-xKlz{+-u)9ipNo|LnPm zGJVQZT|R5^rKca4g3MD)l;T?Wd4zj8xMq5G*oBxAhxvJYdrf}z*1WrY|NjNPh-LoG z1>m27H-JBO(L^3p(Jhe18ZT?Uv7rQsZ_yGy1!1reOayV)86^mXhzm=Ki*-TpKoH;= za4a>6d#zclNrz8 zEBHkW3oX9i3t&Fr8bbh2g-v?*=yTWGhq;S$C;*828Sm%h(zK0b&d4F7 z-!%AS_B47^cZ=_U9&xe5w7hq67nS$2=KJe?TVSc1HFuwu-J)+PMX&%Q92R@%F$x1k zfr6mL!VDJ>7LdS+2e-)Bk)v+g6cRu35y?D%qIK6>TFlhcG!UqaLI4qoB8>>N{#L7v*h z09^oFnTKTh#7Ekkl!NoEevBX3OD0ZyQeJ)b30d&QOc_7)&c$r_kdgBF+U4T+R4_4$ z&?BtY;cD3G3zM>~f=ch8+FOCdWT%k-e}Lb{#g1FZ-$TXywO!J+H+G^p3p2EAFOxd3 z;3Tmkzao3KWLq;NBR5ssZI*rD0E-xpZ~woyt6WXT&Jx{)B^t$7dwnwbFs58uPeIxUVRRk?HUh><#KZSD7x`^mb?f7 z$jVQbl#E1Fe4UhICllBQPMjdv`iZJAJ|{arokCzm55L#rl>xmwc1^1n&8LMgMF^6S zktue25xodle-a0|1T*q0D`g;m*h&EM0Q|C_IC@xm_3F3?j2ZE;1e^tYFU0c9ZNTjZ zIZ1woO$d2(KnQr=AOH~_0Ir{+g=;3^KtR?jz#_930VUC@Xvq&@Ou~@T=X6h@8!)Ut z;&+r|ub2}rFUEI=(3)?AI&<1na(LHzWbaF~+5)v7p_AptYTOw`Abj2-JOYXWLIXUn zT9Qs3l%WGz@ZI9VbX6K@Rw*Oem{Q9^vG(3eiCAr8_Y_>i_3<#WKUqt{yv2)W3 z>B0Bt4A8Kp0)H15J0b+&C&0{TC;jfjmwv;V74prcdJ+f3VxkTfi&=b-7S>8_D_uYj zfP_Z#M+EuV1$OuVfvGUOufzZ%a7^2Vnp;pOY+EM1dVjy=xF1_qZCtWY{_x#(k}MDi zu!sia*WG?|tAE4H>YGB<1klpe*ZL&q{6YD2;T(Bt!gv|nrw@tqAeP_y_v|eb9vz2{ zw%KA$KMi7DA^AaGwH0>-+FW4eaIjn2D!6U*4VjGDNUbzk^qS0dcV-g8V;Lv42xkM{qx&t?w_Ri>cOn zDF9_<6@k{GO7*G=0SKgjWw;mn<$!%-_J;+-^i2ywTwv0kRm<+TcU$6)hfJY7= z6)}JRXZTq=_rcq*Ntq>uB*PLDi4QkhC%>Va&al*HAbxug8or^o-`4p{#N#Z22$o4h zz=4qAl156ERQ!&f2Y?ZXr2U*(ec5&(vr!T?UE3xqFQ;uNuIZX(?5On=XeAP7x zXhOqQZnRM|Kah3)D29!6_M{IR&SV%!hw)=y6CSc)%l07WO?(CtJL zieJW>nhPvif!nIQfO5pR*48k9Lm-kf&NJTEJ%Ga$0R?&j;a&heh8$}KL?BOFRiL#D z`n4pPQ_4~giZkmdT21{ZF>cn^{t@f5qRv46+h_UPCcar;0dbQsEk)KFoKztUt-jOg zRE02Fo}|_4vocQAJ-7+nC-7K=02CLOu|g2SW~c*T2w*M%EteRV!!_r81-R^LT9KP= z&+K5-ryCK5ZO1t}47U#5;)icX#?DJXbxPv#)@jIY+4R{*hGc1{x&DQ~kK$s-Ev>z; zN3qz4G2)3YmT_PQHj$tO1c~&eD3A>TP|TRN)e9(YO$HX|i9ja$kOzne0|=e?1bf5J zC7qURYr5nY=CF?(S$)>33%|xMI1NIph zhIZ)hML~k4=DK}hVG%4iU49CrPJFj^pNUffC9=G)1c12!B>)r;i^Zz4YOki>mxV;pBn>Y2inq>2Oy{uZpmmHuMzycq70R;dD zL8Qr95c}AP1iJaHNH~6&-oXjg$)7NnS9tQ2z$APj}Z3pkyXESABN z8Z-Z}xa}oB(FW;_cWdyZ(>8YY&dPCL$EJY6HX#leLj1r$j4=)` z^AHaq3BAy3g(M{HYF8@>Nr?A*@FZ?#oN3cIZFBnFZ_fVi-piF(Kr4dLojJ48u10(J z?mh4Cd`E)*fMwM9h~N2q*l7TWhV=QWSkzPykjGfCJr#Jtm7YLpVK^LwDkM;WXIC)_ zkJq?()NC8JtNT%#j8stVr%`Ap@T(5<|4+l~5001ee-3#jq4&6Gc}HK!$~Z0VK)yXd zRxX~$un;!~Ext0_nEY19S?Qh7s$zc2D8L_J=w4C^On^ND&aj?XfWmC+k&0eiR$wv3 zQiuug6*`rlh`}<}7X0zF09;FU3-JGVeQg9_blZ~CfM$Y`{04>`^n`;M z7)C&8)>@daLKsEG^9@Tz#n~%c$-8-~* z^YeE~%Bh?NRD8TQQ;*8Z{Thk*-621Irt&)!-(>*wv(W(H?`hnaLbSKHo6%+{JNNaU zJ#~bN*r}4Lmo}_MX7g~HEyt+zX1zEf1~;Hb300IELHPzOXRveq3c2f!@rurjtF%JC zkbW#`YDLlg@5L;snQ${Fh@j=M=ydKD#`#{h#vZ&=S9v-Z_JQaYXSAObu4M$U<%#GEfS(tVY^UZs7>{x5)e11<=VsDt&!@fCLjQEeBcb z1GLvL?6`XSTIq*4i9q-mqtd$--Z1t+={cl?b|V2g$kB<}N61gl9S+5&{AvLBIwFK< z@6XynFc{){{|484+efS9hg0vC+lQveZE1sW-6qI_Sx;dt`38JPE&PsPq^ZO%DmAly z+|Z}l-rlS&^HhAc6`4-RVH5(l|9XW?xpS<7hT<;2gN#q;J)DOHfrLzoQ{fLFkTDjM zj8=q$LVo*`WVdx0fZs$f9Wq5;wVLSB7Tb6_ei1zy~Qz0Qre4q7nY$e%bCa*&h1Q(A6Scp5cT zcBU8TGH`mYzTS@zzlNbX1d7kdM}a+;ul#D@)cYf+*4!V_4GXNF!dQW>yh~u#J$H_m zyd4`5Ei@T6V#fkdReBq+bF}kNU#}sI0*y*ruuR^2a{;;idT`k2kC2xVdJpHJRNj9N zDUZWNC(TuPajpgVWd9fKAHAWDxeWl7NJDNu5)$0NoE%CYWR6Zi3!cMlKMh3o<~5R$ zX14SI>6au;l{suBQh6G!dcz_9?dd}ysVdHv4J(((qPer=>BkIbrT;e6S$YLQeGZIh!1Yk4!nGa7yriUz%8r5@6&?dR4E3R?3g=ZsUt`#<{3hr3R({XWpVtuY&(Oak zP#gp5khlnN4$PKT2H8W_{dQCyo8H>;q>iyqDi0KgxiAWXEQBFkXL0N} zZm1tWcn{&a_I!Mj`1fryZMD;f!?I`NN}2ualXAz1bfWwIp?m5XB&_)EZ6oA`A3Z2* z-}#x;`*Io9B04ZMT@Z%&0PPS)3?Jebln*W_Oo9QuvGLa)tvN&5FEMEF`sJEUJi@6jpbca)MKN_R?HO1jim z7IEMMU=Rg@v_u&Iv*Nldj(a|8ubiws>sGu8Ady3{iI-pU@)I&M^KlvIy`sCPS#5Y8 z&w@R-Xvar#0WjV`fcd=Pv4cW81PW}#JnwfXy_dwI{KOF`;78%s=OuUB+RLK(S}^lT z;R4WuTvLYsNkd?!_lyBB2K~H25jKZv^kS^d4QJ{#Mibce4V#zkH!E z^lv6e#DV$l?56Qva>*VwC+B`PcLoPRJZ8^(nzQb*Lge`y~jn$|2InXajv^B`UEUtY;0^G zMx3jybm&7Tz8uIfU3Kj;0;~tH-39^;&VbK*@!1$#MLqBB6Z_#ae|wBPH+{M+TkwKx zTDJzvGB-(0&N?};Z#Rp5rTb6=C@1=mElB{HC;%i;${=7ag7}D2K+BSUTZK_ya3auH z)YcrJ!GIBPw);Gk9~cUz9#OOYc(hr6(SzrAQIfMsCZpot)wTUskh13Q+(c|YAii8> z#wk99_d2!~RnhX!E?8jT^~3N3<0j@jt|+iWI$B*0|utY^Aa`Tr65djA25GAJogZn}cG z@^4}R>HQPN$}it|RjT)8Bi}zrgN158Jucde%3d5a48}@ks!J?fT!xeqf|Mwww$Mn> zIFR?AMgY+O4FJ7Ad;JPy_EY&uiH<_4$@$gun>l~Iwlu8mZDBC~24&~w4<+;Q$7EP~ zhV<*xpJ~;=e&3QQ6Yh}R>)+LhzGi!KadUvi9s2?q4FLHU)!JoB=Ky!Pk(6g)EZ%&5 zj#W(UH3B_pBvyKePsKI}4nwf7q=a3BN?)bJ0Koe*^tXXN zbu<*f7#NtZ_u4PQZ>g{_#QdHxs>_tEG31w;vRo-C%#k`@9=^*EXa3s{wd!@@;CcPQJ>H{(I_xpL`iypdS>ZwXRx zqd*#s*b`#`h~G8_#F4k@q*+)#djDOy>E

`3oEXJ)e0=cmelA5RxK=afI6O_v#& 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 AhyVZp literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/stride_3.png b/res/tetrio_badges/stride_3.png new file mode 100644 index 0000000000000000000000000000000000000000..a4a85f80231008d3375b4f76f0b50daa25b3b242 GIT binary patch literal 28903 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

)B~^|Li&c?QT2252)9>|KOs;$gJ0sffnP8+6|&2 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 P00000NkvXXu0mjf39V6n literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/uoftflag_1.png b/res/tetrio_badges/uoftflag_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b6a51dbffd9a66c0288add897d27b4dbf15fb47f GIT binary patch literal 43347 zcmXtfcUY3|_rGRo<<4?|BeOK$no26}QSMxsm71$W#f64@n^{UJnprt;=Bgaz-jb!L zx%Xb+!i}OJt{;8A*Y6K5uIJ(5oO7RjUibaHduXD^dKqw;j*gDiKwtY29UZ;Szt2TR z+MN*JGh5mhz0V`P`*fwf`~=#?dFVakdvtVV3CzbYF3_$oz0|kzp`+tO{QJ-=BzAkz z(S2Jr(7yLL&~7t~>6Pu1k^R(E-}_h2U25V|e)AP^@#~HE2ul!Rzvk7Zd0D_uo{jNe zuP^>b&-{L=BKG~J7UzY}qL1>D&JmoJ?cL{^dZ);}sKusEQXTWbR9KeJTzZ9%eM3!d zVWC*n>m*b4iG0AHw{Mww{9matl6?C`G(%n$L~dQMWuhvr{=UG>J$=zGSC_S2hsSnt z5IaC<#9m|q;LM&!(4Vl7?)FDs2Wva9t?KvlWv_D_`Sttz4u3PtRU0;jzlnmzWCv&- zUt7H{>&bhJQfKy+-wk|fm8**cKU8SrWC3gcG>w{Nbt8LKU0c=rMqIpKVwS5b@ePH$ z_*S8{;-0jE)|t#D-%D<4B@f+)zmQ;-H#nnLhChnnP!elvq;{re9? zng!2z(Dm??yAPlDptBErn5%S=fUq3T_aYZ(IWR22&3ug9qaGma^Wjo6NOv~v{(0K{ zbHSN>j7+1xpwTmlVSv2bV>6m;bY2Mi3#@HN;m4?mOAyb$Op5YuaaJ_`DUr*qrU=X> zK!Dode4B+ifIa&&JSL{YD-iyjQXNFz*;mfgCi|I%T(~KE9VM6 z-ad}0W^ZFCRIh&bO#PE!_;Bp>?`X>2aNQb%xbZX8U}UWMWH%6Esuz5e%Ek3!oW^)44Uk&Bt z3Q@o+XuPN)Em&+nT;0U8`az08f;3~_d!x`Q6eZ{!4gM7!IAX7!#->;c2O{FDl5>Q^ z`5)yAU-pRG#yKO_Kujwb%i>_iWGtZeI_ z?tVs5qgA7`<96s!_+A!)8{E20BX#bP5UW|rE0e%qANzS#2D{=XL1Cq5$V4H(h*Hlu zc`Wlv*Pm%*V-<_wSjyKw0Y*Gc5jFR{gpN)6FDRPWjTGu4LGNh3$I*Ygn*Qj5ON4A? zr0bAq0m}3E!DJ8_KYf>u)oiuZaO;F87ewU~`T6|xfPiD)+IluMKN2d{N;J7mb9F2? zO%HUMQS)TGaMsB&7T9wt$$&sz?=_s-8mC-_tfZk?jI^$f^OFDY;kl96z(M+{e6O(D zIhrQlW!YzeMtNgtJ|TJz`9e%4{M+3=7T7CRV*Psa$Lfg%GtU=Vs_f8rjk6ncxy(qo zJs3OUsZVe{f+aWmJe?Zi4#ZMe`8B-ap5$Uv?#*0PpwAKBdJzqlfnXieg)_6NXSWh* zB#v(Ya2HBh&(VaiP8;0Ng@pcXp32KgZz^<$;cxcW5LI`6XwX9A(y(z^mZr!&**g4! z+mU%}Np;#6&qgB9b<1C`E?>;A^i&Rj8?zPC-pQBi1^MxK;39r7Nksz74R!o4Ey*}cD1y|pJV&HjrcwY{IYxnlWE{k%1va@d$0&?%wu#Ks+%z;Gg+ zi6*>#mWy4b00r(sr=R$W(xaQi%+8#Hye(P|hg9WV_YFu_rCR|`1q zmptKum@^szj~)4VAcVBF9P2nQH@9~r4^GJg@XfODf}<*ip*N=|{!5(MS@@tur(w5Y z-fQ1b4nn_l)vw0YaEuSqynVZA8Fy{-CzGDJu1EZ+ zYV{p0*Etxp5XjSOI#h7rez>!4jKS{B9E_o8_P9I@eNP9zjd|If^`-K}FzyfgoYKke zuHj&-!Bl>H2qsbaY&*ntX1GecWZj5slnQ+5@0WHfpSdaCcbeSiL^zGesZ(+04|Pau zI6D&d%kRZB3j0m_h17@jlvj?4TNGZCQ1F&5S+2d*%3gkoCwcW(DMooT+M&%Luljpa zakXCWT@c4TQfO@dK^seaAVrF>f~g+|+{h`=FV=%>SdB$uSEcj!_F448AsaIsS+Aoh zcKSL=7Unm%2zH6fYU^l1q*FNL)R>_`0(HyY!Tti3b@ihYGEE@|z{~e)9APWeE!)d#gZfsPnJnu>VmU zZ7<=hZq03ir910M5Ka6NXtJ*Fwk-l22Z~SWBB1$}1Nx4dw!1H68`O zba*h<{lFDAz}rw*Y`i`e@CJMFhfTG6-h3uT;{*5HIOUufWh-3~Z>Hq@T{S8jX4BL5aU%L7PUEko-Nk@+5`2`Y@uuoHJWkU0}oE z;S?*ewtdsq717ntF=r{K{hItt{W<&rS1fr#*}~Y!c2(S6j4H72aw?X#d?K=*R!lyI zxz-7*ldj6O251fP;pANqXbHfLG6shL_r})QA3kpNM0cjb30WZYL)7_Z3*sXV3?+!+ z^b7RWWV4$Yts{LgEwAH|ec|~rG3U;>W_V;G zvI6V(lFluTjEutu_Kmn86y(+CQhbK@EP5u6U?1Ku4V0a3D@H3!@xd>tT#vyd`+K~@ z1`ds}8(4cOIo7}ychhqw=~Dqx?Ow-zk)d}(HMJ32@=RSlo>C6IXv zdM};kvbZDA?S_BXFE?EVV-H>8k{Us!nIDuxldJU*>N62U<*OmoPx0aM|&jlaR##;*OzzND^JXjM~t^*tjkh$;sX z?mfUev{o8lSq*(O>0vv-8`5u3I>n#i%3F$?OB%F|u>1JsxB-4pgD)$T%4pnlANW4P zfu_-5b^!7qTrS&r$3f#}d7%eJ24~c@7OffHw-8NC``l5(bvuR%U1YV4RN4)2J9A4f zL)8q2sohI`>ZFdnVMfO4HSN9|Azmf9N6Yu;c#A<&ZgN<8E?V!_g(On>w5E6`IMB~N zewm$g@mCQd|Hx0OQ4(OX@~w{Fx4(Qd&ZnV+~c3puO`(;p;@~hd|Sq(jRZ1IWnEOX77d-l$7)%PY`2jE z8mYEJR&rRv`upvHGnX?PFSdd8?hV}kW3!`ff08*FrVowI%C_wtpM+wk~Btgm^iZlKcm$~~CSicjX=2hwL| zsoV0crJvC-%)iE$X(6BVsh`)v&%^+QmZ(o^`h>L%U~_;tYPA|JGyUenNws4_9b_F7t`A_93jDjMNEQ*$*qqD5Axx&0fqmd*6+Z|`^4y(*-X z9|Hg{Fl580Nb)7;shn0Ss=c(X5^h$MYn?0XeFD|oMYHeo_>A)=e*)k~Rs=sCOgmWu zMKF&$kIXSuwBgGo9yD>aY{#DY;L^Y7cam27FO0fpZz3QNZi~ol*kl3uoz?U!T=PJT zU~7N4yQP&?6Ny_$7wnlpE!I-5^%kK;1%UZ7wPfIhmTT=A;F!DQ$_4x?!zA5Ac1;JF z*lK`I_V@nG@+zUoFT!>-X&m2!#;%sl+p`~-{p0u<9q33MhE8xa2`@!=2$`{5ALlsq z6KKv8rK~4^wjCOblc%+{L%-y7>DxR-y&1IKV3UpxM7d9_-!c zJq{B2g{9Q^usvYfRNI06hT&}Y`5?$?f`D8W|VaPiu8Z#8KN{@nQB68rYP|&Ks*d)-zk|3RRiFJc#;9QgZ8E{ow07{ z+h_{2Tr2q=U_ScxPQYvw26|i;sfR6er?Z1~#a-QW45`K4$OW_Y6Bo{QRs+Mm(g?a0( z@Zglp9y?gxK)U3umv)jOJ2Gh+AIo?jVzsGh+T%b?t=RMo$JS6mo z#WaD!xz}~cnI&d_WJGBK9EAK*w{+QD*z^jyAKZ7O5yQ4%4Y}Etq%vvKupX*p8NOUhggY8{OzJkH^5hf$dyFsoRH=%5Us>z8-OVA;>(` zhmn}XQZSZD%*3=}R1Sc@qiEeYp}{&wS5N$y$_j65Ldv$@NeBvH>J8-OU zK={4Ajj!+AA}c(Rpv4B(r8S{N8yp)J>_#*z&(vP+fhp!pXhJHZDGhB)(UfsAtrq_A z1GjcX7YbjNrDZBjd_L*C95yRr{+BOFE4m7l8DG^pb#wR|C>V8vn}}+mv^BNTnT!PTMi^l)Luo(V`o|4@KiLN)Q}6Wk%P20 z)*ko>Mm(K#u(rEZqS-#_^;LKm^*~1p0Jem*rouhrYzfe96&W>Z&Yw}5Ceq9 zc_3bP&*+VzsnX4@79w#J*8vygW~i@a|Z=2n&kVrqUeV zzhwJ?Mi7~vV4$~>^@3e~jm+&H_gA*9{T17t)=~IDp@|7Pkag}gDZ2LbnVhzrK|S%y z4B|B)cZ=?#-`cJ?mrx3Sse9l`WhLpy)-;ETsFA);!bxD7(u(5HC3l2-uOICEYueOv zm+-n#5znZY<5#l?jZW$0e4c{jlfD??@4|LTfu^7B zEItal&KFjGw$AOfMC$SaAbJCakH+#kUAOQW8P45G)iQJ$?!7`2G(aQ1KoI>n9!n`v z*g|Fcchd@--IMQFMXZa*`%sNCr}T)~*g+rr4hx;O98c1NK@rwtnb%0rG{Da}A5<>f zt8|d$x;LTMK%2W%({E*NPu|wtf3UQ!GIYypTtN*#F6pgeb5 zO7ZL#VM6*Fn5LEBPjcFk$bl`s67=LZkd&!fV5jN|99l@okES@uwKCfPb^1U5)q*rq z{EiA%vJ~^w-dhyAgvNZw*8cfc-zI)~0UCg}Kq&;5#l-MeKB4}}mo~}Bm?ekQo41qB z+M1uI#y|^3jEmf_TsE9P^iq zztNGa<+iyO*Pj;Gn5K?NN-wLwnEX_^U$#=vvvp+rjGASiZ&mcWZ)qbq8rkKK6qUq; z_))ioc&6GU5t*H~1cH}!<|!P4k0J}xnRc#ZgW~&57u+gi%fjbYB9!KCt5@t~lK!G? zl7o*8f4g2|BYRozP~W~)fAG^^C!oH{LhJZ`nle78ph8$G^@_S&Jr9~LhFV74Xrh{3|$cHAlusj2)=O8-<04!!0Y!#^><5j;L2yiO=^O+ zLNAAD;7Jsrz?$OO`7^tOo#YlL5Qi=3I0Dza?X&mx3Z9)f(}a+=(Z#xuV+jEMec}u+ zZAQhw9R{6c2dCR3l#FMNIfgd7Vi(-hE1nvg#^U+#H{`Gp-TXu(v1Mw2<4jtvm4>QO z#-t3-nqjo53ZAzIz_a9*ic4AkoOk=#b-BdPh7zSiP|Bi0*B1I#5%QtpNZIe4le}Y+ z*2$hHe!0RX>FOddWVE6aHL_suqkTqE?*jp!Gs|h6zOoB_yoFDJ_zpEl=={NxHomWz znjYC!ynT)2$qMGrB*!}~ZYdi@Wjm8$-iL8mSsk2wc69`bye}m6pzl=MBkuH3@L|WZ zBU;`%aW;EaGYG`RrMjZpvyw%VPD^F`KvwucmnYTzNe?t2Rd&_)02H zI1%wH0_}q7j(42i(3)HJU@aTkm@y@I^jR)wvblige_)7)pTI-NboBr)v>aqoB!_j^ z?d1~z;%qJM^awGQ39BDmW6x9Bz7rqvM)U0Q2(KP*3G}^3+#Ca54Ls=?#~jh9m_ccF ze-x1Hr3uCsK?_OqJ78UR1O`U4g=~$cma2wvT6GO=d7ArDdz~sJGG248oMgvL>e;9I9_oNrhpYX4t5P5 z3_%;Kovy#DWJBV%6(ONNYq7Y8ko}KU2ZFRlWbsao$&~umeOglGO|y)Nrbx-P+BeKH z9R$kcZ)!n?4GH7CLC&g1()r2LjjA&OK%91?qpntCzyQJ>(K_1e9w!pFR-m5^vST@T z{1C73@(xbVLekhUppIQYAIWLT)Rz}sOUrGu{%v0?!;L}g?uhpEWKole9+IxsamhNc zW$QB*A&X^lfP~Vuw@1*?o|bB+XCE3g=eu0Q(%_N z;}LV6uA^W@rvAgO}bSU^=AMau2+I5Ek)~>;(GGTvX{RCYt z8cu-lM6~j+$YVLNM&>t<;}XZFy)H2ctk%fi3M}A_;OSu(DSZJUshqINJe|d0!@G*3 z75z{GL*Rru8RKp!qKG!RXDW}{6M{9$46FXRsBZHoY%1}(Joc+FIQ)JiKPz$7Tu(>q z=9+99903}+Dxjh=cbiSQzwhdWhUeOf5J$+O=@R4rY#ci`LFuH;fh7m=Pk7wzYcH|!Ww)gqgmlN|AIn6 zaBIz1VY?@*3~vPn&PyQSb@IAi54(0ArFrq~B&##LIyxmjuRpU#62Fj)^bum zE?l!aExWtNRn$ViHaXfg1wWS8&Iydj#n!f@xV@*F``}Kf+G^QT7@Vh^=rn(t?FNe2 zKiDhuCX>;iUFFDwj}t(gv&vR#wG#E5Tq}&GSRAUK1AT|o@|&%z3kjd7taNx*1?t@j zfHbabv*#V5y@u_F)r193a5}kF!n_2Tieaa9H5gN9JttCMFgo=pG|#F)581?Z`C zdi_LvNPbBI)q5@d=V!uqXlj=5X>LCg&?D}6KNbS$<+bprzFgD-9DC5{P>CI%m1qw+ zt(LpfTt>|4opC3AJk*IDN)yKL<6h-3q1B&-C)8ude_6V}KT@2#eK20qhMwAC^BKW* z$YPKF6B;B%E(JUR;7mLa_75^_!{<1`t)D#*ckERHp8uLN=yPveQGiVhSGrC980G?s zNTWw_7lO9y0IWpOM%94nI%isX9&U5Zh;WvF(86wY8iftyTIKII$o^7FwS$lxisjp0 zR(xcIjAcww*lqyv$2#J$twPle;92WzE%Xl(&IiP00a)Ray&;7q39Wn%o(RK!4*MkK zdb9*72V3pp(D>m^M6>b*Bz*s|oUZMMWaxWU$h1DB@o0@{5Er2UzzINdg>`odbsG1@ zBh>uZ0eI7UXogKwD6;IWSwO-UR{B_y-gBf%ix(YvraWZQ+do8_J>-Ptp+(`PG1;p> zEI!V9kPlr^fBFmrGGvWS)nW%Y>=U8`eR9~vw$joDSzUIUuRb-l|1645kE^ioGxV*Y zJh??r3^;KzcoyuAUsDYz&io)4GKm}RkzFIw&zh~bU$@I%Y-i?{bU9inkY^NvoCw|l z{*LceK=c#_RL^l-vh&>db!#Dx(-r${r-_qALa?c^@3mO&9kVUJ}868M>@+et$XW~dOZCMa2s~sV6 z8H%v=kEUeZ7ZOh?!diYa|6*$3URdfH&dlV6V1E-Gh>zkLB%*)q-E9W3`kq@2?H6!~ z<9TIr9yS?J=f)yVOLwim@vmD;XA~!Cxr=Z7HrsOu%QgrX2OnY0AjC@~FGklcoyHVQ z#DvmnR0z$&p9l5b5x=K*OU%UXplm$j{F!6!45~#~J#|Bj-AVmm5_rMRZVWLmbfR=U z^WJWlKDeX(HFok{N6H^dDg!~m8`u`F@+LX*&q$caf zWJ3%mk|=P!d7~6`4kU%bO)H%Q(iTM7|9sM~S$*5S%%kWqLBE1c5r}M?d-|7^7=K@g zFz)6Dw>jNUHwYD13xef>luyIGER=~6*b~EL{}v(MMqn$N8Y)D3+%MYf`yKRw*U-3Z zsY?}BwL~Yc^BNj=R1V@6QtnlE9DY+aWThpnTfy1q&Qwt*_%;DfLY5v;K&}SYKi@t*p zvAh!J!y`s5=GlXJWcGSLFXn>s?_&Ar3y|fI_m=uz^dS*BFkZ~@0vo|h?=W>*^EFzk zNP}V8Dd>TUd^odeT7Q#NzRGD}#ItZy{AQ$tC*gr~aQkXl=kcipH}u`1v3b;D{4fKx z-FSWIT7+#@IDCmpN}59(#|pW?5?Exy*{Qcz2Wh7~;xf=J>|EFx^3?&38v3KG`%{X` z9`bZTm2wPO#^L%c0@P&8-% zS&B;hLHbp`JC@8y9}uZdr(Ag2w2uWz_0mj#xHKkx_1SugY8|AXHXXI-uv==~94+27 zI7;{St9R9L`L6~R9kH%A%Lh?d{tI+%iw>ONbwjj9?olq&GZj?aSK)GuiZVg<0*V|# z#=i1~i~fFo1$z9dN*Q|xsf4X8uMDZ9;SEwFS3y3(oKNNU{LyOGdE11hohuJdY}@I7 zfAe^6NtaH(a2>;Fv7Vyr)X)8Wp~C)l1P5&gl-y}i2>kMP*;~c#a9YM2kSpIRu~lPT z3fQ?CM*a~`iUtjHyki)@I?_LWUc9J`Z>8KDbMLNxP!DO`=tb7uI@ye!j_-tHa<56i zu}1?bU$#a+W>dq(9x;8%S8Day26Om!8CKNcF<58bI9u*MU z)Aro6!!Z$eT|aUYf8i1=N*`#P=-}6edVXO8clOW*5ltF$rgpJJYO2;OZ;9}!zuB{< z$^i}x6}3g0?MvvDPV-_>feoelr*tjFVKi%1T zQ)O@&<(F@s1(LO`dKu%}W>I5W$d3t1llf_ncwVzq^gZH5pDHk~bJ&GG_lNkoV`|UV z+i{Ib%8QVx!0ni=0M|dyA*!ScV+BQ9%t+#vxVWu?FA|<1-+DB(#zbqy8oL^Xj(Yh} ztciiG_b35!U}XlSYE6&m3H!2%3E`U9u4OStpSyuQ{9+LZ`OHDiy}~?n*mKYNg`4qUDjEq0<265+bp)hPTUDM4W zwAa`lcyj1e-o@yh3{iy@FF;>UaO2xX99cUG<3Roso;U2Cd1ajL<=PIWN>APG0gP`C z3SOW}9Q3>2TkT9BG(JWUDya7Lt`kIylNCITR7+`PLu0U8|1APF&WT3D| z*}eD4-daX^92x;#?+9_po}WEWI=2W(E+41ZrcECOxdt{5)}K52zGO|Gptp*aA8gSO z#RSXXcGe}H7HpsVJTqQOEIg604Y$q*?S8#dZN|vvZsA*KP5c^0{^_jb&6Qw<)_Bsuc8O`$5q8`QabCkO zv!B2k6A^BGPG?WNHZ56kK|QM7vUFs@&K~~!Yv-Wf&(bBX^i5;x~dtecK!n~XZ zjF6FTBD}TE|DHscbKIG26gq#xcvKXQE<1Nxr7O9^1pJxY^P5WFcyVYmxFG5H1 zZ|XlyZ`XHDdQ>Ve`5S_pu>b5WwIt*J`9XO3^(Ze8SBQdq+rS<>3E*{7$$YFgnmqPaA%^#nYi!f3c)Xr}V-v=rd zTY_XXh9mw=Z5anlTZKLceNZG;DdcUR%G!Jn8MxW_3Cox3X*73(wyV~*;V01~Y_h7q zO~p=x|SlKx@kIt$1L3rcv2x~E!pv z{29VqX^i{B=1DGMdcd)Y?as;+Fyy&-FYj4Awl-1rA1-%Cp|!^LC-~0s0)>Wm!S!}z zFP!T`$pgG3%OahmRO(h1HC~A*6kPtU>-k)2C9PZJzr!De%dX%;HzUzCPp#=;8Xg^& z?3HzTuz4~F5v=ay0W6p@NNYom`M=F!_<=q1vMOI+X|(!3-xjZIY1zGYvh5-rv@}B) zonkz7@{v{D&QK`aJ9`X^vnEaOT1Z>DZwvnr_BYxvD$HsSwcX7nPH?=#3q}^C0Iy^! z$p81d_xCwYH{CB>@BDWfeN(hVh5VL+iWkS{5#QPYi^DLf9&a5-si~C3M)9#>Qib$Z zMl&^XX6UK4$1WLYf0=5(p(j~Oyr8}(!Yaf-G+(**r##$_j^tx+^RYRI&~GQXZ~;1- zuD-W!{c|TwLH$)kBT7ye+&Wci`Dqj_9$&)6N|evCr)Bw~$V1;yIR{ip{ouWj!x%{q z#jn$iF606SW8JC9yYD`*JQ68a?%7y;(qCHEkz>&=4IcsL-+$9Jswe?oBw|)Fe>&A4 zG%ytus`)1!O+Yk5mDEa`tML)3iS(+6$0oSf_kpUCc8}5xs-iIV4a!YhC8*Pk*3M-r6Hf?w$q94O`uqW{k4d~>W=S6~=TShu ziy))B(ap6R;9GOtWiL|sI{>gsPAThV#}CM&v|=H=>Qf)t*5mi-K1Is-+uXa;6G?+L z>Sa8=0J8p2A#Bik&jW#JpLLAl=h=aJQzFGOw(WPYs!{Q6hLbAhHqBpE)b@e`3O-A~ zme`o>{6*Orj3B_FJ+GAXF`?wa*%69*dB0tt(<^2Ek~u>x zt7wvzu?}$>aOe>SuBnKJkKE|^EVRp;db?Jv)@LWz1$^SaI+E3`@#Z?vm>JR6W3*CyqCFKcCd z1XIN63R-;b^x=xFj{+52^~AE8`zMa%^ZLdnskNF{Lwrze^?euTDcQEwS!v;Zv-E`S zgayG6XAt*^Aw=cd=1~cWCziX$zn}xX-MAr7W}%LGO}6o6HHb8=H=JP| zq(!mOl-KN~Mwk=B8IHGN{GBSVf*~E+wcUlifas3R(TA_Q0d6w7j%Mf&fTtcF+}lb| zld{=lOu%gu{J)BNq5X`dg__^@p1B(f^|>cROE8E&zAmnBdLHipO)#n+hCD5cUI+z z;53aWE^Y0@E=;E(5v&Z;D4l^ z3Q!rc*)6clR4m%zmT0kF-?!K+7qXm3%Jo?(RA_QZ3cuGiXK4TWMCH0hA)mo~h1@iS z1CeYhbay(4jKx4}J%R#)O=B&F1f?2Rrd5WvOb-L<*M!1{z5^I%nVsflYAu@hv`ZOX zSfl*G99}o3*+vyFQeBEZEIo{WRjQi`w6ND^v@6K~qINIAe=nu4z~76Ffy`=_SH0r` zeEt0M*qi+mNXUeZtODara^a9EBT?51b!}~&F6;XXhD;+nP72{I*td3l3z;RJd9;*I zm_3?2TkCxYfz7aoEaXApUtD_bBk8X86~z5D{#wKG9%{?YZYeOSxod5^!uN^c`#tf# ziv^0&BmRH)^|uPsQA$oVsR>_KT9;Gay|lv9i)p-ACpDneZf@3VVbB+dmK++Xg0jfO zKHN__(;H|135i$cwAlic%oju6WgXkpuG=Ba-X;kOZGHGw0Ga=|hc$D%TAtXz4~&x+ z`tkESy%mcSXX>J5)Q!L@Gqc%;P18G#y2B9JO8m_L>)u2Y(T1Z>`0B%uKK+PiftJc^ z{o$Ctz?-X&He62%ydVF`BED#Om;L85+@-9nXex9Q#}ue6waM&tb1! zG*>qfc+Sd0Fz%9#akq!Ihp4dhO8@rV9KfW*d-It`Y7a~C=^gT4XO=w3btzdd1@YI~ z4<{&p;cOmNZHuHKX^op|c(^rcmVGCVGYn3Op!|k9bwrBA;}4Ezdr+5~C6_~@!xr}J zSrkgppdUZy6~hhc##gHw4SQ~Y&+SZq>iAXi;*FxacX11U+6?ri6|!GZC!x@@{0LSw zAetdbk>lxUaeK%n@0e7qj5QD*bAIhu~5jdeY^=*EC zqBd_)19-5?2XNa7Ydw6cs9L);x(t1NtR%F(U<2{R<+bOC|9J5j9G2SA`0gA0Th6zE zRFm4-B7)1&LorCkeUIV0Z`phA)#(=NO}tay^A6il6`NGmy}EKUgZLNZz$z29;$Ez3 zqPMoTW|XDjgCVtyZ7x+;!|x*OcbBtsuLOgmSx(U%sc*rd6mmpb#C_1m=5MySY8!QX zd6j2^)aZ*}s=;CJF21A=%N5g`9c(@?zm)xeb@^Kg!?2F)CT*R*6)1HX}WBJjEu^-^@HB&r8x~Yy1Alz<=ux3(I5So z_NQGsZpOY&_y&HAl(sIm_@VRRPXYhmrwQtvTYaEq$-qi8=g>+$)Vj2W(->*Lg3W|W zw0{&Fm_SIbQ~mJ^8s70r>>W=KusmxIc2Qw&utEHEN383}??MJZgn>C8MfeB?C)aej z-OqQ%e}XMucfYFXaiyDFaeJt9Nx?**8z&;B+6!RUNp=$PF4fQ8fL!4>Jk0yHVuR&s zDWHN(+MoNmc%zQ9FSL6lU;|y!`Y}MKu*)H*M5zS8>ICpVRVFz!M-0zdb6tSB4W)!C z39rQUhZPnuEE9pk%{f(vd%ENvK~z+y+c|nS|FUTK$V*$t@NDxcN0FF2;I!K-x~rgN zk!fia&XwQ9J@~Jzb?si&zs2`@EkAf0`+LGo>YhfK1phuD+?T_KPv83>c8~Uc#+LoJ zT!zy)cJ-asm&FR#CUF z?i3Dm@Idm(PkHw*U$jO}$_U@$IX|o`l%;0K->xrCk&Ifg5koNq@fw!HZ%n4&Op4-G zeGx-yBlAyZk!ibwSf}x`lhe|^hgyAqAHEVAWADswc}72H9>)_*nPpF&@Z)liQ#VQ* zPc#WXXQym!ey+9!<`aY5LN)Y7XGc4g(K%I^L0LL|E{(9bcYN41`7u+Zd-HCMMCPC~;TNMvHc!B6JZ2A8;89QTh< z-RR3A%aV!nB2YdFoS0}E3mQrN*GWuQmF3qt|M zLLr8jW@EBU|HNCx@)oE=e`JJkJfliuzb;$p2^0=`YG-yNhWTc%obXijjJ9p9?BNM} z38UP$PRiK=W_m;={@>bGVE47iE zzwTgRWly_Hs4QkopO3E#WF=moP-@?l2R}!esjr2;v@Yhv9(SPF9+*ZE62}#j$Sr>LzP1gHJy@N+XuRQAK!CV>baGlDfdhH!P&f?9x zkUN6afgbsF55>svOFhE9Z_B-{l7i9zyc+_W7AKy3VCWLaxws+Q%bSk{NgBiog8*L^rN4D}L9qAb8$a%k3$H zHn%@Yd6Dcsot07meqLNS}y>_b$x=ZC!;9|JQL^|^qX_JPFh#vW| z8{ZUwf8I6Q3#B4UnAkNbF!;p+L6UedTo zwXs(SpN6^%bK^5IqG32C-b3c)IfVUWu^HqS{4deEAa$rLx$+Ph8nx21QO~)c4%?c( z{i|Xtd;-F{_LsAbcuNB89pyL*&pPIx1H`Y_#Vc;!{0;l6_%t(S>SB`d%a77m_%=Wm z`?rTY_X-lSXYecngg6WSlz0_4l`(59XZ8>?+)-0ckvH(|9zdRvzfdj7%i<{d){-pm z!`W`OzmwgQi}|YWdvnp}S3OwZ{2u}O2BIBB>aXv1q-B#76*T=B>JXa6ZIKo$_rKoh zHMrWYaD%zpSt&>`o8Rz!st<9xx3rg3m7soivp#AoZ~X3TY(-nOsVoySQJ;xL@t4}K zc=1XaV{ScuIW>WDcKaZCu;ak(b4$x~e|xl)pVWMB;l0_AQ112#7sU^ZxmWIct{h3I z!c(3IiFe@~w?sAq=_apAU>YtzyaZWRyb;fH@>95C*D-L1JN8w~s3>2tp`C$u5$2*C6ZKBM9kAS0f^s3;j+vLd&?fgtIM*gDr zr0Q%MJ+j65e25=gT#rxo78u3e{t~tHVcJ=Hxm;f8VebppNxGgk{X^h)nE(CyP{(^; z=IA`z=``w=9osk0pZq-Z89Fl2cynoU{&~MUGd!>*m-k`z)SmafMl*jTfFZu5I^{zq zeB;Z9P_0R4Un`{aq$Czrz_rUcz~ksHXQ#LC&2`<}q~l5EVG6^eKR-r)^8WUVQhZK~ zGxO-^Y}b9G7$$mYJjx2}?e5Tm=I!LGY=8Eq{S(f>@dewh ztR$?67^vAoKy2^3-P=z<;E%Q|A69=m zyy3nVbC$03f))PfD?QMMdE@W88`73;ah{C_hf@uCPE#RV+~rZI%F2m*{-d|7)LWNq zuCz?HlmAH^Cin5R#m2x>0oh)yV9$?l9Q$hUFM;k@o4>os*za*L z?N=+oxJZt(RM~_F70~JtZhuDBsn1n`c?|DXlxXjBX~KUs_6&kzyy!cVi(w|-bH&bM zSicFdM4d;QsQi0yl;ahr-}v$j?VqA4*mrQSXio6F+s%oUdi`*brAzmo?!EIV#}m^# z_R113c5aDvJTZS9Y^VRmu~5}MUL@l~Mz!uHY}w<>>{)%abRji3{mzxJEdF>}$c3JVR{qppV+wFYhMtH2bZ35?at8ZT%&QBmi z;*aYFY7>T`dz@k|UihSt;?7>5q~ezhb>_4kT5w3_Vp2x>P6|bJ1>*g+bmg(1Lz847 z>0=Vvm|df`q|g3vH^_2}z0ZpMYUXmIE|R{@-$yJzDFJ=_S|<+)R5AeCmA3_ldJ5hY z?%-x};8zHBMLjIc0w-5!jN>SyMsJ+KeVw$x?rC9R{pbm@Gp*l6SpGRy_w1G)xf0Zr zB9`SXIU7U*zV7o@HWynRf~wy2^8-~oHPVS$AJwWly0}LJTV&J6zJam>M|}MClSIA0 zV|&{FA4ON;)@0X*MWkC&HtCcSaD!3Oh_pz_2pP?&O*$k5snNaB5&|OKA}EX)Bc(x* z7!o2dln{NtegDC8J=Zzscb~fso^pl~1(G6n*Sw ze)F&WEP>U_K;(NY{L%*(Fz6ww(IGOim;?uv79Is&3b~;>_gM#gwRwkc$ZPlDxYuEf zOTIp#OXA*ZKeE?9gudiwyt`?34){*E{T42Xyg+95= zC&LiZmMbnx=nRFX@^}I*&u$IZWu0@NrX2Nk#QTWgWgoA9+AS_B6n}Rf<-NXDcv-di zmQM`;@}w8Dzi9QI9V#N{ z2k7B0nMuJ{zd7&A<-~f>@%pI=ZPZiU{AIAqP0ncFnk`e*R+eN$a0aJ?|HV&4B4){3 zZ9H(lnXQ(;zrt~wz!K|pfB#YJGOpkCFKii7vSAoY628}c%`K5y@=Yzd!mh8jC|#15 zY~oU<;UvXLbpW>6D|VnrGj~(Ye+ehmg;HCOFj8t`I~uQ38`t}tFUf~lg8s~K zSMZvI9b#Jlq+T;;qDS*;!D#MyaNA#}I5DujuK>|H)7xn-0i0*V1oNvfpVwW$rhz_V zWj0iX3!X5;kW$Mu=Q#=E!;QfcanL^Zb0D#6olo^k83xI2o0vH4>PKS0GluIl&_~TX z!I%ZoRr&GHmurH29dR8H-WPsV=%g(Tax+pE{`S{nfymb#i;qGo^mFal6q@`O?f{S| z>Sc@`iGNAN?+?j-=Zuj;UTMtV7yIOc{{$O1QSRFBa*+EmW+9c8!!))ElS&&$$_4c} zgQRgMTrprAi+i;cA*076p!S!P%wkOVW)pFyf^mKNSMISjCW>Ly>fdI@Ij7+1?@Exa zLaH(XcQ*Cc)$u>aL{H)lid&&DI7bD%+r6e3A)N<)p9TK#0+1JYe@VFVv@$$ThII^X!@rT7Hqf*ewc<&g>GxPV4HDetHOIlR?9>K}>;;cYQyr z*WQO~;$0tnMk7S!LJa4lc7OEEclo;z?yRodpRh9}-LdVMU1y$z5N_M9E=rA~WxLFgHVJ6Y~;A6zR|Knllq zgBzSsPQ3}^F5YK`DOB~+WVVST7}gZ?cj3_NHP_#K64D1z1bCNyY`b+bVnTTExqM7< zL@{fKxX3cKU(4*zK$NJD>=!$*!@%y(mVnSz!Ikc$34KLd;FopngM#leJU{<`5)XW4 z#1y1LFb)gHl?4jiBVZ1Ti$QtW)Z^eKh6Y>Xv)Emf0in+|JEC5H4f3}2@z!EUV zumQ3cjF5zU>mKMLls7#n_H;&@HyCt2oJ^xvArP$dVDDU=hfO+*cS}QG9S9fnrJm*H zwf*{_6Y23wI8ISDB3-l24CZ*8nnx^li}A(GxC&=@n6vmSRI$obiWo zieynT`=mi46o+|1RJU{474iy=J&4FrGm%x-W9$8tNdUVcT7LIQpihFzh4uSoMe1c| znad$948ds?b{ej}U3qDCOQGc8?2dWLu9)#9c0@mgXiXm6aL{~-aryukrSs2ZuLcV2 zKJmHj@Xkz$ka|Y=J;8Cg_Pv^Ovp-gTUnc(SJOPAN3(j?Sn4XGnG1>y?0VdQT@CRc{ zKfx=F8@UHfmh#^z(?o3;(h&?k)kc@H)pu;vHc#(7Y0UBhniZRruBJ!y)mC>|?B*eD zaQr>{dDtMEr?ncLm^8Af#V*F(ZMo-7?C6tP`eq1(@8QWbafU4H3z_}khNzrW4h%);Y!(j5EcMmBnPLl*2R zFFKbuD^93_Gk1049H|M*VYC3^?uWn67pW4Au!JMn9U;9YYGb z*{gdnM(@V&I{Trc1Jm$+ABSSk!ZYi6@JI3mm$l!(quf@cXcHoj2&DbDT75UWJs%QR zYE{y^&qV23l}p_R{G4d-NCu zu;l?#A0>qH{s7qk^@TXpZA9_fMY4~^Z zE-?4C7g;##DNUTB-<07B#Uz{8mqlk_*dXRC|MM@(o}clZHYu_2*_)o}&jo*pQ2mvi z?1XQOS$%lYw}Oy#@!OksRMqQy($M)Q?9yFOCEoOhz1pRnt15mC{v{$s@gYu-MPp)1 z@ojOcATKi8W_>4r_<}3XOS#6&XXyx`Rvh1Z?t`4E;Ozs%J{yYq_-a0*Jvp{$p{7Yc z_`L3S;HbiD@7&Urs?Tz5s2wPJ^pQb+Gjd2`;ndj;DlY(?@PBI8j{otm_k_G44QBD9 zQG9`*nV^seDkzn-i>$u5UhHgGIeGGaQ72H+-rQeXjYlpm8M4hG*s~$qYFqpw{V&Sh zjZx#{u7ore$F$-x87g*`>wnW;(|6}IN?qRPPX3LT+1=)4b2Za1Rd({dXTK5MR&+-I zg1(HVa5WKSNj1O!W4o9WS!wXhOtACzGHrSR8gTCypO?=Zcfo7L(|y_eB0t-=sgHi$ zt<{-rKV387Nt4RrgAAr8-$KV;kDIA{QxaF>EVoAzT!wz@x40U{ zt=_&6KFXr?cVtejcR7~KXd&&}_3wv=kKFnYG9K1urM=(Q;OI$(WN*2#0asMCny8nO zk5zbzYs)#8U@xwL$c zg3~+)3CSBR!Rb3a@Bo!G@RC%n-`XC;#G?)mL|HX@udO?3ZJ4zuYmu=3w2WcFYR=a` zg%SKY3YxGAh{xwe-Ad`BNUc_l`KtLUoB>eIdkj=?;$!}!H-sNBLM~qIELy?z1~ww+ z)rId5Pt`f?yuCXAAih_VAn3N{D7w_7r~oKf5YnIM+6YZWq&o9&k2uVbsh)E`{39Mi zvaIbVu121H4m9aG?8iy8-m+i#HK@7z;=*v{6WIi*ae>qR{69ivQOC{)tEgW8k|PkJ3o0_|;9&JFO{`PMKk?4qzUhpe9&e`}!&pc(t zg~>Ns zk8LRNB%gC9WqmyB?W_l)yzaRSvG7kgk-r~%gOMmGN46v@4>hq>Z_W8{Uv?F}$-kWu z?FCI1@F^B86UI0n5J?#t7Ld4?46xilWmI zZNLJDc&cFfJGKt&GC!qAK)#${aYyoanRaleFm#U=uYb5#`CmBg+34B!YYm^fY3Cgm zV44%LXQZYiuQNK6@GtFq{%ISP1FaVkg6=If1qqw3(iyq+&K-5}U|YhGLW~hWVTBB( z;{dd3_P+_eiiAZhNph(00t!~Nt0asAqXYX7(=9|N%m$U3k>O}^i*xa+2Wl+A zqq~E$D5&d)8I{=jt@d3TIkEXh$Q0vXNXGhqxbiz+L#3Sx(r3)5j33+L-+UB4{_&x` zZ)1^ODvjWN5cvw^$;{fVH1pbFvaZCh%VX3Z76t#&V>shFgY|IT6}k7Dej_xc$;hCy zIMI)U2V7QvSS)oC7?w_<(k$Llsntu@hN-|QmGZQBX!H>q^g(&^Yia+(v|IX5x% zwL0Y}2x)lAPWwPKe04oBV9_TvWm^J2_uAz@y&#G{2|f0G!R9E*4YP973*G$YZD$P6 z@~s-K@S^fwoB62AuxT-g&ih@YfCKe&s3_2mDp)%JqYVl6F$(YeF@7H;^9`tozd1|f2 z@mFe)XKRZ0uA$M-@Jq73(N_O{Z&ts{j6eGd4Z<{C-P6wUo=yrQdLt&&Bx}SY@ElC- z#n4XS3>jglNq)ff&!Wqao=;apmX#NPP0%*`4wJS02%bne34=$SsuZo9q(djRg2Qt* zub(EjOUjpYuOJ!8j()ON3BP;+7;qLHy280Fr$U^c?4NnGMG10pa1I)J^303K3Q@a2s>&g(UB|#e|`a1{|%M1NOuEIEc-Q~AvW2pT;yb_TQ@^4?H1n&tfVj}7mCqhu% zfT1tqc6;(}DqlE1aJ=N3O)Pc$)SQCSVi3sm<~j$sG3H;cihLYOCr?TQ?=4L{n~=he z5;UG3C{0H0+Y1iMz6RGevrg=!^*sUpy>$j%L8h=BoIn=zA^zcjmV2xdFa{!dy@s;> zu$zDJ$3q7lL7{gJ1Gh}S$qf>?(7a__bqs<5=Xa6rw!H1YonD7yL2sHBMG_5K5q#kB zr*bW58JG!1=W_2na^7ga7+v!LpwgjDI!@9N<=3FzPIcDNj14IB69VoI=mCShEG00yar1t44_Z zg#LejJx^#(BOX5Ib9$kEwvuJGSi`;gQ<`XIS&SlI+Tsn_c=N!+$@nW5&o|%mUvxi& zPB)uDo^ZCXA4-BBz7751pj+910%pKKojg1?zdwxSTk}pd{6AAy)$>t@7!_L39hdQ&fLk;LivZ-zrr>7)R%QJ7nX!W;#IayEmJpbUT=9y8zmkqRYFF7HhX@* za2g-81-Qj|arUg;9``oQk^pH(xyqQ|h}jEnq}&U!_iYisyO$m!wO*0r5b*sx&g%l+ znsn#?D1sF_TLRf?d=PS*!|!dEwg3_3?o|wy8sgf`hQ;E+;)m^tBnLq>267hWGeIs` zZFe$eWhh>)A6d-Am9a~9ZznTj7R!a;!#2fZ6$(x=5^C{3=KL*i`yMJ+*;)cB_HtV; zEzpjk5W?1tj|XTS!QMYl2CWffS$B3vQ0bSh9)so`vd5*JVst6~e~%+pLLHjc&ogG^ zXK5+7Y1u$nu>HlE>Wr-3#mZzQ{=0-NSUT_&sO1R38a*ml1;^w3x42cn$Vr7Li3RwP z%v^$+fFF*o8Ou^2g`GNTHmheyLXK|5I#<}Y*)Sv}`4#U)b>53%u_p2P4^JZ3wYA=cPC*L`#!MK>84sZSH6h^?BP8+n$l_WD1m-%)b%K+Mbf($%^B>OhBW<8@5>jBbBr&qwQ#4j)QgEJZsj188q#%sv`Q? z$jS30*Wh6V?)LGBC;mQIelbZUsW=s7x*Go5nWN1gxDJjZmb-_DWfLp$#ch_P;y`Y| zpxb&t@UoN0M+KY?{r;S`w)V!U%LIN86Z*CBtOrlhbh=os=lqdUSN&{m$BMU!3B87kgtrI2fVhQ6<5njDvUrTG**@? z-CzCt@F_=uGjLAWX={_*ZvL=_D*s)k+txFuSc$>T6?(+fd`95cDhcln3#;eLij2_3 zNx%+q$&>wBj3if8Dema^R8|8&qg4?6${t%T02b$!`o8CNgQz;_inTyYdpbZNB9$V$ zWLI5xB3Kg$o==~2z8dtP+PUnMO4UPy^sj(Rx_4A~%%g+Yw0R>>N`h+OW(&hN=v zt^$IuCpX{rNcw=>&lSmCu!^GM3x^xyS?kW{Z*|Z2u{PhDU$TM8>7pMSrGWZfvM{hg zvL$c0Ch&en)~g{(5dC{ zTh87$ypIm4JX*}!|8D%oF!3mK`=5*F1untqQTmW{Puy@X=4Ts>D25ka3>z|ivlc=! zF==#4DsP?jiaE&o7tigsW*aPZc{(5chH_e;9sD0NF|q+jU&Eb_a7OYxexP^@r0#1! zWF7TlE0fMLiWat$H{12}ebB~{H%RB}-FM!{1>Wod=JnHbqAH=otsBAiMk@*$@9oGp zW5AyfEvT*TaBSP?+ds`I;1%Fm26cy?&o+#iTV~~^3$H~gxjQ||i_IlkmJbW4=Gl7m zYJ1~=U9@N9Z-_KsGRqsQgpg2s3oQ~?I-;hbD<8gb@B7UNE>rx}W~K7@cIbAqyxwmv zTVMh??PeDHa%H+m3wdjn4 z>G#$)IkzU0_<-1c+c z(i1u4hwa{0g=%vVfZfvDsn2ayuU37KQASYz zxz!oWpvSo)n(kZMc1L$M4FYTBWlqIa7d+S4Wx2_F9X3sUDSU7g5NCtZHNql1`n7(u zdp@Uf7d7J)H4Kb{#Eb@;et@Q90+&kx9aQ)IBz8h0rK1js!2NC0^OedS&UfL6-;#s# z-zI|%tbg=k6^DP@ni!N|ZOXkLfw{e*-ZOA=&K(Mqo{_C_t3>lY?_0}Sy-qYBP4d4D zS@Ipuu!d8bQ)^B~Ra~rnVY1?*0I56y$S+epSRww0QtlJU9T7-j-j2E@>X4IVqa)&N zaLD){%!sAom(P5*5qW!KD5}O|M^P>rnQRJW-6I8(^yr%)~RYR^l(Zli1C`pjRzpM-1U^HLGO z4b0Prq;6|4dd4&8IcW6wF3i7{>QVH)b4}NlM7mc#s{XhUb(a6yH!Y4Et+Dt${@&|{ zzLI`kq5LJz45M3x^T;RkUSg>MDO}|Fc9?ecx#cefULgEb=_1Cv?iEy-XE(FLWSHxR z?#*rM^?kmt32KF-YbFg{MA0DltMh-%O5aSPxKr@2F@2ij%{}xf3aZ=GU!$5a{9U&l zbXuTU8r$4frN#o=@3IX)| zN9g4j=|eaC&1Xx*=lO)FO7=65@Jx8-9f0>X{?Q3G!e@K#(&Y&J3F{A;Z?T>){r=k% zn#f#tc`tQqI3#aB%5vTQfo7l8NU}Wjyfv2=K?RInjiPNU%hGbk3UYZJ_dbyo7zwp8 za+mHBA&Sqs7RO|*+KmtFBmkMnWW(UYAN6q?=R?by%5y8-5&@)Mgwd?-83naS!$P?( zy5uaJ$ons)=7diuIUVqg^wG1+<`%_ITS#(5GoA%L-}$IH!|Zox(rhs0-axhDlj6F~ z#B_-Um|5@0sfrrzl*(pnHrFe9eAj=I1dxIv>fGalA^=JnyorrBDGUe6Lq>ndw%Q;w z<@*a+4hd8U>K82`_PI1CHdy+h(K3i;-2Al=>tx%L++qj$IPeY{Z$)yKPY-rU* zDXG(l`RC_5o6%vu6f_3dc@e8-uxx-P3G@V?&E~@Ry;``0V!8Ajz8Vfxa{sux9c?oR1|n$6BS=W(dl*xN?Yo=bl-#7V8{U8rr(hdC2qaDlDQ6Hj*@!> z#>E0>7+%nt4>&`gpe!>Kb+au^V6P z+UM6jDmE4no9+M|TuyWtslvxi@i&X|o7w-?qtnB^_cH$pc&TMLwgdpf=Lg+mNY3cD zVJSng`Ci-In-;VA$k__1MPx7T6tJKoz#=Ulzh%qNe+n8-M-Qi{U5@hErie3qc$yYD z_2HqW;PunTLE#MDl$75^5JW!Y1QjCduBY^-b~erY1N5P*-xzsl3T_jk5JjB!ADLuU zD3B~N?R(*0{!CfxcX8`qOzyhX_Bq>1x7UsT?!DVo`+rAG`&Um-rT^+-Puw^^Qht+A z0ZVvKU_&Wl)=j5jTlbaIiK@~HtV5l6&xD@I>&B(td=f@4Hzwr(iDJ83)W4kNgQ0EG zu+8wTlM!E~A2)oD-k7B4**PpG3B5%gZ^12Ti=GsPrQi*LpVYD2 zW?5&4Ad%5GU0)joo-B5zP%o>8{Owsg<=3+x6J!FfBp72}gS15J7<~AGAS>h+NX3S` zX}GyiJb@jF>OrYg=&%0$C|3B*+u==x`UJn;O+Z{BDmqka95xhR4qSRFhvoV{V!W#j zG68C09Nzo8OF)H5zd4y0o`v5-IG6%Q=|V5sTP{`FQ#8DieidvgeqON;bXWTQ=%a2q zh~Ce(TKc zz<@ymy#N-dc*}b99_wt(BAcs+-jB}Pe`THZM?e6Pj$+*%Se|HMoQCs_RYH;y-1Uk* z3s6eIZ`;Es^aCvu@=QcVH+?*ifYSzhG z4z&GO$;6Y@UZVWL^uo$Yo|$vn7agbFS?@Xky{wclX?j&?cxEwNn+Xz3+Md?q2rw;v zjeG)kF96oQTfMC^e`=RXYBj_fZ0EyamPrLkR8`&9F`Eilnz@k#%AvU682&EZ8joR} zqwcQ3ySde}D)w!)DzUQC+%-{WD!*yd;+uPi2v>l%TVJWVU0N;fTyzk*VQjuCzoyRK z=pB?dS@U!~pWaTk(k>~?<2FoX0^3f0S@Y56*nT4D!qc@Psep(H$)L`A7mwT@1C`M{Riod4Wgf<%KCM?TGzcgZA$5vKHLKw$i2yCV&z`l1Xh-;M{k?|&^^(73L)rE%Dw^^z_cH-1V~J&Te4K#N7DtXL=vgw z*uv*s?*@7gamdyfnVl=w%VHbx7!Qwc)u|?|qVYck36uhBA#;-fp2;@t%rGH!OF^Nb z(Ci%BHiHJR9Co*zvS+wT?BHe8HnP8y*;$JC2vBf7r~rw|HVs&6voI ztJXUHafGi16=}=Udz$wSgrHsllZZk4Iy;7&s~m5f{r0oHiBm^VS<2BA zq-{#_o2w@OXT`XJu)E3Chkc3 z7g|9$I_I`Za?i{Ug~IxQ3-7@Ej1KMQKAB!&W|18RABpAVEMFt;oHfca9J96;thpJ2 z-u9@DX`aytqlK6LqYz7=7Vh>LrmUgtT2i-s975JQ4WFc7NU>n-5+q>dM2G_@j$xI( zOMZE+mM*S)fwiuhMq9zh>7!YnuE8noKKlJ1!<9Fvq`rCPf^?OO&%Z>>dQ5KqEWGQF zOdr;DM&B~f{YQU^L9hxUrENleH2HQ$Igl_z@m%1Dzc#C}L%nc;%Cg|Q?iPQ`P9K)_wbGn7Yc6RddZVTsMJVtT|;#J@EQeIZuWFDk2SX ziU;{)@~O>x*|1)%rdNoHvP`+QUsa3qss|+ONGgijP;Ng-T>|OKGNL~kDV;~8#SIQw z2+K(_N!v>~zMEX_9kB!R!glDI=g8jL5E#C7^N%#?=H%Bk88X2}svsG)Mr+ty=;((( zJtWHO;P&>OBgJ=D@*T(xW`;A5ErSllU9b;DZCQ5}&N{lXXL7TMl$85|cQsaXb`>)s zOF~eeaIv}|iDYkNEk55^Gm2MXic(3{F||xN#I9C_vOqp=m_PFkh+Sa2`^(_FY*d%6 zvBCP)c-GAJXrx=~;!`p7(Fk60kc%a=fPCVYCtxVCt37^zNLdc_3Te;?+^x;oWrb%b z8mF?KcW)PJ`DEJr{@5h34oSy5v~?PMW@y=Ev2KX3&lM5NP-^rWTIYB_N^)t3dT&h1 zx>&D4qrYI6>b|9meu(`>wgFiE?X+w8FaRMtOKbeHY=@g=oP9{Pt`gxRDNr4J$Nbqt zbIf6{uzMvbT&(n7_OWbPoxIfb7uH$EmGLNS+zyUU;a<$@C>R|+Engb{og!tk&Ff4pzNTXg5!Uw)Rij9CKxdxUv2A)gIr?Qw9)pORX>z{BftB(l^6|S%Y zn6_O|2C0t0#aiigu%|4W%fTl8qyoSHHhQsQLwf7KYtCw3|9CZ&pfdz=P3AEem%q5lC9G3RZK3u4cW5&jYB_p%?`5SADVe@?|C zzI%JS$x=wp?}*Mn1}O^}^e28_5MjDdRs*+-E)ft*Pprwi(J5hX=>e>ab>#VmVP&p9 zZ9{g_6a%iV=R>`o40J&S^>1R)j~JVwdF)!+HZbnAK%qPYW;j3mIvqsmt$Sw6cKFYL z%fJN)83^9RHh*RbwepZoq7r{Nzoht#QK<9kDt29&w0axQk6LDj+)DCA3^!a>VXT6K zB$UbH%rNDWah7S9ZW4e&>MzFxLarXO5uVnPS*vZYX1@(%>>BOQe8`SJ&TXOlKrJ~a z=9Xntv(Y*_VIAbMjp7EfZB>D49`Ot&`h9&7&SjL=OjTF{bQVeT@6c#__1)YUtTYL0 zEVcnQ<>H$9oIK>qq4}rlpYj*LG0-9yO;V*bDh{|xQC%iHs0oOB-`Rd*y9?4KYsY{< zcE97Qt8L`3DSzL|F8UJQj7Kx%WpRXf@W-5w`J*=x4J)V~AMK}|*Zz_OOXAbX8SDq zfiChKjnBxY^K@th25ldwWF59SF~qn67F;wjx|iZvZY2=fCK+p<&H6qBjNy~l$O1R% zx3<=<4DFPH;;dmpu$jSzGyJ3q>MdUslDe=)XBgcu}9w7QU%a8!r#I|t1)Kfk}>d$A^u(v-qEJsDv3 zbD88zXsW{-Bacf5YZu`B*A?ov=tskR6l3x!B@5&)8-_ghduyVUn16Bbak6B3z zLhS3wnBJRR6_@`sn^519WpGgF*Yt79d{lD+pBu($t2w$}skSV9sNkrJ^0NPjWO{k$ zN~Kq#^hX}l)JNmb@8_9AwiFczT^H;0uAj*Ro{X2ln>c{sEXB4l^!QD(u2G9D5)rx| z!n`Il#7)IwlSg*k8ndgu%XpXRNAAHt>*Uq_uzG^XN5lwMdFHE~`|YMUjU{Wo1@dgG zn=jCM{JGfCz+LSc(sNMU-rsCYmKBOF3-zu9JT;pVmzFcvAkP`b&}<|kk#)V94V?k$B+VRCgyZJn4n#dMkqT1 zK8eKrcjqT!Rgqul;Fim>ke+eA$w_!A@LKyzof?`+J*>(xt?_A?Sm)er-x#RCmB$;# zhTd^H%Pafm^+q;dyh0&e{Ir&zQMIDQWOn}8h^QylrjprB69&YS&g)eUL+h??r|$qQ zWYu@h*B8cRb6nzz@PC1OJr0WEM#YL`$uuwRLBb%B2Bj<)xO^1J4W`Am|4zp`iY*>;cPiL1ZhfDaEIxE`ELZnUCs7yWzR&(1B2L2pHNI| z+x!~zmiDvmCfc`=wdmb%cNzypONOQFHAhRdaRxkW;pck5`Pi_wv69_FFSyfinhC zK6G5TevT>`Ds0{a^_o?!)~Tzjc9aBGQGEhh$G!=NPVM)Q#L>DVvDH`Jx6(xA?>f77 zu=*33zp$eNb~Ur#W$!PkM~_^MRIC#;j46#foXQ(c2}tEK>z%wQ!=QV2KO*^4(E%(0 zOXIfjIh7{S&a|!i98T=NJAPqq%M8w(^em-_NJ#q)vD(C|4hOwyI|!ROwJ2`!r}r@5 z9)5Weg9OFXtoXKsT(O4K*jY-1h=j%P+`3s}!*RHg9l;S5`2Aps%%)@P<8 zDCgZ+Dhn@jE5@=Ahy*?>ea-VxB0{*hRR7+OdVMq&1xtFMfWYmLyC4)Lhi~4LoGP=< zqoI3GxbZ;z%YM8{TwN}qmRSJT6lkLCmN=LmG4&~L>R>=xs+IJDr&LFNVI<<8xX13@ zU{O73o{U24Uz79z&eB2*NCumD!xkwGU^6JEf09yULHRNGEQ=`juj4o@&OKhjr@SS) zbCk;`Bpu0cu2d;sjNjWehfziTGK>;*{?&f;CgQv|&PNn`b#l^tiB zglLbvF7i~k{ykq+jBgNE>Lc`G?>n}5u+E-;3`Q&m@%%i;=e&6+zTLa%Qq8amX8N)? zzTFbQd5G0#EHW#c{98-X6xH)U|NcId;O6(e$kyP^Y-KU5q#rO0K0f=T9@c8%h-@6B z%tqtwht+LTfj6di?0@m0A0IV8&hYZd-HQBF983GS3P@c}&okU+{T)G9$sI+gQ$!?3 zJB1(=ADAo~$XV;^pD6n0fl@WuVNsv2Cn!`!Ig(x<;&iynnTgRuKZ?W|_i%183f=E~ z-9hvl4As(?XYw=OsD5rFKav;*6i!URSu#IQbgt6hm9@f~YL`guJ5F}lPr^?#KKDg} zxXVh|rMHHbrI^P>HZUj6?4E)Rj@Uhg5?s>z<+|@K%>%R<&4c;4OOmS%wy2c>b24TW zfPHJ-?MHneR0c0;)srDzY{|I>Vy4w-P-dDfd7`^Q4`y*{?v2&Is9kbLYb1o-B?tA4I+dzM0G zrDh{5OH0uTi4or&G_p9Q%3t&15}oGqzUe$?W~aHe2vzx-kK=D8o0Mf~P8Af~H=@l) zhg7kf=HrJi|&BIR4l7LuDk|qqp;3HqX898!CFYeddN## zqZ8v@DUBXS^Aii^b%%P{Ti^HhAML(86W_aIvdF}3RrFp(S$?qO3s_7HtZHk(lZVuP zMO@|6Fgl28$t{cuhrfD572IW{Axj~@SWe0A)ibqcWwgIk#rOI}veY1S*t1)!Pxl+M zXs5WWr|%c9%<+9338Uo7wc|4fN(wz6p5|Z9pOhn(-Xt52SlDP^t!;rO!XRO%*?&nm7M_!FEANh5vJ|$a z>JLNXuu?~OmHVsYmu_Eg8uGExjbtlOcaAY{wt5Zd4O(9!7&D6a#*9wopYl2rS*ss8 z@VidAPBK6kq*-gTTv#!zhBa99Mq2{kEU&o0M8uHKlcd-PPT_YJ&4Lr38<%yrFeh5$On)`?F{u}c_GAOO|oj> zB(3+(nMSUj0QrJV`8F49)w`p_>cOW!FUjmm*E4h^Eytb>-U^|XB0PEsTPs8H8xI1- zmt^c|J!`t`2OfuHzm>B*$&{jJauq$ladJU*ic5Z9pT_!7Jf(yE5s||(03y+_P{cRNt&qfdz0UfZ&G4W;4uGNOVeRlW5E&< zZoDl$yrnSp)A=6E@cvw9o;3VPbCY_#2l4i5@vsFnK|!8J@?GP5yM?SH+vU@O&10|* zrmBHbxo*l6eow@tFkM>d&zmz$HSiXyE@qH&7jNLK^er;>u$8R66WXrx$oM0xcTo5T z_xSTGH`izVn?r^0@046FqMcE10aUWBY(MX34RbedII6c&K=P32xz})D_HfnsjeX|{ znz58NHnkhBE6m$gy$`or`Mel!9eB}y8b89pVk8C?dB#OQGVhH`E>fL0)V^USvSa|;yLDl=PLcrGE;`l-rU{O)Eq>`InH+C{U%bo0q8k(F z>>K$mtYxJyoWk2UhL4{obtUY+X&%2ZGv%U zhI^p~)D(xlQj7yoAJ*oz9c%MplUx`EZ7iHgWs{=HMqRG)ivk}_u!nt%UJa*}qcB8} z2T9)}J^GQLXfAYtjBi(dfyxeC@kl=NdN3IKdxf#^eetzi=VRxMkmo&X^Q=ES^N$>m z4%5^jmYw^*Ke*%Js?I5T`^ztBGGR*Lp=G&cihh}%;@0=duC37{kSOOIcy5JX za?b53+)=ZvT5wTt_$w3*1WCPJzJI~NsmRNfhvHeMzr4sX&%iS*5!6}UOBo1zq@7F= zuUB0|Ze!3jnmNI>w8DmNj3_>IGVaypkQ4{BsZ}Dk@sdKdLUe7;-W>kN;-K__4o0fs zW|&^toI5)c#PQGBAK#A;el*SIPIZbs@=UnpEakez*T7UvgKZgH)uuTPd-?Q%R-c>u zu}&pLgOaqXwC!0_r2Vz}_L#6c*e*~ZIQWjw)5u~@WPw(Zob|lwqTN4JK7w`e@NEtF zwwQeaBptsKS$M|)G;d*STyfNlpoZlhYMTZ<4DpcGo+37d__Q_TxX@duq(4Z(r>S-1 zna&kx+X|_-QIFPEUcExm6-XC8-s+mxyhCpH@7~Y2)WE(}t(26i?;FBq!MGT1sg2Hx z-AV`hv+@T)1|NCKHGorLQSJGi>0-_Lp%vLikb^wX1a00y=w0f@AuN|4;I)#={g~R$ zRXNX-nFfd}<7uOA_yBjHChhIQD>7vcZUY^=!PtVo@C4=#U^?DRxBrkNWM1*FnkFF& zz)AJqZ)GXdOGYXsz~W{Np9nnRw^3Ufuq@lVud#VDvAS|sb|R7@--V}+bl9FSSV*B< zloC?f6*(A|I9$>aU%tPGD2iGC@aA2WqLaNE5uPFc@B`^kqMi&}9w6=MaGydol+`%p zmqYkT`X5qaV1gFUxTy3kCdPQB+<`=Ewad(Yn<~F~8V|eMfiqI=Y_V5ib5#2av0CLt z^sX2*E0$UJYkntxTa41O(TLiV)DGO~Y{2Inu+@`EJV{8}t0d>B;x0=#{DAnYQMUB{ zq#yQIjCBT9>|93!d3fGTmXg@rSqkL8dZ)&a!P=0AWm1`(e(u?5BmR9`gbGd1znbo) z`x<9x#xjT+%bUE(z4d-Tn$$sJ@xpPS_+%e8W2JVQ^1wf4-8}B)?SJ?mKPTokzMSne z9jG28lk{Ekm$7SQaaAB^DA+HTZ-gC2owQjf^$eF5I1#5Z_;i?OXftO={yIIrRONP43Y@I_u*<{rI`+KcViWuY(xnFlB7;b~v}$Se=my7uoHt~FH@jlXJd%aE^ENpG zuKuebNa>##ovUEbW^8fr9w_lc?_?W$+VWsG0vB~1FQ8pj5_@{E8y;lfY@+UeR2-4y+K-VHOGajst zgFY{pp#>KM6@IQ^BF=&C~X zG@jXvJUhHN~d58dK8UZM7xM|(Ve&Oy3z$V>;}< zcRAOxBIR(#D1}^O#W78GBABllv6lQrVj)t_b^>zDp`7cw!}O3tovOu(L$L6`DX5mX zL9)0dIAF`6F57r+tQ`O9b%nVFv~?i}VMv-H&K7xer-GK7VH{%Ogs$|lw?8p`=iKL} zS({IVg@0&zxHtmQW(yO48uDAuDHp5n_AA z0oqXFs-NLHQ;hgBDC8TcD+j!kAE7+f^MY}1mPcM#G8AL;@HpAXDdreHFeWR7pAR0= z5V9eFHqXnoD0r;KD?u>;xrIkt!AiNBuNf>MZ~}79b57(u0lL+#+8wzhW5_AGKpu}x z2Ef;>ttRye{_MDI4Aq$;8za9L@~T1`U)XiWEz;!|pO#L4 z(NXwYbsw(MViR``jOx+{NIEyB`*Da6ce!I62a7F9KuEZYiZg2~y? zfMSJSAz?g%e##`1U00rt-NSp;x8rDHGpGpkd_1upbIB<(#$~~5jP)Q-HI!l*L(T^g z13a%BTp-Vxxyo@S9~o0~It=bAK;6ky@Y1kKq(O2n1}G3v4TH;85Ahl!*8?cWQU;GB zEH=uHJPu*{MYFri2lJa! zz~)8-TaKgXJdX>qam-rSRYFD!^Ee{T&-8*>pr;r#Rt9xZ1*(zjIyW7vnIJPY%dY2o za40v|p(;NSVomnA7~*M#%^hT_XQ`e?S)A~(W=(IoZOwMx7KXGbI$jF8xe&)buK)MF z>C<2RJ^rBh%j!Ks{*`SJfL8nmjnKbjy>d4&26aZZ;>CGdqY}Ho@w+u{zJvd_qwhi*~BigRB~6&yF%zjJX--irGmt zRC1M5h2~#6e$TZzkh|>e2IyBF{T@)|`8WvLR8+YEebo@CW*hShW-v1z;&>JdGVbsT25PO?)Jw;BbXJ>BJvV|(XqVaS^z&KCLgoTq`t&HIaQ{qX;$ zPk!YJ{6YDb^)tmg7Z#21p6^*q1OC^y#i5lz_~Iy53Wb1>!!hzE3$PH_s;PM#`sA$Y z4(ZLf9UJvvGm$HVgAOGs_2q#0d&RmD?$lP>dWiFu zF)U4yKx4cZ?0tnc_QAjT%is00^rav839>eX7K0W>v&G?|ImWnI8u7F#PAKWFd;3gQi8!Bw{kjUm~%KPcVJ zrc2sdw%XPS&RfQ?HbtB*@-O{7(EplY9AG_;_}TAWl)iVx zswRs7pv~3;gt(Zf$9RG+`NN_SE7^>P0&zU!J|5%9X*Zw8k;_Iu&B}>!vJMx{V?PlkBg}4MZtoX ze9Ui%hCKHGn}?I>1q*_UtWV}OEFKic>{XryHyX171u2iJO3-F*_s8QmJ1FWxmQ~0C z3(>gb1|GUsXPPxV7TF+nS-5Z^e>&DOhWkvBjd2awGeGes!ebh|@yNL# zH^(2Di^djCWg%#Rnah2)CZfy-69C#JN3e!++%KIx*9lyu^7z!vP}t~H4I;`JvN2C} z-6mcdL-9bIr>F{*_0Xm`jqCW8@ocJk5H@!YtvF|1bL2FfA=Jh}xd?T*YrHV2zT zVb>HWN3Q3um~a4X4)eQivIzyVG|17eQZm`tim!|#UnzzUkWF=4u1{2IRAY=YrieTK zu(n#)w$59^h-iug8slQHV?n&!*to!o)jjEnZ@E8x|92~kKRT(yd4$%B7FJ#``N83V zz4Bp~&BkzD^~mY&#UUt0@!9~@F$`6k74cZlC&UTLs#WDxbVn2a(LgbRKyCnTtPI&T1`UD$e0jjieh{HZ5Ue$uE68w4x;+0$>gx#~GCw==jYtw5!yDEM3n`_G5V52bj>+k-qqbS?T%5ZRU%o8LEpZ#=)kA8KGKnv&3}DDwAc# zKxuJZG@yb+ucCn0%pq%4_CyeGXD}|XZhcRB;fEKe zb1z#~7`SFi@P%cdTwPFHL=xqr4Mww9a)M}JjPmkAc4*O;5DQdgP&T=2Ivz9*b=5UJ z_=xI|@*6z9Cb=u~c%hK~cyeeJxhkiFi;%etDVZ@y&M&PXR4Pd-o>WFLJg8{C$51lqMy%OQdA$&Qu_ysKs)Wt4nI8}G886)a(6DDH zAP4qFsb0uuN*>QFjTp&9Rj-0>^O@j0qDfvgtU9jAo)^>2tYnWaJzzl7u>>0?0&T70&+K&FFJEglM+yN#qZ)d9>@A-S0F&2 zRO5{G9OW74j5^_Z=~#+aOP~7AnzYZ0R-`Zgc)%JWaX$J;>sMd|M*JBV4(KwlgFsi9 zVRV46gZahuwocoH4}D`~sw zPygRA+yGvIeR!|SEn`H#r`QY&7cM*)>>SWT%`h^+ z%bs5d`WXHQ|1ygM^PL6uBhcYyXcG_HXJWeay<4VjX8XTi{CjatX>CQ$2Q9*=SO3P% zY^6e~!F+O%n|g&=41^M|#5F~efH*I?dm-j|JoBi>9Lj`3Ww2Nv&a;YAP7H#qJRyhB zF28a@Jk=81A8VUhwQmd|UScmDu)09$_#w+qcv|LVrFVQYsICylQ7LeKkn;SqR;SDV zHgK<#D|j#V;yYYU7hTl)Wn4$y2N*a|o_Vps{10fj?-IyM-MkaDGv;+_wILF)@%>Pq zG4|^oHGll)+Vqgq?oEHWo~Mj`pt??N;fBR1Xsu>jHPIoz;>C$*vxN>Jj$w!ifT^)2 zWh+qR3(e-5=jgnm8mi-)4zeMf6Pl~yoi*mhz>Y;S zsG2HYMD{ZmLY^mWg;=hWg(kd$csx1Ip}8upsjM8vf@^Mum>?*gqUZFA*$6a7v%gc* za(y~B49zc(N1Jm3^1VxY(y5|pu7Xd*WW(`y%BrzpJoYY=S*=_H$}l;qP_&g zi%aZJ+W>?Ar18#C-vM2UC;e6$+Xd#yj>f+ZKT&TRT{y3Q--~CYH=VqhFS^$Lc8xWz z_^?tXYf&pExD0X8S2>~7Dp$RRVZ$iRgIs7*$HwlXZfR~o?p4=O8n*=~zB-0V!^qFZQFDn-pW8}q3WjLU}vFma1#xNrLTqb(erVZp`NzMghd?}!eSeIdZ zkc~M?Qwihl-w-D(mI5EBI5U5CmJvd8y^;)WF_6t>-ojRb$6IWnHf+6u0R<8vsSk6B z3w_N|P^{ThfRG$*73A7cuI4)nSIC$L+5@yRXbTX}*ft08t26us4d0u*9dt8@|Jq!QC%bZK?12O3#dgM* z-#okR7an!cr1bT-&q~|s1DFyjD>I|=w~p>!DK6Ls2zGKJ%N0YPn?67_ONE7y8(orf zZ73zZ0`nDO1JR&>rhyIEToEJASfvp*J`{6>ScAv6g^lD!H?g7l#aNSrBj)7!Hbck} zH!;YBsZgvQ4L#{EH}<6Gd}vkr=dAvX}&GGGE{OS&uRNI&{ zt215jmYM1C57qmOr4<^uGmR2uVPhL4lqzIba!w4`05py!VY;hCa5Z9s9DRavvNIyY zDX?i6jLnWe#|594F-SI^g}^C7uGY<{#8S4oK$MFxn-n0{Yq@?d=6nOt9Aa*?X*|;% zA?)n$u1#+^cePy&Y7@bi$ZrFk+n;7AjOkCG#}e>IelOT_LH)XE_q|KH(-YpjIKBDY z2h)1{qBmD4DXdNdWSX0SF}Bz}o^h0oI}ujO>p3%Lma|Yso&y0{^$fvh_G%5*hfQXy zMcoZ*67uJ!krSL(>{5wREy#B4B-sjc(_G5(<>LfbEg}0N+!kFX<3VFs83nj8*3vxl zF*xNKw0n#pBu%hMCyv^d?fn4bq}@q))i8#(Q^kQt+(0P~SM1@H?yNJW5KD-D1;po3 z{)lKZxpFK7Wrg*s(MQc}c3FD1193cagh5U8;5;Td$aL8dm`k|L#&;RMul{UpI`E|{ z(yy-G<0)RjK{`52OjJ(Jl zu>la$x!9|*+zi?T6wL1w8$d*?VX_r2xQ#_ep{i6NL&&+}M3e!=lw!je#)D6y5a&(V zbOf2X5=>R$9%c_5TX}S-W}cIys^=Wa6d^0d+-#lHs~iao&(BZmcbQyd|{BDfLkNDx2q?icRy zm(%aPaCus{&KE7AMJ?Fog3&m2kcC3XGW~8V1i&0nr3NLUV<;i`s>n4I6LJVIWonk8=x=85ShE*m zE)&BDv&&fJ0dX#xyK%&tiieyAnKs37ZuEJ4Pj^rH++}Oi-Y;5_F8$pmpB?gF^EZKx z^t~`_sGH&l0u}==bMzPxzvSI7ti#%T$CsC-BmQS`y7oWoiUk}OzZSe~f~mIRgRc@( z)pH#3@)#u9!C6vda9fUpV!GRjoDY~S=#rHiV^urC{Iq*)X=4XaH9N_DIm8{Rl@a7z zuNng|U5MjSmd|7;5HD^~d+}vLAl|l5=wuVzaR_Dlt@J zsVuuvD&<0$)3<)RE}j1QHR-l{bI_*XZqSR+41Z9lgNynrIb#G^FuaJw!$5yFLqEZb zO7N`lLl>>|ukH}waRcTZ(gbql4MGk^Is2Swi`3X<1>(jR7fe?{AiG0caF^;-Wm0KQ zE>jR!4#9cFafBFC6e~otlV}53O~A}JgOXeM=66{(a^;biF3<~~hnb)_gYxqOn1aD6 zXB~;A7ox&V$TA$}qX87hg6$u8sr-butxC^$&+36{i2SO}enZs|+eO_JM>w!>crD3C zfcP?_-{8-!-u#7S=@GA5lKy-{yc3!hd1bNZO$B5Tf^8V*+C18H1{kfuhy$ucHP$sq zC%1r#EatMnI;u~m+HT5RY4SG>fE?##uApbR8biQV&E>p|yG*~$&1HfWTTZUjffCaP~bQ;7{LH+uox4^T;!(O&Ho%XQ@(|t>OZ-l%#w0X2?@@6f>9A|#p zRW7?N>t4u1%Ag@STsJiYdkKTajm<_d8###F#t*syg5{9tAhHvnpWJ=9zU&mE42t6s z=64b_-)6{W2zoBFv654&0Xgf)i(`;cX1NT;laU9X$&J8eD|^y=zrH5z`NEayOaHqz zHM;cpl*tz2v|_OsyDy3bG;e*2`=(;f3Paed)}h-sc)~aPptE1 z${WQW)MyC$D3fagMov^VSgz}=s82-(Ac&@f^dcXmIvUT$kq&O;F@)6!u@+#)2BMmC zW%QhQERxAcv837XVDoaL#ZY%nmLR;vdI8L+@IBY=EFm2T(?_D zjB{CWLT&)C#b#hJ9v94o9R_o`-$ErjXHK+{WHW9a2NlK?S^?=AyyPsy=FNDIvFDxw z9_Z3|#yw7U9kXnby%zsbJnzTr?9A}APp(OKEh_8-0AAATK&;shfCl9C_fVj2ilY!% zV9P--g~~6UPZVvAHS2oPm(UFNe(}om!jG-7TLcUc_nv+I@Ro;SA6k$;}|h+-qjygKxYHkBw*+ zRssSn#tSPxazilWxi}Z9xO3(nb=b$pIJ~>F^l;-MkR0)eGmt#ZdNsAs#>6{;|O`p1Soqe)Az~Hq= zUxsGLi!zTm)ZbGahrnXwr+U0UHs8nDWbjS*&wO`P+Vwe$(`g@Hp6*NCYL2AnZ+OYjjf*jB^z7Pw+{WdPRn+#EIhQ8`n$LxhZe5LWC;1TiKPG?`bHvRBV>j&yk4(`qJH^YgUxYEF#Zev0+6=aCWHI$ zn1d&#S3h-fdg8+;qcGQxZ&pH^xwNU9$R$oYNL-5P!J+uRpqd$d#Foac)UBZkFuUS{9-&-APr7??N?-Zuy7Yw~ts9K@3x$7xPDeBRNuiE> z)J?HQ2Abp1V4nx=ZH58F{N20KYo0bGJ?qhv(&S0n(E-&BuccPC8D}g6TaVzf5!oK$ zamuR>B2~!sAvd#2Ziu`qwZ4fSo5!OJY8GFbk9Y-gHH6-< z+7!Hom7xuKNe-NdGU4f0|GPU~@UwO48^2mN(6KtBy~g8E*PzWQPHDQ>@Vx zXpTFAy$8hi90%n?wnOGkNY8)Vq;$%oC#J1tVTicAY%|Br&CN~$57mv|Cm7?gtQQ-M zi-p}`af)|N^fjK~(>4F; zJ8po$U!!~$^nNtM5&K)^R!}#^8XE+f)wfbNpcw)AJucIX(W+32EvS zZhCjxzV`-@p{^2jGsy;-@UUDnHyg?YE6*cvLodUIk>}%RI+qG3N-`CP;s}#_ z(%)|GNta%+E?xTj?sV0SL-NBA{=S~K19&%@;eX9C%27AP8Z80M@v&fhS;VjM3>r2= zLwwvrCZrP|J~5r}Fq}co?ls~%m)Q{PL~Nqvzy|sn zt2AZ+)oc#2Rk(fi+Me{=YrE5r{(fs_zy5o7TCuuXXi&kI#-9Uy3e9lH@18dl zsGDMqwt?n25$stYUeRPP+J*u<%;`)=AJCN^dEkU})BzLHK0EO&QjLZYe1OblYC50> z(@Z8jr8i1Niad*J8r%mwHkKl)I-$Acc@;7*z;M+~-RXD#SP$w>zrAMtV1H237ybkK z1n7&S^#hbvQ8&dJ{f3`8J`?m7&;hg!CFaiRC>rGc6Vf5`I@5u>cBL89&`j8H)MSX* za5ASI6giP&TyF>)N32U!#}q6^b{YzqU!uP0jvo7@_%AmWP4NG&$C==&kUR9?9V6Kc zzlmlzs=iv&U(`*p#wG*Jk=vXf#PH1VaN33g>@mA;)0yV))|KY(UNp%Ack4_$&FxGR z*}h|Qvc^=1g-|jC^kI+9j=Y3E>bZG6&i-z@x7)rjeckQdXo75n>+KWb<-^+m1%7z) zJrKWE!wbHSE7VP~#()A1lDA!bIfz%{nLyicgYQl9rT5NrJJK#&ccxvp?F8>gJ8adN zwwl?IX3qdk??}__TbXKRm8KS(UzWiw6teFP-pb+LrOAH(O@8l^Zu|Z4t@q$8@Xns} z?>oEgvRi}rrRhxp@1e{u@1KWexX3Ky5Oq_mF(^QD+y;yv%CJEW`iC+D0e&?|4RY#a zHq(wYgVzzV#>WWg(^7h11%DX=zPz~3;PO=|tz6xc9$cC1^8BL=n6>>bnHLH9Ihx_9 zJAi>k-4tsKD*Qtko;AJ<^k|TuZq*n@a3lW)bRLNB6)vN(MuEC1))+FNLGB6mY!E-W zo=01aAp&sb^{;HJD5^b61rLBB#H+@$+`k9X8fvBro6nj|lN`#2Drg@6`ui$O>`EB9|rNu&b(bGKj~c)IPLd=_#PiQFSA9?OKjZ^ nx*3g+zaXhGg3)?YOzHmvQ=*h212t+r00000NkvXXu0mjfM|Q5M literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/uoftflag_2.png b/res/tetrio_badges/uoftflag_2.png new file mode 100644 index 0000000000000000000000000000000000000000..4113f0bf5c6b3b714ec03dc45d1da8f532197ad7 GIT binary patch literal 25582 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 KH+Zc_qWlkOC70Cz literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/uoftflag_3.png b/res/tetrio_badges/uoftflag_3.png new file mode 100644 index 0000000000000000000000000000000000000000..d26430c81a893b02029dad74e5e566455b0231c6 GIT binary patch literal 28203 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 Q_~0AJ*wD#oe;~OTpa1{> literal 0 HcmV?d00001