From e403b0cbee60dbd4f7dc9732a8647e57c98f07ab Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 6 Sep 2024 00:42:21 +0300 Subject: [PATCH 01/86] big and scary refactoring --- analysis_options.yaml | 5 +- lib/data_objects/achievement.dart | 98 + lib/data_objects/aggregate_stats.dart | 29 + lib/data_objects/badge.dart | 34 + .../beta_league_leaderboard_entry.dart | 21 + lib/data_objects/beta_league_results.dart | 24 + lib/data_objects/beta_league_round.dart | 25 + lib/data_objects/beta_league_stats.dart | 43 + lib/data_objects/beta_record.dart | 25 + lib/data_objects/clears.dart | 102 + lib/data_objects/connections.dart | 43 + lib/data_objects/cutoff_tetrio.dart | 42 + lib/data_objects/distinguishment.dart | 29 + lib/data_objects/end_context_multi.dart | 97 + lib/data_objects/est_tr.dart | 39 + lib/data_objects/finesse.dart | 29 + lib/data_objects/handling.dart | 32 + lib/data_objects/leaderboard_position.dart | 8 + lib/data_objects/nerd_stats.dart | 28 + lib/data_objects/news.dart | 15 + lib/data_objects/news_entry.dart | 15 + .../{tetra_stats.dart => p1nkl0bst3r.dart} | 0 .../player_leaderboard_position.dart | 63 + lib/data_objects/playstyle.dart | 25 + lib/data_objects/record_extras.dart | 15 + lib/data_objects/record_single.dart | 50 + lib/data_objects/results_stats.dart | 82 + lib/data_objects/singleplayer_stream.dart | 18 + lib/data_objects/summaries.dart | 39 + lib/data_objects/tetra_league.dart | 139 + .../tetra_league_alpha_record.dart | 39 + .../tetra_league_alpha_stream.dart | 16 + .../tetra_league_beta_stream.dart | 111 + lib/data_objects/tetrio.dart | 2600 ----------------- lib/data_objects/tetrio_constants.dart | 188 ++ .../tetrio_multiplayer_replay.dart | 7 +- lib/data_objects/tetrio_player.dart | 150 + .../tetrio_player_from_leaderboard.dart | 145 + .../tetrio_players_leaderboard.dart | 759 +++++ lib/data_objects/tetrio_zen.dart | 24 + lib/data_objects/user_records.dart | 13 + lib/data_objects/zenith_results.dart | 36 + lib/main.dart | 2 +- lib/services/tetrio_crud.dart | 21 +- lib/views/calc_view.dart | 4 +- lib/views/compare_view.dart | 5 +- lib/views/main_view.dart | 25 +- lib/views/main_view_tiles.dart | 105 +- lib/views/rank_averages_view.dart | 4 +- lib/views/ranks_averages_view.dart | 3 +- lib/views/settings_view.dart | 2 +- lib/views/singleplayer_record_view.dart | 2 +- lib/views/sprint_and_blitz_averages.dart | 2 +- lib/views/state_view.dart | 9 +- lib/views/states_view.dart | 2 +- lib/views/tl_leaderboard_view.dart | 2 +- lib/views/tl_match_view.dart | 4 +- lib/views/tracked_players_view.dart | 2 - lib/views/zenith_record_view.dart | 2 +- lib/widgets/finesse_thingy.dart | 2 +- lib/widgets/gauget_num.dart | 2 +- lib/widgets/graphs.dart | 4 +- lib/widgets/lineclears_thingy.dart | 2 +- lib/widgets/recent_sp_games.dart | 3 +- lib/widgets/singleplayer_record.dart | 4 +- lib/widgets/sp_trailing_stats.dart | 2 +- lib/widgets/stat_sell_num.dart | 2 +- lib/widgets/tl_progress_bar.dart | 2 +- lib/widgets/tl_rating_thingy.dart | 2 +- lib/widgets/tl_thingy.dart | 12 +- lib/widgets/user_thingy.dart | 2 +- lib/widgets/vs_graphs.dart | 4 +- lib/widgets/zenith_thingy.dart | 3 +- 73 files changed, 2869 insertions(+), 2675 deletions(-) create mode 100644 lib/data_objects/achievement.dart create mode 100644 lib/data_objects/aggregate_stats.dart create mode 100644 lib/data_objects/badge.dart create mode 100644 lib/data_objects/beta_league_leaderboard_entry.dart create mode 100644 lib/data_objects/beta_league_results.dart create mode 100644 lib/data_objects/beta_league_round.dart create mode 100644 lib/data_objects/beta_league_stats.dart create mode 100644 lib/data_objects/beta_record.dart create mode 100644 lib/data_objects/clears.dart create mode 100644 lib/data_objects/connections.dart create mode 100644 lib/data_objects/cutoff_tetrio.dart create mode 100644 lib/data_objects/distinguishment.dart create mode 100644 lib/data_objects/end_context_multi.dart create mode 100644 lib/data_objects/est_tr.dart create mode 100644 lib/data_objects/finesse.dart create mode 100644 lib/data_objects/handling.dart create mode 100644 lib/data_objects/leaderboard_position.dart create mode 100644 lib/data_objects/nerd_stats.dart create mode 100644 lib/data_objects/news.dart create mode 100644 lib/data_objects/news_entry.dart rename lib/data_objects/{tetra_stats.dart => p1nkl0bst3r.dart} (100%) create mode 100644 lib/data_objects/player_leaderboard_position.dart create mode 100644 lib/data_objects/playstyle.dart create mode 100644 lib/data_objects/record_extras.dart create mode 100644 lib/data_objects/record_single.dart create mode 100644 lib/data_objects/results_stats.dart create mode 100644 lib/data_objects/singleplayer_stream.dart create mode 100644 lib/data_objects/summaries.dart create mode 100644 lib/data_objects/tetra_league.dart create mode 100644 lib/data_objects/tetra_league_alpha_record.dart create mode 100644 lib/data_objects/tetra_league_alpha_stream.dart create mode 100644 lib/data_objects/tetra_league_beta_stream.dart delete mode 100644 lib/data_objects/tetrio.dart create mode 100644 lib/data_objects/tetrio_constants.dart create mode 100644 lib/data_objects/tetrio_player.dart create mode 100644 lib/data_objects/tetrio_player_from_leaderboard.dart create mode 100644 lib/data_objects/tetrio_players_leaderboard.dart create mode 100644 lib/data_objects/tetrio_zen.dart create mode 100644 lib/data_objects/user_records.dart create mode 100644 lib/data_objects/zenith_results.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index a7acf24..f3cece1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,10 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +analyzer: + errors: + use_build_context_synchronously: ignore +in ignoreclude: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the diff --git a/lib/data_objects/achievement.dart b/lib/data_objects/achievement.dart new file mode 100644 index 0000000..d3794a3 --- /dev/null +++ b/lib/data_objects/achievement.dart @@ -0,0 +1,98 @@ +// ignore_for_file: hash_and_equals + +class Achievement { + late int k; + int? o; + late int rt; + late int vt; + late int min; + late int deci; + late String name; + late String object; + late String category; + late bool hidden; + late int art; + late bool nolb; + late String desc; + late String n; + String? sId; + double? v; + late int? a; + DateTime? t; + int? pos; + int? total; + int? rank; + + Achievement( + {required this.k, + this.o, + required this.rt, + required this.vt, + required this.min, + required this.deci, + required this.name, + required this.object, + required this.category, + required this.hidden, + required this.art, + required this.nolb, + required this.desc, + required this.n, + this.sId, + this.v, + required this.a, + this.t, + this.pos, + this.total, + this.rank}); + + Achievement.fromJson(Map json) { + k = json['k']; + o = json['o']; + rt = json['rt']; + vt = json['vt']; + min = json['min']; + deci = json['deci']; + name = json['name']; + object = json['object']; + category = json['category']; + hidden = json['hidden']; + art = json['art']; + nolb = json['nolb']; + desc = json['desc']; + n = json['n']; + sId = json['_id']; + v = json['v']?.toDouble(); + a = json['a']; + t = json['t'] != null ? DateTime.parse(json['t']) : null; + pos = json['pos']; + total = json['total']; + rank = json['rank']; + } + + Map toJson() { + final Map data = {}; + data['k'] = k; + data['o'] = o; + data['rt'] = rt; + data['vt'] = vt; + data['min'] = min; + data['deci'] = deci; + data['name'] = name; + data['object'] = object; + data['category'] = category; + data['hidden'] = hidden; + data['art'] = art; + data['nolb'] = nolb; + data['desc'] = desc; + data['n'] = n; + data['_id'] = sId; + data['v'] = v; + data['a'] = a; + data['t'] = t.toString(); + data['pos'] = pos; + data['total'] = total; + data['rank'] = rank; + return data; + } +} diff --git a/lib/data_objects/aggregate_stats.dart b/lib/data_objects/aggregate_stats.dart new file mode 100644 index 0000000..2e16afe --- /dev/null +++ b/lib/data_objects/aggregate_stats.dart @@ -0,0 +1,29 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; + +class AggregateStats{ + late double apm; + late double pps; + late double vs; + late NerdStats nerdStats; + late EstTr estTr; + late Playstyle playstyle; + + AggregateStats(this.apm, this.pps, this.vs){ + 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); + } + + AggregateStats.fromJson(Map json){ + apm = json['apm'] != null ? json['apm'].toDouble() : 0.00; + pps = json['apm'] != null ? json['pps'].toDouble() : 0.00; + vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00; + 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); + } +} diff --git a/lib/data_objects/badge.dart b/lib/data_objects/badge.dart new file mode 100644 index 0000000..69976e6 --- /dev/null +++ b/lib/data_objects/badge.dart @@ -0,0 +1,34 @@ +// ignore_for_file: hash_and_equals + +class Badge { + late String badgeId; + late String label; + DateTime? ts; + + Badge({required this.badgeId, required this.label, this.ts}); + + Badge.fromJson(Map json) { + badgeId = json['id']; + label = json['label']; + ts = (json['ts'] != null && json['ts'] is String) ? DateTime.parse(json['ts']) : null; // man i love osk + } + + Map toJson() { + final Map data = {}; + data['id'] = badgeId; + data['label'] = label; + data['ts'] = ts?.toString(); + return data; + } + + @override + String toString() { + return "Badge $label ($badgeId)"; + } + + @override + int get hashCode => badgeId.hashCode; + + @override + bool operator ==(covariant Badge other) => badgeId == other.badgeId; +} diff --git a/lib/data_objects/beta_league_leaderboard_entry.dart b/lib/data_objects/beta_league_leaderboard_entry.dart new file mode 100644 index 0000000..34672b4 --- /dev/null +++ b/lib/data_objects/beta_league_leaderboard_entry.dart @@ -0,0 +1,21 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/beta_league_stats.dart'; + +class BetaLeagueLeaderboardEntry{ + late String id; + late String username; + late int naturalorder; + late int wins; + late BetaLeagueStats stats; + + BetaLeagueLeaderboardEntry({required this.id, required this.username, required this.naturalorder, required this.wins, required this.stats}); + + BetaLeagueLeaderboardEntry.fromJson(Map json){ + id = json['id']; + username = json['username']; + naturalorder = json['naturalorder']; + wins = json['wins']; + stats = BetaLeagueStats.fromJson(json['stats']); + } +} diff --git a/lib/data_objects/beta_league_results.dart b/lib/data_objects/beta_league_results.dart new file mode 100644 index 0000000..2d4ad50 --- /dev/null +++ b/lib/data_objects/beta_league_results.dart @@ -0,0 +1,24 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/beta_league_leaderboard_entry.dart'; +import 'package:tetra_stats/data_objects/beta_league_round.dart'; + +class BetaLeagueResults{ + List leaderboard = []; + List> rounds = []; + + BetaLeagueResults({required this.leaderboard, required this.rounds}); + + BetaLeagueResults.fromJson(Map json){ + for (var lbEntry in json['leaderboard']) { + leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry)); + } + for (var roundEntry in json['rounds']){ + List round = []; + for (var r in roundEntry) { + round.add(BetaLeagueRound.fromJson(r)); + } + rounds.add(round); + } + } +} diff --git a/lib/data_objects/beta_league_round.dart b/lib/data_objects/beta_league_round.dart new file mode 100644 index 0000000..fdaf81b --- /dev/null +++ b/lib/data_objects/beta_league_round.dart @@ -0,0 +1,25 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/beta_league_stats.dart'; + +class BetaLeagueRound{ + late String id; + late String username; + late bool active; + late int naturalorder; + late bool alive; + late Duration lifetime; + late BetaLeagueStats stats; + + BetaLeagueRound({required this.id, required this.username, required this.active, required this.naturalorder, required this.alive, required this.lifetime, required this.stats}); + + BetaLeagueRound.fromJson(Map json){ + id = json['id']; + username = json['username']; + active = json['active']; + naturalorder = json['naturalorder']; + alive = json['alive']; + lifetime = Duration(milliseconds: json['lifetime']); + stats = BetaLeagueStats.fromJson(json['stats']); + } +} diff --git a/lib/data_objects/beta_league_stats.dart b/lib/data_objects/beta_league_stats.dart new file mode 100644 index 0000000..88e1c56 --- /dev/null +++ b/lib/data_objects/beta_league_stats.dart @@ -0,0 +1,43 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; + +class BetaLeagueStats{ + late double apm; + late double pps; + late double vs; + late int garbageSent; + late int garbageReceived; + late int kills; + late double altitude; + late int rank; + int? targetingFactor; + int? targetingRace; + late NerdStats nerdStats; + late EstTr estTr; + late Playstyle playstyle; + + BetaLeagueStats({required this.apm, required this.pps, required this.vs, required this.garbageSent, required this.garbageReceived, required this.kills, required this.altitude, required this.rank}){ + 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); + } + + BetaLeagueStats.fromJson(Map json){ + apm = json['apm'] != null ? json['apm'].toDouble() : 0.00; + pps = json['apm'] != null ? json['pps'].toDouble() : 0.00; + vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00; + garbageSent = json['garbagesent']; + garbageReceived = json['garbagereceived']; + kills = json['kills']; + altitude = json['altitude'].toDouble(); + rank = json['rank']; + targetingFactor = json['targetingfactor']; + targetingRace = json['targetinggrace']; + 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); + } +} diff --git a/lib/data_objects/beta_record.dart b/lib/data_objects/beta_record.dart new file mode 100644 index 0000000..d50bba5 --- /dev/null +++ b/lib/data_objects/beta_record.dart @@ -0,0 +1,25 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/beta_league_results.dart'; + +class BetaRecord{ + late String id; + late String replayID; + late String gamemode; + late DateTime ts; + late String enemyUsername; + late String enemyID; + late BetaLeagueResults results; + + BetaRecord({required this.id, required this.replayID, required this.gamemode, required this.ts, required this.enemyUsername, required this.enemyID, required this.results}); + + BetaRecord.fromJson(Map json){ + id = json['_id']; + replayID = json['replayid']; + gamemode = json['gamemode']; + ts = DateTime.parse(json['ts']); + enemyUsername = json['otherusers'][0]['username']; + enemyID = json['otherusers'][0]['id']; + results = BetaLeagueResults.fromJson(json['results']); + } +} diff --git a/lib/data_objects/clears.dart b/lib/data_objects/clears.dart new file mode 100644 index 0000000..c63a72a --- /dev/null +++ b/lib/data_objects/clears.dart @@ -0,0 +1,102 @@ +// ignore_for_file: hash_and_equals + +class Clears { + late int singles; + late int doubles; + late int triples; + late int quads; + late int pentas; + late int allClears; + late int tSpinZeros; + late int tSpinSingles; + late int tSpinDoubles; + late int tSpinTriples; + late int tSpinQuads; + late int tSpinPentas; + late int tSpinMiniZeros; + late int tSpinMiniSingles; + late int tSpinMiniDoubles; + late int tSpinMiniTriples; + late int tSpinMiniQuads; + + Clears( + {required this.singles, + required this.doubles, + required this.triples, + required this.quads, + required this.pentas, + required this.allClears, + required this.tSpinZeros, + required this.tSpinSingles, + required this.tSpinDoubles, + required this.tSpinTriples, + required this.tSpinPentas, + required this.tSpinQuads, + required this.tSpinMiniZeros, + required this.tSpinMiniSingles, + required this.tSpinMiniDoubles, + required this.tSpinMiniTriples, + required this.tSpinMiniQuads}); + + Clears.fromJson(Map json) { + singles = json['singles']; + doubles = json['doubles']; + triples = json['triples']; + quads = json['quads']; + pentas = json['pentas']??0; + tSpinZeros = json['realtspins']; + tSpinMiniZeros = json['minitspins']; + tSpinMiniSingles = json['minitspinsingles']; + tSpinSingles = json['tspinsingles']; + tSpinMiniDoubles = json['minitspindoubles']; + tSpinDoubles = json['tspindoubles']; + tSpinMiniTriples = json['minitspintriples']??0; + tSpinTriples = json['tspintriples']; + tSpinMiniQuads = json['minitspinquads']??0; + tSpinQuads = json['tspinquads']; + tSpinPentas = json['tspinpentas']??0; + allClears = json['allclear']; + } + + Clears operator + (Clears other){ + return Clears( + singles: singles + other.singles, + doubles: doubles + other.doubles, + triples: triples + other.triples, + quads: quads + other.quads, + pentas: pentas + other.pentas, + allClears: allClears + other.allClears, + tSpinZeros: tSpinZeros + other.tSpinZeros, + tSpinSingles: tSpinSingles + other.tSpinSingles, + tSpinDoubles: tSpinDoubles + other.tSpinDoubles, + tSpinTriples: tSpinTriples + other.tSpinTriples, + tSpinPentas: tSpinPentas + other.tSpinPentas, + tSpinQuads: tSpinQuads + other.tSpinQuads, + tSpinMiniZeros: tSpinMiniZeros + other.tSpinMiniZeros, + tSpinMiniSingles: tSpinMiniSingles + other.tSpinMiniSingles, + tSpinMiniDoubles: tSpinMiniDoubles + other.tSpinMiniDoubles, + tSpinMiniTriples: tSpinMiniTriples + other.tSpinMiniTriples, + tSpinMiniQuads: tSpinMiniQuads + other.tSpinMiniQuads + ); + } + + Map toJson() { + final Map data = {}; + data['singles'] = singles; + data['doubles'] = doubles; + data['triples'] = triples; + data['quads'] = quads; + data['pentas'] = pentas; + data['realtspins'] = tSpinZeros; + data['minitspins'] = tSpinMiniZeros; + data['minitspinsingles'] = tSpinMiniSingles; + data['tspinsingles'] = tSpinSingles; + data['minitspindoubles'] = tSpinMiniDoubles; + data['tspindoubles'] = tSpinDoubles; + data['tspintriples'] = tSpinTriples; + data['tspinquads'] = tSpinQuads; + data['tspinpentas'] = tSpinPentas; + data['allclear'] = allClears; + return data; + } +} diff --git a/lib/data_objects/connections.dart b/lib/data_objects/connections.dart new file mode 100644 index 0000000..ca0ae83 --- /dev/null +++ b/lib/data_objects/connections.dart @@ -0,0 +1,43 @@ +// ignore_for_file: hash_and_equals + +class Connections { + Discord? discord; + + Connections({this.discord}); + + Connections.fromJson(Map json) { + discord = json['discord'] != null ? Discord.fromJson(json['discord']) : null; + } + @override + bool operator ==(covariant Connections other) => discord == other.discord; + + Map toJson() { + final Map data = {}; + if (discord != null) { + data['discord'] = discord!.toJson(); + } + return data; + } +} + +class Discord { + late String id; + late String username; + + Discord({required this.id, required this.username}); + + Discord.fromJson(Map json) { + id = json['id']; + username = json['username']; + } + + @override + bool operator ==(covariant Discord other) => id == other.id; + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['username'] = username; + return data; + } +} diff --git a/lib/data_objects/cutoff_tetrio.dart b/lib/data_objects/cutoff_tetrio.dart new file mode 100644 index 0000000..586192a --- /dev/null +++ b/lib/data_objects/cutoff_tetrio.dart @@ -0,0 +1,42 @@ +// ignore_for_file: hash_and_equals + +class CutoffTetrio { + late int pos; + late double percentile; + late double tr; + late double targetTr; + late double apm; + late double pps; + late double vs; + late int count; + late double countPercentile; + + CutoffTetrio.fromJson(Map json, int total){ + pos = json['pos']; + percentile = json['percentile'].toDouble(); + tr = json['tr'].toDouble(); + targetTr = json['targettr'].toDouble(); + apm = json['apm'].toDouble(); + pps = json['pps'].toDouble(); + vs = json['vs'].toDouble(); + count = json['count']; + countPercentile = count / total; + } +} + +class CutoffsTetrio { + late String id; + late DateTime timestamp; + late int total; + Map data = {}; + + CutoffsTetrio.fromJson(Map json){ + id = json['s']; + timestamp = DateTime.parse(json['t']); + total = json['data']['total']; + json['data'].remove("total"); + for (String rank in json['data'].keys){ + data[rank] = CutoffTetrio.fromJson(json['data'][rank], total); + } + } +} \ No newline at end of file diff --git a/lib/data_objects/distinguishment.dart b/lib/data_objects/distinguishment.dart new file mode 100644 index 0000000..143ad17 --- /dev/null +++ b/lib/data_objects/distinguishment.dart @@ -0,0 +1,29 @@ +// ignore_for_file: hash_and_equals + +class Distinguishment { + late String type; + String? detail; + String? header; + String? footer; + + Distinguishment({required this.type, this.detail, this.header, this.footer}); + + Distinguishment.fromJson(Map json) { + type = json['type']; + detail = json['detail']; + header = json['header']; + footer = json['footer']; + } + + @override + bool operator ==(covariant Distinguishment other) => type == other.type && detail == other.detail && header == other.header && footer == other.footer; + + Map toJson() { + final Map data = {}; + data['type'] = type; + data['detail'] = detail; + data['header'] = header; + data['footer'] = footer; + return data; + } +} diff --git a/lib/data_objects/end_context_multi.dart b/lib/data_objects/end_context_multi.dart new file mode 100644 index 0000000..d92c2d3 --- /dev/null +++ b/lib/data_objects/end_context_multi.dart @@ -0,0 +1,97 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/handling.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; + +class EndContextMulti { + late String userId; + late String username; + late int naturalOrder; + late int inputs; + late int piecesPlaced; + late Handling handling; + late int points; + late int wins; + late double secondary; + late List secondaryTracking; + late double tertiary; + late List tertiaryTracking; + late double extra; + late List extraTracking; + late bool success; + late NerdStats nerdStats; + late List nerdStatsTracking; + late EstTr estTr; + late List estTrTracking; + late Playstyle playstyle; + late List playstyleTracking; + + EndContextMulti( + {required this.userId, + required this.username, + required this.naturalOrder, + required this.inputs, + required this.piecesPlaced, + required this.handling, + required this.points, + required this.wins, + required this.secondary, + required this.secondaryTracking, + required this.tertiary, + required this.tertiaryTracking, + required this.extra, + required this.extraTracking, + required this.success}){ + nerdStats = NerdStats(secondary, tertiary, extra); + nerdStatsTracking = [for (int i = 0; i < secondaryTracking.length; i++) NerdStats(secondaryTracking[i], tertiaryTracking[i], extraTracking[i])]; + estTr = EstTr(secondary, tertiary, extra, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); + estTrTracking = [for (int i = 0; i < secondaryTracking.length; i++) EstTr(secondaryTracking[i], tertiaryTracking[i], extraTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].dss, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe)]; + playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); + playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)]; + } + + EndContextMulti.fromJson(Map json) { + userId = json['id'] ?? json['user']['_id']; + username = json['username'] ?? json['user']['username']; + handling = json['handling'] != null ? Handling.fromJson(json['handling']) : Handling(arr: -1, das: -1, sdf: -1, dcd: 0, cancel: true, safeLock: true); + success = json['success']; + inputs = json['inputs'] ?? -1; + piecesPlaced = json['piecesplaced'] ?? -1; + naturalOrder = json['naturalorder']; + wins = json['wins']; + points = json['points']['primary']; + secondary = json['points']['secondary'].toDouble(); + tertiary = json['points']['tertiary'].toDouble(); + secondaryTracking = json['points']['secondaryAvgTracking'] != null ? json['points']['secondaryAvgTracking'].map((e) => e.toDouble()).toList() : []; + tertiaryTracking = json['points']['tertiaryAvgTracking'] != null ? json['points']['tertiaryAvgTracking'].map((e) => e.toDouble()).toList() : []; + extra = json['points']['extra']['vs'].toDouble(); + extraTracking = json['points']['extraAvgTracking'] != null ? json['points']['extraAvgTracking']['aggregatestats___vsscore'].map((e) => e.toDouble()).toList() : []; + nerdStats = NerdStats(secondary, tertiary, extra); + nerdStatsTracking = [for (int i = 0; i < secondaryTracking.length; i++) NerdStats(secondaryTracking[i], tertiaryTracking[i], extraTracking[i])]; + estTr = EstTr(secondary, tertiary, extra, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); + estTrTracking = [for (int i = 0; i < secondaryTracking.length; i++) EstTr(secondaryTracking[i], tertiaryTracking[i], extraTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].dss, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe)]; + playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); + playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)]; + } + + @override + bool operator == (covariant EndContextMulti other){ + if (userId != other.userId) return false; + return true; + } + + Map toJson() { + final Map data = {}; + data['user'] = {'_id': userId, 'username': username}; + data['handling'] = handling.toJson(); + data['success'] = success; + data['inputs'] = inputs; + data['piecesplaced'] = piecesPlaced; + data['naturalorder'] = naturalOrder; + data['wins'] = wins; + data['points'] = {'primary': points, 'secondary': secondary, 'tertiary':tertiary, 'extra': {'vs': extra}, 'secondaryAvgTracking': secondaryTracking, 'tertiaryAvgTracking': tertiaryTracking, 'extraAvgTracking': {'aggregatestats___vsscore': extraTracking}}; + return data; + } +} diff --git a/lib/data_objects/est_tr.dart b/lib/data_objects/est_tr.dart new file mode 100644 index 0000000..d378a4a --- /dev/null +++ b/lib/data_objects/est_tr.dart @@ -0,0 +1,39 @@ +// ignore_for_file: hash_and_equals + +import 'dart:math'; + +class EstTr { + late double esttr; + late double srarea; + late double statrank; + late double estglicko; + + EstTr(double apm, double pps, double vs, double app, double dss, double dsp, double gbe) { + srarea = (apm * 0) + (pps * 135) + (vs * 0) + (app * 290) + (dss * 0) + (dsp * 700) + (gbe * 0); + statrank = 11.2 * atan((srarea - 93) / 130) + 1; + if (statrank <= 0) statrank = 0.001; + //estglicko = (4.0867 * srarea + 186.68); + double ntemp = pps*(150+(((vs/apm) - 1.66)*35))+app*290+dsp*700; + estglicko = 0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4; + esttr = 25000 / + ( + 1 + pow(10, ( + ( + ( + 1500-estglicko + )*pi + )/sqrt( + ( + ( + 3*pow(ln10, 2) + )*pow(60, 2) + )+( + 2500*( + (64*pow(pi,2))+(147*pow(ln10, 2)) + ) + ) + ) + )) + ); + } +} diff --git a/lib/data_objects/finesse.dart b/lib/data_objects/finesse.dart new file mode 100644 index 0000000..6e982b5 --- /dev/null +++ b/lib/data_objects/finesse.dart @@ -0,0 +1,29 @@ +// ignore_for_file: hash_and_equals + +import 'dart:math'; + +class Finesse { + late int combo; + late int faults; + late int perfectPieces; + + Finesse({required this.combo, required this.faults, required this.perfectPieces}); + + Finesse.fromJson(Map json) { + combo = json['combo']; + faults = json['faults']; + perfectPieces = json['perfectpieces']; + } + + Finesse operator + (Finesse other){ + return Finesse(combo: max(combo, other.combo), faults: faults + other.faults, perfectPieces: perfectPieces + other.perfectPieces); + } + + Map toJson() { + final Map data = {}; + data['combo'] = combo; + data['faults'] = faults; + data['perfectpieces'] = perfectPieces; + return data; + } +} diff --git a/lib/data_objects/handling.dart b/lib/data_objects/handling.dart new file mode 100644 index 0000000..345f3e7 --- /dev/null +++ b/lib/data_objects/handling.dart @@ -0,0 +1,32 @@ +// ignore_for_file: hash_and_equals + +class Handling { + late num arr; + late num das; + late num sdf; + late num dcd; + late bool cancel; + late bool safeLock; + + Handling({required this.arr, required this.das, required this.sdf, required this.dcd, required this.cancel, required this.safeLock}); + + Handling.fromJson(Map json) { + arr = json['arr']; + das = json['das']; + dcd = json['dcd']; + sdf = json['sdf']; + safeLock = json['safelock']; + cancel = json['cancel']; + } + + Map toJson() { + final Map data = {}; + data['arr'] = arr.toDouble(); + data['das'] = das.toDouble(); + data['dcd'] = dcd.toDouble(); + data['sdf'] = sdf.toDouble(); + data['safelock'] = safeLock; + data['cancel'] = cancel; + return data; + } +} diff --git a/lib/data_objects/leaderboard_position.dart b/lib/data_objects/leaderboard_position.dart new file mode 100644 index 0000000..3d9e76f --- /dev/null +++ b/lib/data_objects/leaderboard_position.dart @@ -0,0 +1,8 @@ +// ignore_for_file: hash_and_equals + +class LeaderboardPosition{ + int position; + double percentage; + + LeaderboardPosition(this.position, this.percentage); +} diff --git a/lib/data_objects/nerd_stats.dart b/lib/data_objects/nerd_stats.dart new file mode 100644 index 0000000..6ad8730 --- /dev/null +++ b/lib/data_objects/nerd_stats.dart @@ -0,0 +1,28 @@ +// ignore_for_file: hash_and_equals + +import 'dart:math'; +import 'package:vector_math/vector_math.dart'; + +class NerdStats { + late double app; + late double vsapm; + late double dss; + late double dsp; + late double appdsp; + late double cheese; + late double gbe; + late double nyaapp; + late double area; + + NerdStats(double apm, double pps, double vs) { + app = apm / (pps * 60); + vsapm = vs / apm; + dss = (vs / 100) - (apm / 60); + dsp = ((vs / 100) - (apm / 60)) / pps; + appdsp = app + dsp; + cheese = (dsp * 150) + ((vsapm - 2) * 50) + (0.6 - app) * 125; + gbe = app * dsp * 2; + nyaapp = app - 5 * tan(radians((cheese / -30) + 1)); + area = apm * 1 + pps * 45 + vs * 0.444 + app * 185 + dss * 175 + dsp * 450 + gbe * 315; + } +} diff --git a/lib/data_objects/news.dart b/lib/data_objects/news.dart new file mode 100644 index 0000000..ccbca45 --- /dev/null +++ b/lib/data_objects/news.dart @@ -0,0 +1,15 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/news_entry.dart'; + +class News{ + late String id; + late List news; + + News(this.id, this.news); + + News.fromJson(Map json, String? userID){ + id = userID != null ? "user_$userID" : json['news'].first['stream']; + news = [for (var entry in json['news']) NewsEntry.fromJson(entry)]; + } +} diff --git a/lib/data_objects/news_entry.dart b/lib/data_objects/news_entry.dart new file mode 100644 index 0000000..6d125f1 --- /dev/null +++ b/lib/data_objects/news_entry.dart @@ -0,0 +1,15 @@ +// ignore_for_file: hash_and_equals + +class NewsEntry { + late String type; + late Map data; + late DateTime timestamp; + + NewsEntry({required this.type, required this.data, required this.timestamp}); + + NewsEntry.fromJson(Map json){ + type = json["type"]; + data = json["data"]; + timestamp = DateTime.parse(json['ts']); + } +} diff --git a/lib/data_objects/tetra_stats.dart b/lib/data_objects/p1nkl0bst3r.dart similarity index 100% rename from lib/data_objects/tetra_stats.dart rename to lib/data_objects/p1nkl0bst3r.dart diff --git a/lib/data_objects/player_leaderboard_position.dart b/lib/data_objects/player_leaderboard_position.dart new file mode 100644 index 0000000..4a79fa9 --- /dev/null +++ b/lib/data_objects/player_leaderboard_position.dart @@ -0,0 +1,63 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/leaderboard_position.dart'; + +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]; + } +} diff --git a/lib/data_objects/playstyle.dart b/lib/data_objects/playstyle.dart new file mode 100644 index 0000000..89cf43e --- /dev/null +++ b/lib/data_objects/playstyle.dart @@ -0,0 +1,25 @@ +// ignore_for_file: hash_and_equals + +import 'dart:math'; + +class Playstyle { + late double opener; + late double plonk; + late double stride; + late double infds; + + Playstyle(double apm, double pps, double app, double vsapm, double dsp, double gbe, double srarea, double statrank) { + double nmapm = ((apm / srarea) / ((0.069 * pow(1.0017, (pow(statrank, 5) / 4700))) + statrank / 360)) - 1; + double nmpps = ((pps / srarea) / (0.0084264 * pow(2.14, (-2 * (statrank / 2.7 + 1.03))) - statrank / 5750 + 0.0067)) - 1; + //double nmvs = ((vs / srarea) / (0.1333 * pow(1.0021, ((pow(statrank, 7) * (statrank / 16.5)) / 1400000)) + statrank / 133)) - 1; + double nmapp = (app / (0.1368803292 * pow(1.0024, (pow(statrank, 5) / 2800)) + statrank / 54)) - 1; + //double nmdss = (dss / (0.01436466667 * pow(4.1, ((statrank - 9.6) / 2.9)) + statrank / 140 + 0.01)) - 1; + double nmdsp = (dsp / (0.02136327583 * pow(14, ((statrank - 14.75) / 3.9)) + statrank / 152 + 0.022)) - 1; + double nmgbe = (gbe / (statrank / 350 + 0.005948424455 * pow(3.8, ((statrank - 6.1) / 4)) + 0.006)) - 1; + double nmvsapm = (vsapm / (-pow(((statrank - 16) / 36), 2) + 2.133)) - 1; + opener = ((nmapm + nmpps * 0.75 + nmvsapm * -10 + nmapp * 0.75 + nmdsp * -0.25) / 3.5) + 0.5; + plonk = ((nmgbe + nmapp + nmdsp * 0.75 + nmpps * -1) / 2.73) + 0.5; + stride = ((nmapm * -0.25 + nmpps + nmapp * -2 + nmdsp * -0.5) * 0.79) + 0.5; + infds = ((nmdsp + nmapp * -0.75 + nmapm * 0.5 + nmvsapm * 1.5 + nmpps * 0.5) * 0.9) + 0.5; + } +} diff --git a/lib/data_objects/record_extras.dart b/lib/data_objects/record_extras.dart new file mode 100644 index 0000000..4b85b7d --- /dev/null +++ b/lib/data_objects/record_extras.dart @@ -0,0 +1,15 @@ +// ignore_for_file: hash_and_equals + +class RecordExtras{ + +} + +class ZenithExtras extends RecordExtras{ + List mods = []; + + ZenithExtras.fromJson(Map json){ + for (var mod in json["mods"]) { + mods.add(mod); + } + } +} diff --git a/lib/data_objects/record_single.dart b/lib/data_objects/record_single.dart new file mode 100644 index 0000000..92eb304 --- /dev/null +++ b/lib/data_objects/record_single.dart @@ -0,0 +1,50 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/aggregate_stats.dart'; +import 'package:tetra_stats/data_objects/record_extras.dart'; +import 'package:tetra_stats/data_objects/results_stats.dart'; + +class RecordSingle { + late String? userId; + late String replayId; + late String ownId; + late String gamemode; + late DateTime timestamp; + late ResultsStats stats; + late int rank; + late int countryRank; + late AggregateStats aggregateStats; + late RecordExtras extras; + + RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); + + RecordSingle.fromJson(Map json, int ran, int cran) { + ownId = json['_id']; + gamemode = json['gamemode']; + stats = ResultsStats.fromJson(json['results']['stats']); + replayId = json['replayid']; + timestamp = DateTime.parse(json['ts']); + if (json['user'] != null) userId = json['user']['id']; + rank = ran; + countryRank = cran; + aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']); + var ex = json['extras'] as Map; + switch (ex.keys.firstOrNull){ + case "zenith": + extras = ZenithExtras.fromJson(json['extras']['zenith']); + default: + break; + } + } + + Map toJson() { + final Map data = {}; + data['_id'] = ownId; + data['results']['stats'] = stats.toJson(); + data['ismulti'] = false; + data['replayid'] = replayId; + data['ts'] = timestamp; + data['user_id'] = userId; + return data; + } +} diff --git a/lib/data_objects/results_stats.dart b/lib/data_objects/results_stats.dart new file mode 100644 index 0000000..328da53 --- /dev/null +++ b/lib/data_objects/results_stats.dart @@ -0,0 +1,82 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/clears.dart'; +import 'package:tetra_stats/data_objects/finesse.dart'; +import 'package:tetra_stats/data_objects/zenith_results.dart'; + +class ResultsStats { + late int topBtB; + late int topCombo; + late int holds; + late int inputs; + late int level; + late int piecesPlaced; + late int lines; + late int score; + double? seed; + late Duration finalTime; + late int tSpins; + late Clears clears; + late int kills; + Finesse? finesse; + ZenithResults? zenith; + + double get pps => piecesPlaced / (finalTime.inMicroseconds / 1000000); + double get kpp => inputs / piecesPlaced; + double get spp => score / piecesPlaced; + double get kps => inputs / (finalTime.inMicroseconds / 1000000); + double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0; + double get cps => zenith != null ? zenith!.avgrankpts / (finalTime.inMilliseconds / 1000 * 60) : 0; + + ResultsStats( + { + required this.topBtB, + required this.topCombo, + required this.holds, + required this.inputs, + required this.level, + required this.piecesPlaced, + required this.lines, + required this.score, + required this.seed, + required this.finalTime, + required this.tSpins, + required this.clears, + required this.finesse}); + + ResultsStats.fromJson(Map json) { + seed = json['seed']?.toDouble(); + lines = json['lines']; + inputs = json['inputs']; + holds = json['holds'] ?? 0; + finalTime = Duration(microseconds: (json['finaltime'].toDouble() * 1000).floor()); + score = json['score']; + level = json['level']; + topCombo = json['topcombo']; + topBtB = json['topbtb']; + tSpins = json['tspins']; + piecesPlaced = json['piecesplaced']; + clears = Clears.fromJson(json['clears']); + kills = json['kills']; + if (json.containsKey("finesse")) finesse = Finesse.fromJson(json['finesse']); + if (json.containsKey("zenith")) zenith = ZenithResults.fromJson(json['zenith']); + } + + Map toJson() { + final Map data = {}; + data['seed'] = seed; + data['lines'] = lines; + data['inputs'] = inputs; + data['holds'] = holds; + data['score'] = score; + data['level'] = level; + data['topcombo'] = topCombo; + data['topbtb'] = topBtB; + data['tspins'] = tSpins; + data['piecesplaced'] = piecesPlaced; + data['clears'] = clears.toJson(); + if (finesse != null) data['finesse'] = finesse!.toJson(); + data['finalTime'] = finalTime; + return data; + } +} diff --git a/lib/data_objects/singleplayer_stream.dart b/lib/data_objects/singleplayer_stream.dart new file mode 100644 index 0000000..a59f74b --- /dev/null +++ b/lib/data_objects/singleplayer_stream.dart @@ -0,0 +1,18 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/record_single.dart'; + +class SingleplayerStream{ + late String userId; + late String type; + late List records; + + SingleplayerStream({required this.userId, required this.records, required this.type}); + + SingleplayerStream.fromJson(List json, String userID, String tp) { + userId = userID; + type = tp; + records = []; + for (var value in json) {records.add(RecordSingle.fromJson(value, -1, -1));} + } +} diff --git a/lib/data_objects/summaries.dart b/lib/data_objects/summaries.dart new file mode 100644 index 0000000..08f5db6 --- /dev/null +++ b/lib/data_objects/summaries.dart @@ -0,0 +1,39 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/achievement.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; + +class Summaries{ + late String id; + RecordSingle? sprint; + RecordSingle? blitz; + RecordSingle? zenith; + RecordSingle? zenithCareerBest; // leaderboard best, not overall + RecordSingle? zenithEx; + RecordSingle? zenithExCareerBest; // leaderboard best, not overall + late List achievements; + late TetraLeague league; + Map pastLeague = {}; + late TetrioZen zen; + + Summaries(this.id, this.league, this.zen); + + Summaries.fromJson(Map json, String i){ + id = i; + if (json['40l']['record'] != null) sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank'], json['40l']['rank_local']); + if (json['blitz']['record'] != null) blitz = RecordSingle.fromJson(json['blitz']['record'], json['blitz']['rank'], json['40l']['rank_local']); + if (json['zenith']['record'] != null) zenith = RecordSingle.fromJson(json['zenith']['record'], json['zenith']['rank'], json['zenith']['rank_local']); + if (json['zenith']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenith']['best']['record'], json['zenith']['best']['rank'], -1); + if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); + if (json['zenithex']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenithex']['best']['record'], json['zenith']['best']['rank'], -1); + achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; + league = TetraLeague.fromJson(json['league'], DateTime.now(), currentSeason, i); + if (json['league']['past'].isNotEmpty) for (var key in json['league']['past'].keys){ + pastLeague[int.parse(key)] = TetraLeague.fromJson(json['league']['past'][key], DateTime(1970), int.parse(json['league']['past'][key]['season']), i); + } + zen = TetrioZen.fromJson(json['zen']); + } +} diff --git a/lib/data_objects/tetra_league.dart b/lib/data_objects/tetra_league.dart new file mode 100644 index 0000000..0419fd8 --- /dev/null +++ b/lib/data_objects/tetra_league.dart @@ -0,0 +1,139 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; + +class TetraLeague { + late String id; + late DateTime timestamp; + late int gamesPlayed; + late int gamesWon; + late String bestRank; + late bool decaying; + late double tr; + late double gxe; + late String rank; + double? glicko; + double? rd; + late String percentileRank; + late double percentile; + late int standing; + late int standingLocal; + String? nextRank; + late int nextAt; + String? prevRank; + late int prevAt; + double? apm; + double? pps; + double? vs; + NerdStats? nerdStats; + EstTr? estTr; + Playstyle? playstyle; + late int season; + + TetraLeague( + {required this.id, + required this.timestamp, + required this.gamesPlayed, + required this.gamesWon, + required this.bestRank, + required this.decaying, + required this.tr, + required this.gxe, + required this.rank, + this.glicko, + this.rd, + required this.percentileRank, + required this.percentile, + required this.standing, + required this.standingLocal, + this.nextRank, + required this.nextAt, + this.prevRank, + required this.prevAt, + this.apm, + this.pps, + this.vs, + required this.season}){ + nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; + estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; + playstyle =(nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; + } + + double get winrate => gamesWon / gamesPlayed; + double get s1tr => gxe * 250; + + TetraLeague.fromJson(Map json, ts, int s, String i) { + timestamp = ts; + season = s; + id = i; + gamesPlayed = json['gamesplayed'] ?? 0; + gamesWon = json['gameswon'] ?? 0; + tr = json['tr'] != null ? json['tr'].toDouble() : json['rating'] != null ? json['rating'].toDouble() : -1; + glicko = json['glicko']?.toDouble(); + rd = json['rd'] != null ? json['rd']!.toDouble() : noTrRd; + gxe = json['gxe'] != null ? json['gxe'].toDouble() : -1; + rank = json['rank'] != null ? json['rank']!.toString() : 'z'; + bestRank = json['bestrank'] != null ? json['bestrank']!.toString() : 'z'; + apm = json['apm']?.toDouble(); + pps = json['pps']?.toDouble(); + vs = json['vs']?.toDouble(); + decaying = switch(json['decaying'].runtimeType){ + int => json['decaying'] == 1, + bool => json['decaying'], + _ => false + }; + standing = json['standing'] ?? json['placement'] ?? -1; + percentile = json['percentile'] != null ? json['percentile'].toDouble() : rankCutoffs[rank]; + standingLocal = json['standing_local'] ?? -1; + prevRank = json['prev_rank']; + prevAt = json['prev_at'] ?? -1; + nextRank = json['next_rank']; + nextAt = json['next_at'] ?? -1; + percentileRank = json['percentile_rank'] ?? rank; + nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; + estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; + playstyle = (nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; + } + + @override + bool operator ==(covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && rd == other.rd; + + bool lessStrictCheck (covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && glicko == other.glicko; + + double? get esttracc => (estTr != null) ? estTr!.esttr - tr : null; + + TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) => TetrioPlayerFromLeaderboard( + id, "", "user", -1, null, timestamp, gamesPlayed, gamesWon, + tr, gxe, glicko??0, rd??noTrRd, rank, bestRank, apm??0, pps??0, vs??0, decaying); + + Map toJson() { + final Map data = {}; + data['id'] = id+timestamp.millisecondsSinceEpoch.toRadixString(16); + if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed; + if (gamesWon > 0) data['gameswon'] = gamesWon; + if (tr >= 0) data['tr'] = tr; + if (glicko != null) data['glicko'] = glicko; + if (gxe != -1) data['gxe'] = gxe; + if (rd != null && rd != noTrRd) data['rd'] = rd; + if (rank != 'z') data['rank'] = rank; + if (bestRank != 'z') data['bestrank'] = bestRank; + if (apm != null) data['apm'] = apm; + if (pps != null) data['pps'] = pps; + if (vs != null) data['vs'] = vs; + if (decaying) data['decaying'] = decaying ? 1 : 0; + if (standing >= 0) data['standing'] = standing; + data['percentile'] = percentile; + if (standingLocal >= 0) data['standing_local'] = standingLocal; + if (prevRank != null) data['prev_rank'] = prevRank; + if (prevAt >= 0) data['prev_at'] = prevAt; + if (nextRank != null) data['next_rank'] = nextRank; + if (nextAt >= 0) data['next_at'] = nextAt; + data['percentile_rank'] = percentileRank; + data['season'] = season; + return data; + } +} diff --git a/lib/data_objects/tetra_league_alpha_record.dart b/lib/data_objects/tetra_league_alpha_record.dart new file mode 100644 index 0000000..ad59825 --- /dev/null +++ b/lib/data_objects/tetra_league_alpha_record.dart @@ -0,0 +1,39 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/end_context_multi.dart'; + +class TetraLeagueAlphaRecord{ + late String replayId; + late String ownId; + late DateTime timestamp; + late bool replayAvalable; + late List endContext; + + TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext, required this.replayAvalable}); + + TetraLeagueAlphaRecord.fromJson(Map json) { + endContext = [EndContextMulti.fromJson(json['endcontext'][0]), EndContextMulti.fromJson(json['endcontext'][1])]; + replayId = json['replayid']; + ownId = json['_id']??replayId; + timestamp = DateTime.parse(json['ts']); + replayAvalable = ownId != replayId; + } + + Map toJson() { + final Map data = {}; + data['_id'] = ownId; + data['endcontext'][0] = endContext[0].toJson(); + data['endcontext'][1] = endContext[1].toJson(); + data['replayid'] = replayId; + data['ts'] = timestamp; + return data; + } + + @override + bool operator ==(covariant TetraLeagueAlphaRecord other) => (ownId == other.ownId) || (replayId == other.replayId); + + @override + String toString() { + return "TetraLeagueAlphaRecord: ${endContext.first.userId} vs ${endContext.last.userId}"; + } +} diff --git a/lib/data_objects/tetra_league_alpha_stream.dart b/lib/data_objects/tetra_league_alpha_stream.dart new file mode 100644 index 0000000..ebce2ac --- /dev/null +++ b/lib/data_objects/tetra_league_alpha_stream.dart @@ -0,0 +1,16 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; + +class TetraLeagueAlphaStream{ + late String userId; + late List records; + + TetraLeagueAlphaStream({required this.userId, required this.records}); + + TetraLeagueAlphaStream.fromJson(List json, String userID) { + userId = userID; + records = []; + for (var value in json) {records.add(TetraLeagueAlphaRecord.fromJson(value));} + } +} diff --git a/lib/data_objects/tetra_league_beta_stream.dart b/lib/data_objects/tetra_league_beta_stream.dart new file mode 100644 index 0000000..f51a135 --- /dev/null +++ b/lib/data_objects/tetra_league_beta_stream.dart @@ -0,0 +1,111 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/beta_league_leaderboard_entry.dart'; +import 'package:tetra_stats/data_objects/beta_league_results.dart'; +import 'package:tetra_stats/data_objects/beta_league_round.dart'; +import 'package:tetra_stats/data_objects/beta_league_stats.dart'; +import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; + +class TetraLeagueBetaStream{ + late String id; + List records = []; + + TetraLeagueBetaStream({required this.id, required this.records}); + + TetraLeagueBetaStream.fromJson(List json, String userID) { + id = userID; + for (var entry in json) { + records.add(BetaRecord.fromJson(entry)); + } + } + + addFromAlphaStream(List r){ + for (var entry in r) { + records.add( + BetaRecord( + id: entry.ownId, + replayID: entry.replayId, + ts: entry.timestamp, + enemyID: entry.endContext[1].userId, + enemyUsername: entry.endContext[1].username, + gamemode: "oldleague", + results: BetaLeagueResults( + leaderboard: [ + BetaLeagueLeaderboardEntry( + id: entry.endContext[0].userId, + username: entry.endContext[0].username, + naturalorder: entry.endContext[0].naturalOrder, + wins: entry.endContext[0].points, + stats: BetaLeagueStats( + apm: entry.endContext[0].secondary, + pps: entry.endContext[0].tertiary, + vs: entry.endContext[0].extra, + garbageSent: -1, + garbageReceived: -1, + kills: entry.endContext[0].points, + altitude: 0.0, + rank: -1 + ) + ), + BetaLeagueLeaderboardEntry( + id: entry.endContext[1].userId, + username: entry.endContext[1].username, + naturalorder: entry.endContext[1].naturalOrder, + wins: entry.endContext[1].points, + stats: BetaLeagueStats( + apm: entry.endContext[1].secondary, + pps: entry.endContext[1].tertiary, + vs: entry.endContext[1].extra, + garbageSent: -1, + garbageReceived: -1, + kills: entry.endContext[1].points, + altitude: 0.0, + rank: -1 + ) + ) + ], + rounds: [ + for (int i=0; i ranks = [ - "d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x", "x+" -]; -const Map rankCutoffs = { - "x+": 0.002, - "x": 0.01, - "u": 0.05, - "ss": 0.11, - "s+": 0.17, - "s": 0.23, - "s-": 0.3, - "a+": 0.38, - "a": 0.46, - "a-": 0.54, - "b+": 0.62, - "b": 0.7, - "b-": 0.78, - "c+": 0.84, - "c": 0.9, - "c-": 0.95, - "d+": 0.975, - "d": 1, - "z": -1, - "": 0.5 -}; -const Map rankTargets = { - "x+": 24000.00, - "x": 22500.00, - "u": 20000.00, - "ss": 18000.00, - "s+": 16500.00, - "s": 15200.00, - "s-": 13800.00, - "a+": 12000.00, - "a": 10500.00, - "a-": 9000.00, - "b+": 7400.00, - "b": 5700.00, - "b-": 4200.00, - "c+": 3000.00, - "c": 2000.00, - "c-": 1300.00, - "d+": 800.00, - "d": 0.00, -}; -// DateTime seasonStart = DateTime.utc(2024, 08, 16, 18); -//DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15); -enum Stats { - tr, - glicko, - gxe, - s1tr, - rd, - gp, - gw, - wr, - apm, - pps, - vs, - app, - dss, - dsp, - appdsp, - vsapm, - cheese, - gbe, - nyaapp, - area, - eTR, - acceTR, - acceTRabs, - opener, - plonk, - infDS, - stride, - stridemMinusPlonk, - openerMinusInfDS - } - -const Map chartsShortTitles = { - Stats.tr: "TR", - Stats.gxe: "Glixare", - Stats.s1tr: "S1 TR", - Stats.glicko: "Glicko", - Stats.rd: "RD", - Stats.gp: "GP", - Stats.gw: "GW", - Stats.wr: "WR%", - Stats.apm: "APM", - Stats.pps: "PPS", - Stats.vs: "VS", - Stats.app: "APP", - Stats.dss: "DS/S", - Stats.dsp: "DS/P", - Stats.appdsp: "APP + DS/P", - Stats.vsapm: "VS/APM", - Stats.cheese: "Cheese", - Stats.gbe: "GbE", - Stats.nyaapp: "wAPP", - Stats.area: "Area", - Stats.eTR: "eTR", - Stats.acceTR: "±eTR", - Stats.acceTRabs: "+eTR absolute", - Stats.opener: "Opener", - Stats.plonk: "Plonk", - Stats.infDS: "Inf. DS", - Stats.stride: "Stride", - Stats.stridemMinusPlonk: "Stride - Plonk", - Stats.openerMinusInfDS: "Opener - Inf. DS" - }; - -const Map rankColors = { // thanks osk for const rankColors at https://ch.tetr.io/res/js/base.js:458 - 'x+': Color(0xFF643C8D), - 'x': Color(0xFFFF45FF), - 'u': Color(0xFFFF3813), - 'ss': Color(0xFFDB8B1F), - 's+': Color(0xFFD8AF0E), - 's': Color(0xFFE0A71B), - 's-': Color(0xFFB2972B), - 'a+': Color(0xFF1FA834), - 'a': Color(0xFF46AD51), - 'a-': Color(0xFF3BB687), - 'b+': Color(0xFF4F99C0), - 'b': Color(0xFF4F64C9), - 'b-': Color(0xFF5650C7), - 'c+': Color(0xFF552883), - 'c': Color(0xFF733E8F), - 'c-': Color(0xFF79558C), - 'd+': Color(0xFF8E6091), - 'd': Color(0xFF907591), - 'z': Color(0xFF375433) -}; - -const Map sprintAverages = { // based on https://discord.com/channels/673303546107658242/674421736162197515/1244287342965952562 - 'x': Duration(seconds: 25, milliseconds: 144), - 'u': Duration(seconds: 36, milliseconds: 115), - 'ss': Duration(seconds: 46, milliseconds: 396), - 's+': Duration(seconds: 55, milliseconds: 056), - 's': Duration(seconds: 61, milliseconds: 892), - 's-': Duration(seconds: 68, milliseconds: 918), - 'a+': Duration(seconds: 76, milliseconds: 187), - 'a': Duration(seconds: 83, milliseconds: 529), - 'a-': Duration(seconds: 88, milliseconds: 608), - 'b+': Duration(seconds: 97, milliseconds: 626), - 'b': Duration(seconds: 104, milliseconds: 687), - 'b-': Duration(seconds: 113, milliseconds: 372), - 'c+': Duration(seconds: 123, milliseconds: 461), - 'c': Duration(seconds: 135, milliseconds: 326), - 'c-': Duration(seconds: 147, milliseconds: 056), - 'd+': Duration(seconds: 156, milliseconds: 757), - 'd': Duration(seconds: 167, milliseconds: 393), - //'z': Duration(seconds: 66, milliseconds: 802) -}; - -const Map blitzAverages = { - 'x': 600715, - 'u': 379418, - 'ss': 233740, - 's+': 158295, - 's': 125164, - 's-': 100933, - 'a+': 83593, - 'a': 68613, - 'a-': 60219, - 'b+': 51197, - 'b': 44171, - 'b-': 39045, - 'c+': 34130, - 'c': 28931, - 'c-': 25095, - 'd+': 22944, - 'd': 20728, - //'z': 72084 -}; - -String getStatNameByEnum(Stats stat){ - return t[stat.name]; -} - -Duration doubleSecondsToDuration(double value) { - value = value * 1000000; - return Duration(microseconds: value.floor()); -} - -Duration doubleMillisecondsToDuration(double value) { - value = value * 1000; - return Duration(microseconds: value.floor()); -} - -class TetrioPlayer { - late String userId; - late String username; - late DateTime state; - late String role; - int? avatarRevision; - int? bannerRevision; - DateTime? registrationTime; - List badges = []; - String? bio; - String? country; - late int friendCount; - late int gamesPlayed; - late int gamesWon; - late Duration gameTime; - late double xp; - late int supporterTier; - late bool verified; - bool? badstanding; - String? botmaster; - Connections? connections; - TetrioZen? zen; - Distinguishment? distinguishment; - DateTime? cachedUntil; - - TetrioPlayer({ - required this.userId, - required this.username, - required this.role, - required this.state, - this.avatarRevision, - this.bannerRevision, - this.registrationTime, - required this.badges, - this.bio, - this.country, - required this.friendCount, - required this.gamesPlayed, - required this.gamesWon, - required this.gameTime, - required this.xp, - required this.supporterTier, - required this.verified, - this.badstanding, - this.botmaster, - required this.connections, - this.zen, - this.distinguishment, - this.cachedUntil - }); - - double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1; - - TetrioPlayer.fromJson(Map json, DateTime stateTime, String id, String nick, [DateTime? cUntil]) { - //developer.log("TetrioPlayer.fromJson $stateTime: $json", name: "data_objects/tetrio"); - userId = id; - username = nick; - state = stateTime; - role = json['role']; - registrationTime = json['ts'] != null ? DateTime.parse(json['ts']) : DateTime.fromMillisecondsSinceEpoch(int.parse(id.substring(0, 8), radix: 16) * 1000); - if (json['badges'] != null) { - json['badges'].forEach((v) { - badges.add(Badge.fromJson(v)); - }); - } - xp = json['xp'] != null ? json['xp'].toDouble() : -1; - gamesPlayed = json['gamesplayed'] ?? -1; - gamesWon = json['gameswon'] ?? -1; - gameTime = json['gametime'] != null && json['gametime'] != -1 ? doubleSecondsToDuration(json['gametime'].toDouble()) : const Duration(seconds: -1); - country = json['country']; - supporterTier = json['supporter_tier'] ?? 0; - verified = json['verified'] ?? false; - avatarRevision = json['avatar_revision']; - bannerRevision = json['banner_revision']; - bio = json['bio']; - if (json['connections'] != null && json['connections'].isNotEmpty) connections = Connections.fromJson(json['connections']); - distinguishment = json['distinguishment'] != null ? Distinguishment.fromJson(json['distinguishment']) : null; - friendCount = json['friend_count'] ?? 0; - badstanding = json['badstanding']; - botmaster = json['botmaster']; - cachedUntil = cUntil; - } - - Map toJson() { - final Map data = {}; - // data['_id'] = userId; - // data['username'] = username; - data['role'] = role; - if (registrationTime != null) data['ts'] = registrationTime?.toString(); - if (badges.isNotEmpty) data['badges'] = badges.map((v) => v.toJson()).toList(); - if (xp >= 0) data['xp'] = xp; - if (gamesPlayed >= 0) data['gamesplayed'] = gamesPlayed; - if (gamesWon >= 0) data['gameswon'] = gamesWon; - if (!gameTime.isNegative) data['gametime'] = gameTime.inMicroseconds / 1000000; - if (country != null) data['country'] = country; - if (supporterTier > 0) data['supporter_tier'] = supporterTier; - if (verified) data['verified'] = verified; - if (distinguishment != null) data['distinguishment'] = distinguishment?.toJson(); - if (avatarRevision != null) data['avatar_revision'] = avatarRevision; - if (bannerRevision != null) data['banner_revision'] = bannerRevision; - if (bio != null) data['bio'] = bio; - if (connections != null) data['connections'] = connections!.toJson(); - if (friendCount > 0) data['friend_count'] = friendCount; - if (badstanding != null) data['badstanding'] = badstanding; - if (botmaster != null) data['botmaster'] = botmaster; - //developer.log("TetrioPlayer.toJson: $data", name: "data_objects/tetrio"); - return data; - } - - bool isSameState(covariant TetrioPlayer other) { - if (userId != other.userId) return false; - if (username != other.username) return false; - if (role != other.role) return false; - if (listEquals(badges, other.badges) == false) return false; - //if (bio != other.bio) return false; - if (country != other.country) return false; - if (friendCount != other.friendCount) return false; - if (gamesPlayed != other.gamesPlayed) return false; - if (gamesWon != other.gamesWon) return false; - if (gameTime != other.gameTime) return false; - if (xp != other.xp) return false; - if (supporterTier != other.supporterTier) return false; - if (verified != other.verified) return false; - if (badstanding != other.badstanding) return false; - if (botmaster != other.botmaster) return false; - if (connections != other.connections) return false; - if (distinguishment != other.distinguishment) return false; - return true; - } - - @override - String toString() { - return "$username ($state)"; - } - - @override - int get hashCode => state.hashCode; - - @override - bool operator ==(covariant TetrioPlayer other) => isSameState(other) && state.isAtSameMomentAs(other.state); -} - -class Summaries{ - late String id; - RecordSingle? sprint; - RecordSingle? blitz; - RecordSingle? zenith; - RecordSingle? zenithCareerBest; // leaderboard best, not overall - RecordSingle? zenithEx; - RecordSingle? zenithExCareerBest; // leaderboard best, not overall - late List achievements; - late TetraLeague league; - late TetrioZen zen; - - Summaries(this.id, this.league, this.zen); - - Summaries.fromJson(Map json, String i){ - id = i; - if (json['40l']['record'] != null) sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank'], json['40l']['rank_local']); - if (json['blitz']['record'] != null) blitz = RecordSingle.fromJson(json['blitz']['record'], json['blitz']['rank'], json['40l']['rank_local']); - if (json['zenith']['record'] != null) zenith = RecordSingle.fromJson(json['zenith']['record'], json['zenith']['rank'], json['zenith']['rank_local']); - if (json['zenith']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenith']['best']['record'], json['zenith']['best']['rank'], -1); - if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); - if (json['zenithex']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenithex']['best']['record'], json['zenith']['best']['rank'], -1); - achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; - league = TetraLeague.fromJson(json['league'], DateTime.now(), currentSeason, i); - zen = TetrioZen.fromJson(json['zen']); - } -} - -class Badge { - late String badgeId; - late String label; - DateTime? ts; - - Badge({required this.badgeId, required this.label, this.ts}); - - Badge.fromJson(Map json) { - badgeId = json['id']; - label = json['label']; - ts = (json['ts'] != null && json['ts'] is String) ? DateTime.parse(json['ts']) : null; // man i love osk - } - - Map toJson() { - final Map data = {}; - data['id'] = badgeId; - data['label'] = label; - data['ts'] = ts?.toString(); - return data; - } - - @override - String toString() { - return "Badge $label ($badgeId)"; - } - - @override - int get hashCode => badgeId.hashCode; - - @override - bool operator ==(covariant Badge other) => badgeId == other.badgeId; -} - -class Connections { - Discord? discord; - - Connections({this.discord}); - - Connections.fromJson(Map json) { - discord = json['discord'] != null ? Discord.fromJson(json['discord']) : null; - } - @override - bool operator ==(covariant Connections other) => discord == other.discord; - - Map toJson() { - final Map data = {}; - if (discord != null) { - data['discord'] = discord!.toJson(); - } - return data; - } -} - -class Clears { - late int singles; - late int doubles; - late int triples; - late int quads; - late int pentas; - late int allClears; - late int tSpinZeros; - late int tSpinSingles; - late int tSpinDoubles; - late int tSpinTriples; - late int tSpinQuads; - late int tSpinPentas; - late int tSpinMiniZeros; - late int tSpinMiniSingles; - late int tSpinMiniDoubles; - late int tSpinMiniTriples; - late int tSpinMiniQuads; - - Clears( - {required this.singles, - required this.doubles, - required this.triples, - required this.quads, - required this.pentas, - required this.allClears, - required this.tSpinZeros, - required this.tSpinSingles, - required this.tSpinDoubles, - required this.tSpinTriples, - required this.tSpinPentas, - required this.tSpinQuads, - required this.tSpinMiniZeros, - required this.tSpinMiniSingles, - required this.tSpinMiniDoubles, - required this.tSpinMiniTriples, - required this.tSpinMiniQuads}); - - Clears.fromJson(Map json) { - singles = json['singles']; - doubles = json['doubles']; - triples = json['triples']; - quads = json['quads']; - pentas = json['pentas']??0; - tSpinZeros = json['realtspins']; - tSpinMiniZeros = json['minitspins']; - tSpinMiniSingles = json['minitspinsingles']; - tSpinSingles = json['tspinsingles']; - tSpinMiniDoubles = json['minitspindoubles']; - tSpinDoubles = json['tspindoubles']; - tSpinMiniTriples = json['minitspintriples']??0; - tSpinTriples = json['tspintriples']; - tSpinMiniQuads = json['minitspinquads']??0; - tSpinQuads = json['tspinquads']; - tSpinPentas = json['tspinpentas']??0; - allClears = json['allclear']; - } - - Clears operator + (Clears other){ - return Clears( - singles: singles + other.singles, - doubles: doubles + other.doubles, - triples: triples + other.triples, - quads: quads + other.quads, - pentas: pentas + other.pentas, - allClears: allClears + other.allClears, - tSpinZeros: tSpinZeros + other.tSpinZeros, - tSpinSingles: tSpinSingles + other.tSpinSingles, - tSpinDoubles: tSpinDoubles + other.tSpinDoubles, - tSpinTriples: tSpinTriples + other.tSpinTriples, - tSpinPentas: tSpinPentas + other.tSpinPentas, - tSpinQuads: tSpinQuads + other.tSpinQuads, - tSpinMiniZeros: tSpinMiniZeros + other.tSpinMiniZeros, - tSpinMiniSingles: tSpinMiniSingles + other.tSpinMiniSingles, - tSpinMiniDoubles: tSpinMiniDoubles + other.tSpinMiniDoubles, - tSpinMiniTriples: tSpinMiniTriples + other.tSpinMiniTriples, - tSpinMiniQuads: tSpinMiniQuads + other.tSpinMiniQuads - ); - } - - Map toJson() { - final Map data = {}; - data['singles'] = singles; - data['doubles'] = doubles; - data['triples'] = triples; - data['quads'] = quads; - data['pentas'] = pentas; - data['realtspins'] = tSpinZeros; - data['minitspins'] = tSpinMiniZeros; - data['minitspinsingles'] = tSpinMiniSingles; - data['tspinsingles'] = tSpinSingles; - data['minitspindoubles'] = tSpinMiniDoubles; - data['tspindoubles'] = tSpinDoubles; - data['tspintriples'] = tSpinTriples; - data['tspinquads'] = tSpinQuads; - data['tspinpentas'] = tSpinPentas; - data['allclear'] = allClears; - return data; - } -} - -class Discord { - late String id; - late String username; - - Discord({required this.id, required this.username}); - - Discord.fromJson(Map json) { - id = json['id']; - username = json['username']; - } - - @override - bool operator ==(covariant Discord other) => id == other.id; - - Map toJson() { - final Map data = {}; - data['id'] = id; - data['username'] = username; - return data; - } -} - -class Finesse { - late int combo; - late int faults; - late int perfectPieces; - - Finesse({required this.combo, required this.faults, required this.perfectPieces}); - - Finesse.fromJson(Map json) { - combo = json['combo']; - faults = json['faults']; - perfectPieces = json['perfectpieces']; - } - - Finesse operator + (Finesse other){ - return Finesse(combo: max(combo, other.combo), faults: faults + other.faults, perfectPieces: perfectPieces + other.perfectPieces); - } - - Map toJson() { - final Map data = {}; - data['combo'] = combo; - data['faults'] = faults; - data['perfectpieces'] = perfectPieces; - return data; - } -} - -class ResultsStats { - late int topBtB; - late int topCombo; - late int holds; - late int inputs; - late int level; - late int piecesPlaced; - late int lines; - late int score; - double? seed; - late Duration finalTime; - late int tSpins; - late Clears clears; - late int kills; - Finesse? finesse; - ZenithResults? zenith; - - double get pps => piecesPlaced / (finalTime.inMicroseconds / 1000000); - double get kpp => inputs / piecesPlaced; - double get spp => score / piecesPlaced; - double get kps => inputs / (finalTime.inMicroseconds / 1000000); - double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0; - double get cps => zenith != null ? zenith!.avgrankpts / (finalTime.inMilliseconds / 1000 * 60) : 0; - - ResultsStats( - { - required this.topBtB, - required this.topCombo, - required this.holds, - required this.inputs, - required this.level, - required this.piecesPlaced, - required this.lines, - required this.score, - required this.seed, - required this.finalTime, - required this.tSpins, - required this.clears, - required this.finesse}); - - ResultsStats.fromJson(Map json) { - seed = json['seed']?.toDouble(); - lines = json['lines']; - inputs = json['inputs']; - holds = json['holds'] ?? 0; - finalTime = doubleMillisecondsToDuration(json['finaltime'].toDouble()); - score = json['score']; - level = json['level']; - topCombo = json['topcombo']; - topBtB = json['topbtb']; - tSpins = json['tspins']; - piecesPlaced = json['piecesplaced']; - clears = Clears.fromJson(json['clears']); - kills = json['kills']; - if (json.containsKey("finesse")) finesse = Finesse.fromJson(json['finesse']); - if (json.containsKey("zenith")) zenith = ZenithResults.fromJson(json['zenith']); - } - - Map toJson() { - final Map data = {}; - data['seed'] = seed; - data['lines'] = lines; - data['inputs'] = inputs; - data['holds'] = holds; - data['score'] = score; - data['level'] = level; - data['topcombo'] = topCombo; - data['topbtb'] = topBtB; - data['tspins'] = tSpins; - data['piecesplaced'] = piecesPlaced; - data['clears'] = clears.toJson(); - if (finesse != null) data['finesse'] = finesse!.toJson(); - data['finalTime'] = finalTime; - return data; - } -} - -class ZenithResults{ - late double altitude; - late double rank; - late double peakrank; - late double avgrankpts; - late int floor; - late double targetingfactor; - late double targetinggrace; - late double totalbonus; - late int revives; - late int revivesTotal; - late bool speedrun; - late bool speedrunSeen; - late List splits; - - ZenithResults.fromJson(Map json){ - altitude = json['altitude'].toDouble(); - rank = json['rank'].toDouble(); - peakrank = json['peakrank'].toDouble(); - avgrankpts = json['avgrankpts'].toDouble(); - floor = json['floor']; - targetingfactor = json['targetingfactor'].toDouble(); - targetinggrace = json['targetinggrace'].toDouble(); - totalbonus = json['totalbonus'].toDouble(); - revives = json['revives']; - revivesTotal = json['revivesTotal']; - speedrun = json['speedrun']; - speedrunSeen = json['speedrun_seen']; - splits = []; - for (int ms in json['splits']) { - splits.add(Duration(milliseconds: ms)); - } - } -} - -class Handling { - late num arr; - late num das; - late num sdf; - late num dcd; - late bool cancel; - late bool safeLock; - - Handling({required this.arr, required this.das, required this.sdf, required this.dcd, required this.cancel, required this.safeLock}); - - Handling.fromJson(Map json) { - arr = json['arr']; - das = json['das']; - dcd = json['dcd']; - sdf = json['sdf']; - safeLock = json['safelock']; - cancel = json['cancel']; - } - - Map toJson() { - final Map data = {}; - data['arr'] = arr.toDouble(); - data['das'] = das.toDouble(); - data['dcd'] = dcd.toDouble(); - data['sdf'] = sdf.toDouble(); - data['safelock'] = safeLock; - data['cancel'] = cancel; - return data; - } -} - -class NerdStats { - final double _apm; - final double _pps; - final double _vs; - late double app; - late double vsapm; - late double dss; - late double dsp; - late double appdsp; - late double cheese; - late double gbe; - late double nyaapp; - late double area; - - NerdStats(this._apm, this._pps, this._vs) { - app = _apm / (_pps * 60); - vsapm = _vs / _apm; - dss = (_vs / 100) - (_apm / 60); - dsp = ((_vs / 100) - (_apm / 60)) / _pps; - appdsp = app + dsp; - cheese = (dsp * 150) + ((vsapm - 2) * 50) + (0.6 - app) * 125; - gbe = app * dsp * 2; - nyaapp = app - 5 * tan(radians((cheese / -30) + 1)); - area = _apm * 1 + _pps * 45 + _vs * 0.444 + app * 185 + dss * 175 + dsp * 450 + gbe * 315; - } -} - -class EstTr { - final double _apm; - final double _pps; - final double _vs; - final double _app; - final double _dss; - final double _dsp; - final double _gbe; - late double esttr; - late double srarea; - late double statrank; - late double estglicko; - - EstTr(this._apm, this._pps, this._vs, this._app, this._dss, this._dsp, this._gbe) { - srarea = (_apm * 0) + (_pps * 135) + (_vs * 0) + (_app * 290) + (_dss * 0) + (_dsp * 700) + (_gbe * 0); - statrank = 11.2 * atan((srarea - 93) / 130) + 1; - if (statrank <= 0) statrank = 0.001; - //estglicko = (4.0867 * srarea + 186.68); - double ntemp = _pps*(150+(((_vs/_apm) - 1.66)*35))+_app*290+_dsp*700; - estglicko = 0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4; - esttr = 25000 / - ( - 1 + pow(10, ( - ( - ( - 1500-estglicko - )*pi - )/sqrt( - ( - ( - 3*pow(ln10, 2) - )*pow(60, 2) - )+( - 2500*( - (64*pow(pi,2))+(147*pow(ln10, 2)) - ) - ) - ) - )) - ); - } -} - -class Achievement { - late int k; - int? o; - late int rt; - late int vt; - late int min; - late int deci; - late String name; - late String object; - late String category; - late bool hidden; - late int art; - late bool nolb; - late String desc; - late String n; - String? sId; - double? v; - late int? a; - DateTime? t; - int? pos; - int? total; - int? rank; - - Achievement( - {required this.k, - this.o, - required this.rt, - required this.vt, - required this.min, - required this.deci, - required this.name, - required this.object, - required this.category, - required this.hidden, - required this.art, - required this.nolb, - required this.desc, - required this.n, - this.sId, - this.v, - required this.a, - this.t, - this.pos, - this.total, - this.rank}); - - Achievement.fromJson(Map json) { - k = json['k']; - o = json['o']; - rt = json['rt']; - vt = json['vt']; - min = json['min']; - deci = json['deci']; - name = json['name']; - object = json['object']; - category = json['category']; - hidden = json['hidden']; - art = json['art']; - nolb = json['nolb']; - desc = json['desc']; - n = json['n']; - sId = json['_id']; - v = json['v']?.toDouble(); - a = json['a']; - t = json['t'] != null ? DateTime.parse(json['t']) : null; - pos = json['pos']; - total = json['total']; - rank = json['rank']; - } - - Map toJson() { - final Map data = {}; - data['k'] = k; - data['o'] = o; - data['rt'] = rt; - data['vt'] = vt; - data['min'] = min; - data['deci'] = deci; - data['name'] = name; - data['object'] = object; - data['category'] = category; - data['hidden'] = hidden; - data['art'] = art; - data['nolb'] = nolb; - data['desc'] = desc; - data['n'] = n; - data['_id'] = sId; - data['v'] = v; - data['a'] = a; - data['t'] = t.toString(); - data['pos'] = pos; - data['total'] = total; - data['rank'] = rank; - return data; - } -} - - -class Playstyle { - final double _apm; - final double _pps; - //final double _vs; - final double _app; - final double _vsapm; - //final double _dss; - final double _dsp; - final double _gbe; - final double _srarea; - final double _statrank; - late double opener; - late double plonk; - late double stride; - late double infds; - - Playstyle(this._apm, this._pps, this._app, this._vsapm, this._dsp, this._gbe, this._srarea, this._statrank) { - double nmapm = ((_apm / _srarea) / ((0.069 * pow(1.0017, (pow(_statrank, 5) / 4700))) + _statrank / 360)) - 1; - double nmpps = ((_pps / _srarea) / (0.0084264 * pow(2.14, (-2 * (_statrank / 2.7 + 1.03))) - _statrank / 5750 + 0.0067)) - 1; - //double nmvs = ((_vs / _srarea) / (0.1333 * pow(1.0021, ((pow(_statrank, 7) * (_statrank / 16.5)) / 1400000)) + _statrank / 133)) - 1; - double nmapp = (_app / (0.1368803292 * pow(1.0024, (pow(_statrank, 5) / 2800)) + _statrank / 54)) - 1; - //double nmdss = (_dss / (0.01436466667 * pow(4.1, ((_statrank - 9.6) / 2.9)) + _statrank / 140 + 0.01)) - 1; - double nmdsp = (_dsp / (0.02136327583 * pow(14, ((_statrank - 14.75) / 3.9)) + _statrank / 152 + 0.022)) - 1; - double nmgbe = (_gbe / (_statrank / 350 + 0.005948424455 * pow(3.8, ((_statrank - 6.1) / 4)) + 0.006)) - 1; - double nmvsapm = (_vsapm / (-pow(((_statrank - 16) / 36), 2) + 2.133)) - 1; - opener = ((nmapm + nmpps * 0.75 + nmvsapm * -10 + nmapp * 0.75 + nmdsp * -0.25) / 3.5) + 0.5; - plonk = ((nmgbe + nmapp + nmdsp * 0.75 + nmpps * -1) / 2.73) + 0.5; - stride = ((nmapm * -0.25 + nmpps + nmapp * -2 + nmdsp * -0.5) * 0.79) + 0.5; - infds = ((nmdsp + nmapp * -0.75 + nmapm * 0.5 + nmvsapm * 1.5 + nmpps * 0.5) * 0.9) + 0.5; - } -} - -class TetraLeagueAlphaStream{ - late String userId; - late List records; - - TetraLeagueAlphaStream({required this.userId, required this.records}); - - TetraLeagueAlphaStream.fromJson(List json, String userID) { - userId = userID; - records = []; - for (var value in json) {records.add(TetraLeagueAlphaRecord.fromJson(value));} - } -} - -class TetraLeagueBetaStream{ - late String id; - List records = []; - - TetraLeagueBetaStream({required this.id, required this.records}); - - TetraLeagueBetaStream.fromJson(List json, String userID) { - id = userID; - for (var entry in json) { - records.add(BetaRecord.fromJson(entry)); - } - } - - addFromAlphaStream(List r){ - for (var entry in r) { - records.add( - BetaRecord( - id: entry.ownId, - replayID: entry.replayId, - ts: entry.timestamp, - enemyID: entry.endContext[1].userId, - enemyUsername: entry.endContext[1].username, - gamemode: "oldleague", - results: BetaLeagueResults( - leaderboard: [ - BetaLeagueLeaderboardEntry( - id: entry.endContext[0].userId, - username: entry.endContext[0].username, - naturalorder: entry.endContext[0].naturalOrder, - wins: entry.endContext[0].points, - stats: BetaLeagueStats( - apm: entry.endContext[0].secondary, - pps: entry.endContext[0].tertiary, - vs: entry.endContext[0].extra, - garbageSent: -1, - garbageReceived: -1, - kills: entry.endContext[0].points, - altitude: 0.0, - rank: -1 - ) - ), - BetaLeagueLeaderboardEntry( - id: entry.endContext[1].userId, - username: entry.endContext[1].username, - naturalorder: entry.endContext[1].naturalOrder, - wins: entry.endContext[1].points, - stats: BetaLeagueStats( - apm: entry.endContext[1].secondary, - pps: entry.endContext[1].tertiary, - vs: entry.endContext[1].extra, - garbageSent: -1, - garbageReceived: -1, - kills: entry.endContext[1].points, - altitude: 0.0, - rank: -1 - ) - ) - ], - rounds: [ - for (int i=0; i records; - - SingleplayerStream({required this.userId, required this.records, required this.type}); - - SingleplayerStream.fromJson(List json, String userID, String tp) { - userId = userID; - type = tp; - records = []; - for (var value in json) {records.add(RecordSingle.fromJson(value, -1, -1));} - } -} - -class BetaRecord{ - late String id; - late String replayID; - late String gamemode; - late DateTime ts; - late String enemyUsername; - late String enemyID; - late BetaLeagueResults results; - - BetaRecord({required this.id, required this.replayID, required this.gamemode, required this.ts, required this.enemyUsername, required this.enemyID, required this.results}); - - BetaRecord.fromJson(Map json){ - id = json['_id']; - replayID = json['replayid']; - gamemode = json['gamemode']; - ts = DateTime.parse(json['ts']); - enemyUsername = json['otherusers'][0]['username']; - enemyID = json['otherusers'][0]['id']; - results = BetaLeagueResults.fromJson(json['results']); - } -} - -class BetaLeagueResults{ - List leaderboard = []; - List> rounds = []; - - BetaLeagueResults({required this.leaderboard, required this.rounds}); - - BetaLeagueResults.fromJson(Map json){ - for (var lbEntry in json['leaderboard']) { - leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry)); - } - for (var roundEntry in json['rounds']){ - List round = []; - for (var r in roundEntry) { - round.add(BetaLeagueRound.fromJson(r)); - } - rounds.add(round); - } - } -} - -class BetaLeagueLeaderboardEntry{ - late String id; - late String username; - late int naturalorder; - late int wins; - late BetaLeagueStats stats; - - BetaLeagueLeaderboardEntry({required this.id, required this.username, required this.naturalorder, required this.wins, required this.stats}); - - BetaLeagueLeaderboardEntry.fromJson(Map json){ - id = json['id']; - username = json['username']; - naturalorder = json['naturalorder']; - wins = json['wins']; - stats = BetaLeagueStats.fromJson(json['stats']); - } -} - -class BetaLeagueStats{ - late double apm; - late double pps; - late double vs; - late int garbageSent; - late int garbageReceived; - late int kills; - late double altitude; - late int rank; - int? targetingFactor; - int? targetingRace; - late NerdStats nerdStats; - late EstTr estTr; - late Playstyle playstyle; - - BetaLeagueStats({required this.apm, required this.pps, required this.vs, required this.garbageSent, required this.garbageReceived, required this.kills, required this.altitude, required this.rank}){ - 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); - } - - BetaLeagueStats.fromJson(Map json){ - apm = json['apm'] != null ? json['apm'].toDouble() : 0.00; - pps = json['apm'] != null ? json['pps'].toDouble() : 0.00; - vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00; - garbageSent = json['garbagesent']; - garbageReceived = json['garbagereceived']; - kills = json['kills']; - altitude = json['altitude'].toDouble(); - rank = json['rank']; - targetingFactor = json['targetingfactor']; - targetingRace = json['targetinggrace']; - 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 BetaLeagueRound{ - late String id; - late String username; - late bool active; - late int naturalorder; - late bool alive; - late Duration lifetime; - late BetaLeagueStats stats; - - BetaLeagueRound({required this.id, required this.username, required this.active, required this.naturalorder, required this.alive, required this.lifetime, required this.stats}); - - BetaLeagueRound.fromJson(Map json){ - id = json['id']; - username = json['username']; - active = json['active']; - naturalorder = json['naturalorder']; - alive = json['alive']; - lifetime = Duration(milliseconds: json['lifetime']); - stats = BetaLeagueStats.fromJson(json['stats']); - } -} - -class TetraLeagueAlphaRecord{ - late String replayId; - late String ownId; - late DateTime timestamp; - late bool replayAvalable; - late List endContext; - - TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext, required this.replayAvalable}); - - TetraLeagueAlphaRecord.fromJson(Map json) { - endContext = [EndContextMulti.fromJson(json['endcontext'][0]), EndContextMulti.fromJson(json['endcontext'][1])]; - replayId = json['replayid']; - ownId = json['_id']??replayId; - timestamp = DateTime.parse(json['ts']); - replayAvalable = ownId != replayId; - } - - Map toJson() { - final Map data = {}; - data['_id'] = ownId; - data['endcontext'][0] = endContext[0].toJson(); - data['endcontext'][1] = endContext[1].toJson(); - data['replayid'] = replayId; - data['ts'] = timestamp; - return data; - } - - @override - bool operator ==(covariant TetraLeagueAlphaRecord other) => (ownId == other.ownId) || (replayId == other.replayId); - - @override - String toString() { - return "TetraLeagueAlphaRecord: ${endContext.first.userId} vs ${endContext.last.userId}"; - } -} - -class EndContextMulti { - late String userId; - late String username; - late int naturalOrder; - late int inputs; - late int piecesPlaced; - late Handling handling; - late int points; - late int wins; - late double secondary; - late List secondaryTracking; - late double tertiary; - late List tertiaryTracking; - late double extra; - late List extraTracking; - late bool success; - late NerdStats nerdStats; - late List nerdStatsTracking; - late EstTr estTr; - late List estTrTracking; - late Playstyle playstyle; - late List playstyleTracking; - - EndContextMulti( - {required this.userId, - required this.username, - required this.naturalOrder, - required this.inputs, - required this.piecesPlaced, - required this.handling, - required this.points, - required this.wins, - required this.secondary, - required this.secondaryTracking, - required this.tertiary, - required this.tertiaryTracking, - required this.extra, - required this.extraTracking, - required this.success}){ - nerdStats = NerdStats(secondary, tertiary, extra); - nerdStatsTracking = [for (int i = 0; i < secondaryTracking.length; i++) NerdStats(secondaryTracking[i], tertiaryTracking[i], extraTracking[i])]; - estTr = EstTr(secondary, tertiary, extra, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); - estTrTracking = [for (int i = 0; i < secondaryTracking.length; i++) EstTr(secondaryTracking[i], tertiaryTracking[i], extraTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].dss, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe)]; - playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); - playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)]; - } - - EndContextMulti.fromJson(Map json) { - userId = json['id'] ?? json['user']['_id']; - username = json['username'] ?? json['user']['username']; - handling = json['handling'] != null ? Handling.fromJson(json['handling']) : Handling(arr: -1, das: -1, sdf: -1, dcd: 0, cancel: true, safeLock: true); - success = json['success']; - inputs = json['inputs'] ?? -1; - piecesPlaced = json['piecesplaced'] ?? -1; - naturalOrder = json['naturalorder']; - wins = json['wins']; - points = json['points']['primary']; - secondary = json['points']['secondary'].toDouble(); - tertiary = json['points']['tertiary'].toDouble(); - secondaryTracking = json['points']['secondaryAvgTracking'] != null ? json['points']['secondaryAvgTracking'].map((e) => e.toDouble()).toList() : []; - tertiaryTracking = json['points']['tertiaryAvgTracking'] != null ? json['points']['tertiaryAvgTracking'].map((e) => e.toDouble()).toList() : []; - extra = json['points']['extra']['vs'].toDouble(); - extraTracking = json['points']['extraAvgTracking'] != null ? json['points']['extraAvgTracking']['aggregatestats___vsscore'].map((e) => e.toDouble()).toList() : []; - nerdStats = NerdStats(secondary, tertiary, extra); - nerdStatsTracking = [for (int i = 0; i < secondaryTracking.length; i++) NerdStats(secondaryTracking[i], tertiaryTracking[i], extraTracking[i])]; - estTr = EstTr(secondary, tertiary, extra, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); - estTrTracking = [for (int i = 0; i < secondaryTracking.length; i++) EstTr(secondaryTracking[i], tertiaryTracking[i], extraTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].dss, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe)]; - playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); - playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)]; - } - - @override - bool operator == (covariant EndContextMulti other){ - if (userId != other.userId) return false; - return true; - } - - Map toJson() { - final Map data = {}; - data['user'] = {'_id': userId, 'username': username}; - data['handling'] = handling.toJson(); - data['success'] = success; - data['inputs'] = inputs; - data['piecesplaced'] = piecesPlaced; - data['naturalorder'] = naturalOrder; - data['wins'] = wins; - data['points'] = {'primary': points, 'secondary': secondary, 'tertiary':tertiary, 'extra': {'vs': extra}, 'secondaryAvgTracking': secondaryTracking, 'tertiaryAvgTracking': tertiaryTracking, 'extraAvgTracking': {'aggregatestats___vsscore': extraTracking}}; - return data; - } -} - -class TetraLeague { - late String id; - late DateTime timestamp; - late int gamesPlayed; - late int gamesWon; - late String bestRank; - late bool decaying; - late double tr; - late double gxe; - late String rank; - double? glicko; - double? rd; - late String percentileRank; - late double percentile; - late int standing; - late int standingLocal; - String? nextRank; - late int nextAt; - String? prevRank; - late int prevAt; - double? apm; - double? pps; - double? vs; - NerdStats? nerdStats; - EstTr? estTr; - Playstyle? playstyle; - late int season; - - TetraLeague( - {required this.id, - required this.timestamp, - required this.gamesPlayed, - required this.gamesWon, - required this.bestRank, - required this.decaying, - required this.tr, - required this.gxe, - required this.rank, - this.glicko, - this.rd, - required this.percentileRank, - required this.percentile, - required this.standing, - required this.standingLocal, - this.nextRank, - required this.nextAt, - this.prevRank, - required this.prevAt, - this.apm, - this.pps, - this.vs, - required this.season}){ - nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; - estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; - playstyle =(nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; - } - - double get winrate => gamesWon / gamesPlayed; - double get s1tr => gxe * 250; - - TetraLeague.fromJson(Map json, ts, int s, String i) { - timestamp = ts; - season = s; - id = i; - gamesPlayed = json['gamesplayed'] ?? 0; - gamesWon = json['gameswon'] ?? 0; - tr = json['tr'] != null ? json['tr'].toDouble() : json['rating'] != null ? json['rating'].toDouble() : -1; - glicko = json['glicko']?.toDouble(); - rd = json['rd'] != null ? json['rd']!.toDouble() : noTrRd; - gxe = json['gxe'] != null ? json['gxe'].toDouble() : -1; - rank = json['rank'] != null ? json['rank']!.toString() : 'z'; - bestRank = json['bestrank'] != null ? json['bestrank']!.toString() : 'z'; - apm = json['apm']?.toDouble(); - pps = json['pps']?.toDouble(); - vs = json['vs']?.toDouble(); - decaying = switch(json['decaying'].runtimeType){ - int => json['decaying'] == 1, - bool => json['decaying'], - _ => false - }; - standing = json['standing'] ?? -1; - percentile = json['percentile'] != null ? json['percentile'].toDouble() : rankCutoffs[rank]; - standingLocal = json['standing_local'] ?? -1; - prevRank = json['prev_rank']; - prevAt = json['prev_at'] ?? -1; - nextRank = json['next_rank']; - nextAt = json['next_at'] ?? -1; - percentileRank = json['percentile_rank'] ?? rank; - nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; - estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; - playstyle = (nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; - } - - @override - bool operator ==(covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && rd == other.rd; - - bool lessStrictCheck (covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && glicko == other.glicko; - - double? get esttracc => (estTr != null) ? estTr!.esttr - tr : null; - - TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) => TetrioPlayerFromLeaderboard( - id, "", "user", -1, null, timestamp, gamesPlayed, gamesWon, - tr, gxe, glicko??0, rd??noTrRd, rank, bestRank, apm??0, pps??0, vs??0, decaying); - - Map toJson() { - final Map data = {}; - data['id'] = id+timestamp.millisecondsSinceEpoch.toRadixString(16); - if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed; - if (gamesWon > 0) data['gameswon'] = gamesWon; - if (tr >= 0) data['tr'] = tr; - if (glicko != null) data['glicko'] = glicko; - if (gxe != -1) data['gxe'] = gxe; - if (rd != null && rd != noTrRd) data['rd'] = rd; - if (rank != 'z') data['rank'] = rank; - if (bestRank != 'z') data['bestrank'] = bestRank; - if (apm != null) data['apm'] = apm; - if (pps != null) data['pps'] = pps; - if (vs != null) data['vs'] = vs; - if (decaying) data['decaying'] = decaying ? 1 : 0; - if (standing >= 0) data['standing'] = standing; - data['percentile'] = percentile; - if (standingLocal >= 0) data['standing_local'] = standingLocal; - if (prevRank != null) data['prev_rank'] = prevRank; - if (prevAt >= 0) data['prev_at'] = prevAt; - if (nextRank != null) data['next_rank'] = nextRank; - if (nextAt >= 0) data['next_at'] = nextAt; - data['percentile_rank'] = percentileRank; - data['season'] = season; - return data; - } -} - -class RecordSingle { - late String? userId; - late String replayId; - late String ownId; - late String gamemode; - late DateTime timestamp; - late ResultsStats stats; - late int rank; - late int countryRank; - late AggregateStats aggregateStats; - late RecordExtras extras; - - RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); - - RecordSingle.fromJson(Map json, int ran, int cran) { - ownId = json['_id']; - gamemode = json['gamemode']; - stats = ResultsStats.fromJson(json['results']['stats']); - replayId = json['replayid']; - timestamp = DateTime.parse(json['ts']); - if (json['user'] != null) userId = json['user']['id']; - rank = ran; - countryRank = cran; - aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']); - var ex = json['extras'] as Map; - switch (ex.keys.firstOrNull){ - case "zenith": - extras = ZenithExtras.fromJson(json['extras']['zenith']); - default: - break; - } - } - - Map toJson() { - final Map data = {}; - data['_id'] = ownId; - data['results']['stats'] = stats.toJson(); - data['ismulti'] = false; - data['replayid'] = replayId; - data['ts'] = timestamp; - data['user_id'] = userId; - return data; - } -} - -class AggregateStats{ - late double apm; - late double pps; - late double vs; - late NerdStats nerdStats; - late EstTr estTr; - late Playstyle playstyle; - - AggregateStats(this.apm, this.pps, this.vs){ - 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); - } - - AggregateStats.fromJson(Map json){ - apm = json['apm'] != null ? json['apm'].toDouble() : 0.00; - pps = json['apm'] != null ? json['pps'].toDouble() : 0.00; - vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00; - 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 RecordExtras{ - -} - -class ZenithExtras extends RecordExtras{ - List mods = []; - - ZenithExtras.fromJson(Map json){ - for (var mod in json["mods"]) { - mods.add(mod); - } - } -} - -class TetrioZen { - late int level; - late int score; - - TetrioZen({required this.level, required this.score}); - - double get scoreRequirement => (10000 + 10000 * ((log(level + 1) / log(2)) - 1)); - - TetrioZen.fromJson(Map json) { - level = json['level']; - score = json['score']; - } - - Map toJson() { - final Map data = {}; - data['level'] = level; - data['score'] = score; - return data; - } -} - -class UserRecords{ - String id; - RecordSingle? sprint; - RecordSingle? blitz; - TetrioZen zen; - - UserRecords(this.id, this.sprint, this.blitz, this.zen); -} - -class Distinguishment { - late String type; - String? detail; - String? header; - String? footer; - - Distinguishment({required this.type, this.detail, this.header, this.footer}); - - Distinguishment.fromJson(Map json) { - type = json['type']; - detail = json['detail']; - header = json['header']; - footer = json['footer']; - } - - @override - bool operator ==(covariant Distinguishment other) => type == other.type && detail == other.detail && header == other.header && footer == other.footer; - - Map toJson() { - final Map data = {}; - data['type'] = type; - data['detail'] = detail; - data['header'] = header; - data['footer'] = footer; - return data; - } -} - -class News{ - late String id; - late List news; - - News(this.id, this.news); - - News.fromJson(Map json, String? userID){ - id = userID != null ? "user_$userID" : json['news'].first['stream']; - news = [for (var entry in json['news']) NewsEntry.fromJson(entry)]; - } -} - -class NewsEntry { - //late String id; do i need it? - late String type; - late Map data; - late DateTime timestamp; - - NewsEntry({required this.type, required this.data, required this.timestamp}); - - NewsEntry.fromJson(Map json){ - //id = json["_id"]; - type = json["type"]; - data = json["data"]; - timestamp = DateTime.parse(json['ts']); - } -} - -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; - late List leaderboard; - - TetrioPlayersLeaderboard(this.type, this.leaderboard); - - @override - String toString(){ - return "$type leaderboard: ${leaderboard.length} players"; - } - - List getStatRanking(List leaderboard, Stats stat, {bool reversed = false, String country = ""}){ - List lb = List.from(leaderboard); - if (country.isNotEmpty){ - lb.removeWhere((element) => element.country != country); - } - lb.sort(((a, b) { - if (a.getStatByEnum(stat).isNaN) return 1; - if (b.getStatByEnum(stat).isNaN) return -1; - if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){ - return reversed ? 1 : -1; - }else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){ - return 0; - }else{ - return reversed ? -1 : 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); - if (rank.isNotEmpty) { - filtredLeaderboard.removeWhere((element) => element.rank != rank); - } - if (filtredLeaderboard.isNotEmpty){ - double avgAPM = 0, - avgPPS = 0, - avgVS = 0, - avgTR = 0, - avgGlixare = 0, - avgGlicko = 0, - avgRD = 0, - avgAPP = 0, - avgVSAPM = 0, - avgDSS = 0, - avgDSP = 0, - avgAPPDSP = 0, - avgCheese = 0, - avgGBE = 0, - avgNyaAPP = 0, - avgArea = 0, - avgEstTR = 0, - avgEstAcc = 0, - avgOpener = 0, - avgPlonk = 0, - avgStride = 0, - avgInfDS = 0, - lowestTR = 25000, - lowestGlixare = double.infinity, - lowestGlicko = double.infinity, - lowestRD = double.infinity, - lowestWinrate = double.infinity, - lowestAPM = double.infinity, - lowestPPS = double.infinity, - lowestVS = double.infinity, - lowestAPP = double.infinity, - lowestVSAPM = double.infinity, - lowestDSS = double.infinity, - lowestDSP = double.infinity, - lowestAPPDSP = double.infinity, - lowestCheese = double.infinity, - lowestGBE = double.infinity, - lowestNyaAPP = double.infinity, - lowestArea = double.infinity, - lowestEstTR = double.infinity, - lowestEstAcc = double.infinity, - lowestOpener = double.infinity, - lowestPlonk = double.infinity, - lowestStride = double.infinity, - lowestInfDS = double.infinity, - highestTR = double.negativeInfinity, - highestGlixare = double.negativeInfinity, - highestGlicko = double.negativeInfinity, - highestRD = double.negativeInfinity, - highestWinrate = double.negativeInfinity, - highestAPM = double.negativeInfinity, - highestPPS = double.negativeInfinity, - highestVS = double.negativeInfinity, - highestAPP = double.negativeInfinity, - highestVSAPM = double.negativeInfinity, - highestDSS = double.negativeInfinity, - highestDSP = double.negativeInfinity, - highestAPPDSP = double.negativeInfinity, - highestCheese = double.negativeInfinity, - highestGBE = double.negativeInfinity, - highestNyaAPP = double.negativeInfinity, - highestArea = double.negativeInfinity, - highestEstTR = double.negativeInfinity, - highestEstAcc = double.negativeInfinity, - highestOpener = double.negativeInfinity, - highestPlonk = double.negativeInfinity, - highestStride = double.negativeInfinity, - highestInfDS = double.negativeInfinity; - int avgGamesPlayed = 0, - avgGamesWon = 0, - totalGamesPlayed = 0, - totalGamesWon = 0, - lowestGamesPlayed = pow(2, 53) as int, - lowestGamesWon = pow(2, 53) as int, - highestGamesPlayed = 0, - highestGamesWon = 0; - String lowestTRid = "", lowestTRnick = "", - lowestGlixareID = "", lowestGlixareNick = "", - lowestGlickoID = "", lowestGlickoNick = "", - lowestRdID = "", lowestRdNick = "", - lowestGamesPlayedID = "", lowestGamesPlayedNick = "", - lowestGamesWonID = "", lowestGamesWonNick = "", - lowestWinrateID = "", lowestWinrateNick = "", - lowestAPMid = "", lowestAPMnick = "", - lowestPPSid = "", lowestPPSnick = "", - lowestVSid = "", lowestVSnick = "", - lowestAPPid = "", lowestAPPnick = "", - lowestVSAPMid = "", lowestVSAPMnick = "", - lowestDSSid = "", lowestDSSnick = "", - lowestDSPid = "", lowestDSPnick = "", - lowestAPPDSPid = "", lowestAPPDSPnick = "", - lowestCheeseID = "", lowestCheeseNick = "", - lowestGBEid = "", lowestGBEnick = "", - lowestNyaAPPid = "", lowestNyaAPPnick = "", - lowestAreaID = "", lowestAreaNick = "", - lowestEstTRid = "", lowestEstTRnick = "", - lowestEstAccID = "", lowestEstAccNick = "", - lowestOpenerID = "", lowestOpenerNick = "", - lowestPlonkID = "", lowestPlonkNick = "", - lowestStrideID = "", lowestStrideNick = "", - lowestInfDSid = "", lowestInfDSnick = "", - highestTRid = "", highestTRnick = "", - highestGlixareID = "", highestGlixareNick = "", - highestGlickoID = "", highestGlickoNick = "", - highestRdID = "", highestRdNick = "", - highestGamesPlayedID = "", highestGamesPlayedNick = "", - highestGamesWonID = "", highestGamesWonNick = "", - highestWinrateID = "", highestWinrateNick = "", - highestAPMid = "", highestAPMnick = "", - highestPPSid = "", highestPPSnick = "", - highestVSid = "", highestVSnick = "", - highestAPPid = "", highestAPPnick = "", - highestVSAPMid = "", highestVSAPMnick = "", - highestDSSid = "", highestDSSnick = "", - highestDSPid = "", highestDSPnick = "", - highestAPPDSPid = "", highestAPPDSPnick = "", - highestCheeseID = "", highestCheeseNick = "", - highestGBEid = "", highestGBEnick = "", - highestNyaAPPid = "", highestNyaAPPnick = "", - highestAreaID = "", highestAreaNick = "", - highestEstTRid = "", highestEstTRnick = "", - highestEstAccID = "", highestEstAccNick = "", - highestOpenerID = "", highestOpenerNick = "", - highestPlonkID = "", highestPlonkNick = "", - highestStrideID = "", highestStrideNick = "", - highestInfDSid = "", highestInfDSnick = ""; - for (var entry in filtredLeaderboard){ - avgAPM += entry.apm; - avgPPS += entry.pps; - avgVS += entry.vs; - avgTR += entry.tr; - avgGlixare += entry.gxe; - if (entry.glicko != null) avgGlicko += entry.glicko!; - if (entry.rd != null) avgRD += entry.rd!; - avgAPP += entry.nerdStats.app; - avgVSAPM += entry.nerdStats.vsapm; - avgDSS += entry.nerdStats.dss; - avgDSP += entry.nerdStats.dsp; - avgAPPDSP += entry.nerdStats.appdsp; - avgCheese += entry.nerdStats.cheese; - avgGBE += entry.nerdStats.gbe; - avgNyaAPP += entry.nerdStats.nyaapp; - avgArea += entry.nerdStats.area; - avgEstTR += entry.estTr.esttr; - avgEstAcc += entry.esttracc; - avgOpener += entry.playstyle.opener; - avgPlonk += entry.playstyle.plonk; - avgStride += entry.playstyle.stride; - avgInfDS += entry.playstyle.infds; - totalGamesPlayed += entry.gamesPlayed; - totalGamesWon += entry.gamesWon; - if (entry.tr < lowestTR){ - lowestTR = entry.tr; - lowestTRid = entry.userId; - lowestTRnick = entry.username; - } - if (entry.gxe < lowestGlixare){ - lowestGlixare = entry.gxe; - lowestGlixareID = entry.userId; - lowestGlixareNick = entry.username; - } - if (entry.glicko != null && entry.glicko! < lowestGlicko){ - lowestGlicko = entry.glicko!; - lowestGlickoID = entry.userId; - lowestGlickoNick = entry.username; - } - if (entry.rd != null && entry.rd! < lowestRD){ - lowestRD = entry.rd!; - lowestRdID = entry.userId; - lowestRdNick = entry.username; - } - if (entry.gamesPlayed < lowestGamesPlayed){ - lowestGamesPlayed = entry.gamesPlayed; - lowestGamesPlayedID = entry.userId; - lowestGamesPlayedNick = entry.username; - } - if (entry.gamesWon < lowestGamesWon){ - lowestGamesWon = entry.gamesWon; - lowestGamesWonID = entry.userId; - lowestGamesWonNick = entry.username; - } - if (entry.winrate < lowestWinrate){ - lowestWinrate = entry.winrate; - lowestWinrateID = entry.userId; - lowestWinrateNick = entry.username; - } - if (entry.apm < lowestAPM){ - lowestAPM = entry.apm; - lowestAPMid = entry.userId; - lowestAPMnick = entry.username; - } - if (entry.pps < lowestPPS){ - lowestPPS = entry.pps; - lowestPPSid = entry.userId; - lowestPPSnick = entry.username; - } - if (entry.vs < lowestVS){ - lowestVS = entry.vs; - lowestVSid = entry.userId; - lowestVSnick = entry.username; - } - if (entry.nerdStats.app < lowestAPP){ - lowestAPP = entry.nerdStats.app; - lowestAPPid = entry.userId; - lowestAPPnick = entry.username; - } - if (entry.nerdStats.vsapm < lowestVSAPM){ - lowestVSAPM = entry.nerdStats.vsapm; - lowestVSAPMid = entry.userId; - lowestVSAPMnick = entry.username; - } - if (entry.nerdStats.dss < lowestDSS){ - lowestDSS = entry.nerdStats.dss; - lowestDSSid = entry.userId; - lowestDSSnick = entry.username; - } - if (entry.nerdStats.dsp < lowestDSP){ - lowestDSP = entry.nerdStats.dsp; - lowestDSPid = entry.userId; - lowestDSPnick = entry.username; - } - if (entry.nerdStats.appdsp < lowestAPPDSP){ - lowestAPPDSP = entry.nerdStats.appdsp; - lowestAPPDSPid = entry.userId; - lowestAPPDSPnick = entry.username; - } - if (entry.nerdStats.cheese < lowestCheese){ - lowestCheese = entry.nerdStats.cheese; - lowestCheeseID = entry.userId; - lowestCheeseNick = entry.username; - } - if (entry.nerdStats.gbe < lowestGBE){ - lowestGBE = entry.nerdStats.gbe; - lowestGBEid = entry.userId; - lowestGBEnick = entry.username; - } - if (entry.nerdStats.nyaapp < lowestNyaAPP){ - lowestNyaAPP = entry.nerdStats.nyaapp; - lowestNyaAPPid = entry.userId; - lowestNyaAPPnick = entry.username; - } - if (entry.nerdStats.area < lowestArea){ - lowestArea = entry.nerdStats.area; - lowestAreaID = entry.userId; - lowestAreaNick = entry.username; - } - if (entry.estTr.esttr < lowestEstTR){ - lowestEstTR = entry.estTr.esttr; - lowestEstTRid = entry.userId; - lowestEstTRnick = entry.username; - } - if (entry.esttracc < lowestEstAcc){ - lowestEstAcc = entry.esttracc; - lowestEstAccID = entry.userId; - lowestEstAccNick = entry.username; - } - if (entry.playstyle.opener < lowestOpener){ - lowestOpener = entry.playstyle.opener; - lowestOpenerID = entry.userId; - lowestOpenerNick = entry.username; - } - if (entry.playstyle.plonk < lowestPlonk){ - lowestPlonk = entry.playstyle.plonk; - lowestPlonkID = entry.userId; - lowestPlonkNick = entry.username; - } - if (entry.playstyle.stride < lowestStride){ - lowestStride = entry.playstyle.stride; - lowestStrideID = entry.userId; - lowestStrideNick = entry.username; - } - if (entry.playstyle.infds < lowestInfDS){ - lowestInfDS = entry.playstyle.infds; - lowestInfDSid = entry.userId; - lowestInfDSnick = entry.username; - } - if (entry.tr > highestTR){ - highestTR = entry.tr; - highestTRid = entry.userId; - highestTRnick = entry.username; - } - if (entry.gxe > highestGlixare){ - highestGlixare = entry.gxe; - highestGlixareID = entry.userId; - highestGlixareNick = entry.username; - } - if (entry.glicko != null && entry.glicko! > highestGlicko){ - highestGlicko = entry.glicko!; - highestGlickoID = entry.userId; - highestGlickoNick = entry.username; - } - if (entry.rd != null && entry.rd! > highestRD){ - highestRD = entry.rd!; - highestRdID = entry.userId; - highestRdNick = entry.username; - } - if (entry.gamesPlayed > highestGamesPlayed){ - highestGamesPlayed = entry.gamesPlayed; - highestGamesPlayedID = entry.userId; - highestGamesPlayedNick = entry.username; - } - if (entry.gamesWon > highestGamesWon){ - highestGamesWon = entry.gamesWon; - highestGamesWonID = entry.userId; - highestGamesWonNick = entry.username; - } - if (entry.winrate > highestWinrate){ - highestWinrate = entry.winrate; - highestWinrateID = entry.userId; - highestWinrateNick = entry.username; - } - if (entry.apm > highestAPM){ - highestAPM = entry.apm; - highestAPMid = entry.userId; - highestAPMnick = entry.username; - } - if (entry.pps > highestPPS){ - highestPPS = entry.pps; - highestPPSid = entry.userId; - highestPPSnick = entry.username; - } - if (entry.vs > highestVS){ - highestVS = entry.vs; - highestVSid = entry.userId; - highestVSnick = entry.username; - } - if (entry.nerdStats.app > highestAPP){ - highestAPP = entry.nerdStats.app; - highestAPPid = entry.userId; - highestAPPnick = entry.username; - } - if (entry.nerdStats.vsapm > highestVSAPM){ - highestVSAPM = entry.nerdStats.vsapm; - highestVSAPMid = entry.userId; - highestVSAPMnick = entry.username; - } - if (entry.nerdStats.dss > highestDSS){ - highestDSS = entry.nerdStats.dss; - highestDSSid = entry.userId; - highestDSSnick = entry.username; - } - if (entry.nerdStats.dsp > highestDSP){ - highestDSP = entry.nerdStats.dsp; - highestDSPid = entry.userId; - highestDSPnick = entry.username; - } - if (entry.nerdStats.appdsp > highestAPPDSP){ - highestAPPDSP = entry.nerdStats.appdsp; - highestAPPDSPid = entry.userId; - highestAPPDSPnick = entry.username; - } - if (entry.nerdStats.cheese > highestCheese){ - highestCheese = entry.nerdStats.cheese; - highestCheeseID = entry.userId; - highestCheeseNick = entry.username; - } - if (entry.nerdStats.gbe > highestGBE){ - highestGBE = entry.nerdStats.gbe; - highestGBEid = entry.userId; - highestGBEnick = entry.username; - } - if (entry.nerdStats.nyaapp > highestNyaAPP){ - highestNyaAPP = entry.nerdStats.nyaapp; - highestNyaAPPid = entry.userId; - highestNyaAPPnick = entry.username; - } - if (entry.nerdStats.area > highestArea){ - highestArea = entry.nerdStats.area; - highestAreaID = entry.userId; - highestAreaNick = entry.username; - } - if (entry.estTr.esttr > highestEstTR){ - highestEstTR = entry.estTr.esttr; - highestEstTRid = entry.userId; - highestEstTRnick = entry.username; - } - if (entry.esttracc > highestEstAcc){ - highestEstAcc = entry.esttracc; - highestEstAccID = entry.userId; - highestEstAccNick = entry.username; - } - if (entry.playstyle.opener > highestOpener){ - highestOpener = entry.playstyle.opener; - highestOpenerID = entry.userId; - highestOpenerNick = entry.username; - } - if (entry.playstyle.plonk > highestPlonk){ - highestPlonk = entry.playstyle.plonk; - highestPlonkID = entry.userId; - highestPlonkNick = entry.username; - } - if (entry.playstyle.stride > highestStride){ - highestStride = entry.playstyle.stride; - highestStrideID = entry.userId; - highestStrideNick = entry.username; - } - if (entry.playstyle.infds > highestInfDS){ - highestInfDS = entry.playstyle.infds; - highestInfDSid = entry.userId; - highestInfDSnick = entry.username; - } - } - avgAPM /= filtredLeaderboard.length; - avgPPS /= filtredLeaderboard.length; - avgVS /= filtredLeaderboard.length; - avgTR /= filtredLeaderboard.length; - avgGlixare /= filtredLeaderboard.length; - avgGlicko /= filtredLeaderboard.length; - avgRD /= filtredLeaderboard.length; - avgAPP /= filtredLeaderboard.length; - avgVSAPM /= filtredLeaderboard.length; - avgDSS /= filtredLeaderboard.length; - avgDSP /= filtredLeaderboard.length; - avgAPPDSP /= leaderboard.length; - avgCheese /= filtredLeaderboard.length; - avgGBE /= filtredLeaderboard.length; - avgNyaAPP /= filtredLeaderboard.length; - avgArea /= filtredLeaderboard.length; - avgEstTR /= filtredLeaderboard.length; - avgEstAcc /= filtredLeaderboard.length; - avgOpener /= filtredLeaderboard.length; - avgPlonk /= filtredLeaderboard.length; - avgStride /= filtredLeaderboard.length; - avgInfDS /= filtredLeaderboard.length; - avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor(); - avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor(); - return [TetraLeague(id: "", timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, gxe: avgGlixare, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason), - { - "everyone": rank == "", - "totalGamesPlayed": totalGamesPlayed, - "totalGamesWon": totalGamesWon, - "players": filtredLeaderboard.length, - "lowestTR": lowestTR, - "lowestTRid": lowestTRid, - "lowestTRnick": lowestTRnick, - "lowestGlixare": lowestGlixare, - "lowestGlixareID": lowestGlixareID, - "lowestGlixareNick": lowestGlixareNick, - "lowestS1tr": lowestGlixare * 250, - "lowestS1trID": lowestGlixareID, - "lowestS1trNick": lowestGlixareNick, - "lowestGlicko": lowestGlicko, - "lowestGlickoID": lowestGlickoID, - "lowestGlickoNick": lowestGlickoNick, - "lowestRD": lowestRD, - "lowestRdID": lowestRdID, - "lowestRdNick": lowestRdNick, - "lowestGamesPlayed": lowestGamesPlayed, - "lowestGamesPlayedID": lowestGamesPlayedID, - "lowestGamesPlayedNick": lowestGamesPlayedNick, - "lowestGamesWon": lowestGamesWon, - "lowestGamesWonID": lowestGamesWonID, - "lowestGamesWonNick": lowestGamesWonNick, - "lowestWinrate": lowestWinrate, - "lowestWinrateID": lowestWinrateID, - "lowestWinrateNick": lowestWinrateNick, - "lowestAPM": lowestAPM, - "lowestAPMid": lowestAPMid, - "lowestAPMnick": lowestAPMnick, - "lowestPPS": lowestPPS, - "lowestPPSid": lowestPPSid, - "lowestPPSnick": lowestPPSnick, - "lowestVS": lowestVS, - "lowestVSid": lowestVSid, - "lowestVSnick": lowestVSnick, - "lowestAPP": lowestAPP, - "lowestAPPid": lowestAPPid, - "lowestAPPnick": lowestAPPnick, - "lowestVSAPM": lowestVSAPM, - "lowestVSAPMid": lowestVSAPMid, - "lowestVSAPMnick": lowestVSAPMnick, - "lowestDSS": lowestDSS, - "lowestDSSid": lowestDSSid, - "lowestDSSnick": lowestDSSnick, - "lowestDSP": lowestDSP, - "lowestDSPid": lowestDSPid, - "lowestDSPnick": lowestDSPnick, - "lowestAPPDSP": lowestAPPDSP, - "lowestAPPDSPid": lowestAPPDSPid, - "lowestAPPDSPnick": lowestAPPDSPnick, - "lowestCheese": lowestCheese, - "lowestCheeseID": lowestCheeseID, - "lowestCheeseNick": lowestCheeseNick, - "lowestGBE": lowestGBE, - "lowestGBEid": lowestGBEid, - "lowestGBEnick": lowestGBEnick, - "lowestNyaAPP": lowestNyaAPP, - "lowestNyaAPPid": lowestNyaAPPid, - "lowestNyaAPPnick": lowestNyaAPPnick, - "lowestArea": lowestArea, - "lowestAreaID": lowestAreaID, - "lowestAreaNick": lowestAreaNick, - "lowestEstTR": lowestEstTR, - "lowestEstTRid": lowestEstTRid, - "lowestEstTRnick": lowestEstTRnick, - "lowestEstAcc": lowestEstAcc, - "lowestEstAccID": lowestEstAccID, - "lowestEstAccNick": lowestEstAccNick, - "lowestOpener": lowestOpener, - "lowestOpenerID": lowestOpenerID, - "lowestOpenerNick": lowestOpenerNick, - "lowestPlonk": lowestPlonk, - "lowestPlonkID": lowestPlonkID, - "lowestPlonkNick": lowestPlonkNick, - "lowestStride": lowestStride, - "lowestStrideID": lowestStrideID, - "lowestStrideNick": lowestStrideNick, - "lowestInfDS": lowestInfDS, - "lowestInfDSid": lowestInfDSid, - "lowestInfDSnick": lowestInfDSnick, - "highestTR": highestTR, - "highestTRid": highestTRid, - "highestTRnick": highestTRnick, - "highestGlixare": highestGlixare, - "highestGlixareID": highestGlixareID, - "highestGlixareNick": highestGlixareNick, - "highestS1tr": highestGlixare * 250, - "highestS1trID": highestGlixareID, - "highestS1trNick": highestGlixareNick, - "highestGlicko": highestGlicko, - "highestGlickoID": highestGlickoID, - "highestGlickoNick": highestGlickoNick, - "highestRD": highestRD, - "highestRdID": highestRdID, - "highestRdNick": highestRdNick, - "highestGamesPlayed": highestGamesPlayed, - "highestGamesPlayedID": highestGamesPlayedID, - "highestGamesPlayedNick": highestGamesPlayedNick, - "highestGamesWon": highestGamesWon, - "highestGamesWonID": highestGamesWonID, - "highestGamesWonNick": highestGamesWonNick, - "highestWinrate": highestWinrate, - "highestWinrateID": highestWinrateID, - "highestWinrateNick": highestWinrateNick, - "highestAPM": highestAPM, - "highestAPMid": highestAPMid, - "highestAPMnick": highestAPMnick, - "highestPPS": highestPPS, - "highestPPSid": highestPPSid, - "highestPPSnick": highestPPSnick, - "highestVS": highestVS, - "highestVSid": highestVSid, - "highestVSnick": highestVSnick, - "highestAPP": highestAPP, - "highestAPPid": highestAPPid, - "highestAPPnick": highestAPPnick, - "highestVSAPM": highestVSAPM, - "highestVSAPMid": highestVSAPMid, - "highestVSAPMnick": highestVSAPMnick, - "highestDSS": highestDSS, - "highestDSSid": highestDSSid, - "highestDSSnick": highestDSSnick, - "highestDSP": highestDSP, - "highestDSPid": highestDSPid, - "highestDSPnick": highestDSPnick, - "highestAPPDSP": highestAPPDSP, - "highestAPPDSPid": highestAPPDSPid, - "highestAPPDSPnick": highestAPPDSPnick, - "highestCheese": highestCheese, - "highestCheeseID": highestCheeseID, - "highestCheeseNick": highestCheeseNick, - "highestGBE": highestGBE, - "highestGBEid": highestGBEid, - "highestGBEnick": highestGBEnick, - "highestNyaAPP": highestNyaAPP, - "highestNyaAPPid": highestNyaAPPid, - "highestNyaAPPnick": highestNyaAPPnick, - "highestArea": highestArea, - "highestAreaID": highestAreaID, - "highestAreaNick": highestAreaNick, - "highestEstTR": highestEstTR, - "highestEstTRid": highestEstTRid, - "highestEstTRnick": highestEstTRnick, - "highestEstAcc": highestEstAcc, - "highestEstAccID": highestEstAccID, - "highestEstAccNick": highestEstAccNick, - "highestOpener": highestOpener, - "highestOpenerID": highestOpenerID, - "highestOpenerNick": highestOpenerNick, - "highestPlonk": highestPlonk, - "highestPlonkID": highestPlonkID, - "highestPlonkNick": highestPlonkNick, - "highestStride": highestStride, - "highestStrideID": highestStrideID, - "highestStrideNick": highestStrideNick, - "highestInfDS": highestInfDS, - "highestInfDSid": highestInfDSid, - "highestInfDSnick": highestInfDSnick, - "avgAPP": avgAPP, - "avgVSAPM": avgVSAPM, - "avgDSS": avgDSS, - "avgDSP": avgDSP, - "avgAPPDSP": avgAPPDSP, - "avgCheese": avgCheese, - "avgGBE": avgGBE, - "avgNyaAPP": avgNyaAPP, - "avgArea": avgArea, - "avgEstTR": avgEstTR, - "avgEstAcc": avgEstAcc, - "avgOpener": avgOpener, - "avgPlonk": avgPlonk, - "avgStride": avgStride, - "avgInfDS": avgInfDS, - "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].tr : lowestTR, - "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0, - "entries": filtredLeaderboard - }]; - }else{ - return [TetraLeague(id: "", timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, tr: 0, rank: rank, percentileRank: rank, gxe: -1, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason), - {"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0, "toEnterGlicko": 0}]; - } - } - - PlayerLeaderboardPosition? getLeaderboardPosition(Mapleague) { - if (league.values.first.gamesPlayed == 0) return null; - bool fakePositions = false; - late List copyOfLeaderboard; - if (leaderboard.indexWhere((element) => element.userId == league.keys.first) == -1){ - fakePositions =true; - copyOfLeaderboard = List.of(leaderboard); - copyOfLeaderboard.add(league.values.first.convertToPlayerFromLeaderboard(league.keys.first)); - } - 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(fakePositions ? copyOfLeaderboard : leaderboard, stat, reversed: stat == Stats.cheese ? true : false); - int position = sortedLeaderboard.indexWhere((element) => element.userId == league.keys.first) + 1; - if (position == 0) { - results.add(null); - } else { - results.add(LeaderboardPosition(fakePositions ? 1001 : position, position / sortedLeaderboard.length)); - } - } - return PlayerLeaderboardPosition.fromSearchResults(results); - } - - Map> get averages => { - 'x+': getAverageOfRank("x+"), - 'x': getAverageOfRank("x"), - 'u': getAverageOfRank("u"), - 'ss': getAverageOfRank("ss"), - 's+': getAverageOfRank("s+"), - 's': getAverageOfRank("s"), - 's-': getAverageOfRank("s-"), - 'a+': getAverageOfRank("a+"), - 'a': getAverageOfRank("a"), - 'a-': getAverageOfRank("a-"), - 'b+': getAverageOfRank("b+"), - 'b': getAverageOfRank("b"), - 'b-': getAverageOfRank("b-"), - 'c+': getAverageOfRank("c+"), - 'c': getAverageOfRank("c"), - 'c-': getAverageOfRank("c-"), - 'd+': getAverageOfRank("d+"), - 'd': getAverageOfRank("d"), - '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"] - }; - - Map get cutoffsGlicko => { - 'x': getAverageOfRank("x")[1]["toEnterGlicko"], - 'u': getAverageOfRank("u")[1]["toEnterGlicko"], - 'ss': getAverageOfRank("ss")[1]["toEnterGlicko"], - 's+': getAverageOfRank("s+")[1]["toEnterGlicko"], - 's': getAverageOfRank("s")[1]["toEnterGlicko"], - 's-': getAverageOfRank("s-")[1]["toEnterGlicko"], - 'a+': getAverageOfRank("a+")[1]["toEnterGlicko"], - 'a': getAverageOfRank("a")[1]["toEnterGlicko"], - 'a-': getAverageOfRank("a-")[1]["toEnterGlicko"], - 'b+': getAverageOfRank("b+")[1]["toEnterGlicko"], - 'b': getAverageOfRank("b")[1]["toEnterGlicko"], - 'b-': getAverageOfRank("b-")[1]["toEnterGlicko"], - 'c+': getAverageOfRank("c+")[1]["toEnterGlicko"], - 'c': getAverageOfRank("c")[1]["toEnterGlicko"], - 'c-': getAverageOfRank("c-")[1]["toEnterGlicko"], - 'd+': getAverageOfRank("d+")[1]["toEnterGlicko"], - 'd': getAverageOfRank("d")[1]["toEnterGlicko"] - }; - - TetrioPlayersLeaderboard.fromJson(List json, String t, DateTime ts) { - type = t; - timestamp = ts; - leaderboard = []; - for (Map entry in json) { - leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts)); - } - } - - addPlayers(List list){ - leaderboard.addAll(list); - } -} - -class TetrioPlayerFromLeaderboard { - late String userId; - late String username; - late String role; - late double xp; - String? country; - late DateTime timestamp; - late int gamesPlayed; - late int gamesWon; - late double tr; - late double gxe; - late double? glicko; - late double? rd; - late String rank; - late String? bestRank; - late double apm; - late double pps; - late double vs; - late bool decaying; - late NerdStats nerdStats; - late EstTr estTr; - late Playstyle playstyle; - - TetrioPlayerFromLeaderboard( - this.userId, - this.username, - this.role, - this.xp, - this.country, - this.timestamp, - this.gamesPlayed, - this.gamesWon, - this.tr, - this.gxe, - this.glicko, - this.rd, - this.rank, - this.bestRank, - this.apm, - this.pps, - this.vs, - 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 - tr; - double get s1tr => gxe * 250; - - TetrioPlayerFromLeaderboard.fromJson(Map json, DateTime ts) { - userId = json['_id']; - username = json['username']; - role = json['role']; - xp = json['xp'].toDouble(); - country = json['country']; - timestamp = ts; - gamesPlayed = json['league']['gamesplayed'] as int; - gamesWon = json['league']['gameswon'] as int; - tr = json['league']['tr'] != null ? json['league']['tr'].toDouble() : 0; - gxe = json['league']['gxe']??-1; - glicko = json['league']['glicko']?.toDouble(); - rd = json['league']['rd']?.toDouble(); - rank = json['league']['rank']; - bestRank = json['league']['bestrank']; - apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00; - pps = json['league']['pps'] != null ? json['league']['pps'].toDouble() : 0.00; - vs = json['league']['vs'] != null ? json['league']['vs'].toDouble(): 0.00; - decaying = json['league']['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); - } - - num getStatByEnum(Stats stat){ - switch (stat) { - case Stats.tr: - return tr; - case Stats.glicko: - return glicko??-1; - case Stats.gxe: - return gxe; - case Stats.s1tr: - return s1tr; - case Stats.rd: - return rd??-1; - case Stats.gp: - return gamesPlayed; - case Stats.gw: - return gamesWon; - case Stats.wr: - return winrate*100; - case Stats.apm: - return apm; - case Stats.pps: - return pps; - case Stats.vs: - return vs; - case Stats.app: - return nerdStats.app; - case Stats.dss: - return nerdStats.dss; - case Stats.dsp: - return nerdStats.dsp; - case Stats.appdsp: - return nerdStats.appdsp; - case Stats.vsapm: - return nerdStats.vsapm; - case Stats.cheese: - return nerdStats.cheese; - case Stats.gbe: - return nerdStats.gbe; - case Stats.nyaapp: - return nerdStats.nyaapp; - case Stats.area: - return nerdStats.area; - case Stats.eTR: - return estTr.esttr; - case Stats.acceTR: - return esttracc; - case Stats.acceTRabs: - return esttracc.abs(); - case Stats.opener: - return playstyle.opener; - case Stats.plonk: - return playstyle.plonk; - case Stats.infDS: - return playstyle.infds; - case Stats.stride: - return playstyle.stride; - case Stats.stridemMinusPlonk: - return playstyle.stride - playstyle.plonk; - case Stats.openerMinusInfDS: - return playstyle.opener - playstyle.infds; - } - } -} - -class CutoffTetrio { - late int pos; - late double percentile; - late double tr; - late double targetTr; - late double apm; - late double pps; - late double vs; - late int count; - late double countPercentile; - - CutoffTetrio.fromJson(Map json, int total){ - pos = json['pos']; - percentile = json['percentile'].toDouble(); - tr = json['tr'].toDouble(); - targetTr = json['targettr'].toDouble(); - apm = json['apm'].toDouble(); - pps = json['pps'].toDouble(); - vs = json['vs'].toDouble(); - count = json['count']; - countPercentile = count / total; - } -} - -class CutoffsTetrio { - late String id; - late DateTime timestamp; - late int total; - Map data = {}; - - CutoffsTetrio.fromJson(Map json){ - id = json['s']; - timestamp = DateTime.parse(json['t']); - total = json['data']['total']; - json['data'].remove("total"); - for (String rank in json['data'].keys){ - data[rank] = CutoffTetrio.fromJson(json['data'][rank], total); - } - } -} \ No newline at end of file diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart new file mode 100644 index 0000000..b397d38 --- /dev/null +++ b/lib/data_objects/tetrio_constants.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; + +const int currentSeason = 2; +const double noTrRd = 60.9; +const double apmWeight = 1; +const double ppsWeight = 45; +const double vsWeight = 0.444; +const double appWeight = 185; +const double dssWeight = 175; +const double dspWeight = 450; +const double appdspWeight = 140; +const double vsapmWeight = 60; +const double cheeseWeight = 1.25; +const double gbeWeight = 315; +const List ranks = [ + "d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x", "x+" +]; +const Map rankCutoffs = { + "x+": 0.002, + "x": 0.01, + "u": 0.05, + "ss": 0.11, + "s+": 0.17, + "s": 0.23, + "s-": 0.3, + "a+": 0.38, + "a": 0.46, + "a-": 0.54, + "b+": 0.62, + "b": 0.7, + "b-": 0.78, + "c+": 0.84, + "c": 0.9, + "c-": 0.95, + "d+": 0.975, + "d": 1, + "z": -1, + "": 0.5 +}; +const Map rankTargets = { + "x+": 24000.00, + "x": 22500.00, + "u": 20000.00, + "ss": 18000.00, + "s+": 16500.00, + "s": 15200.00, + "s-": 13800.00, + "a+": 12000.00, + "a": 10500.00, + "a-": 9000.00, + "b+": 7400.00, + "b": 5700.00, + "b-": 4200.00, + "c+": 3000.00, + "c": 2000.00, + "c-": 1300.00, + "d+": 800.00, + "d": 0.00, +}; +// DateTime seasonStart = DateTime.utc(2024, 08, 16, 18); +//DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15); +enum Stats { + tr, + glicko, + gxe, + s1tr, + rd, + gp, + gw, + wr, + apm, + pps, + vs, + app, + dss, + dsp, + appdsp, + vsapm, + cheese, + gbe, + nyaapp, + area, + eTR, + acceTR, + acceTRabs, + opener, + plonk, + infDS, + stride, + stridemMinusPlonk, + openerMinusInfDS + } + +const Map chartsShortTitles = { + Stats.tr: "TR", + Stats.gxe: "Glixare", + Stats.s1tr: "S1 TR", + Stats.glicko: "Glicko", + Stats.rd: "RD", + Stats.gp: "GP", + Stats.gw: "GW", + Stats.wr: "WR%", + Stats.apm: "APM", + Stats.pps: "PPS", + Stats.vs: "VS", + Stats.app: "APP", + Stats.dss: "DS/S", + Stats.dsp: "DS/P", + Stats.appdsp: "APP + DS/P", + Stats.vsapm: "VS/APM", + Stats.cheese: "Cheese", + Stats.gbe: "GbE", + Stats.nyaapp: "wAPP", + Stats.area: "Area", + Stats.eTR: "eTR", + Stats.acceTR: "±eTR", + Stats.acceTRabs: "+eTR absolute", + Stats.opener: "Opener", + Stats.plonk: "Plonk", + Stats.infDS: "Inf. DS", + Stats.stride: "Stride", + Stats.stridemMinusPlonk: "Stride - Plonk", + Stats.openerMinusInfDS: "Opener - Inf. DS" + }; + +const Map rankColors = { // thanks osk for const rankColors at https://ch.tetr.io/res/js/base.js:458 + 'x+': Color(0xFF643C8D), + 'x': Color(0xFFFF45FF), + 'u': Color(0xFFFF3813), + 'ss': Color(0xFFDB8B1F), + 's+': Color(0xFFD8AF0E), + 's': Color(0xFFE0A71B), + 's-': Color(0xFFB2972B), + 'a+': Color(0xFF1FA834), + 'a': Color(0xFF46AD51), + 'a-': Color(0xFF3BB687), + 'b+': Color(0xFF4F99C0), + 'b': Color(0xFF4F64C9), + 'b-': Color(0xFF5650C7), + 'c+': Color(0xFF552883), + 'c': Color(0xFF733E8F), + 'c-': Color(0xFF79558C), + 'd+': Color(0xFF8E6091), + 'd': Color(0xFF907591), + 'z': Color(0xFF375433) +}; + +const Map sprintAverages = { // based on https://discord.com/channels/673303546107658242/674421736162197515/1244287342965952562 + 'x': Duration(seconds: 25, milliseconds: 144), + 'u': Duration(seconds: 36, milliseconds: 115), + 'ss': Duration(seconds: 46, milliseconds: 396), + 's+': Duration(seconds: 55, milliseconds: 056), + 's': Duration(seconds: 61, milliseconds: 892), + 's-': Duration(seconds: 68, milliseconds: 918), + 'a+': Duration(seconds: 76, milliseconds: 187), + 'a': Duration(seconds: 83, milliseconds: 529), + 'a-': Duration(seconds: 88, milliseconds: 608), + 'b+': Duration(seconds: 97, milliseconds: 626), + 'b': Duration(seconds: 104, milliseconds: 687), + 'b-': Duration(seconds: 113, milliseconds: 372), + 'c+': Duration(seconds: 123, milliseconds: 461), + 'c': Duration(seconds: 135, milliseconds: 326), + 'c-': Duration(seconds: 147, milliseconds: 056), + 'd+': Duration(seconds: 156, milliseconds: 757), + 'd': Duration(seconds: 167, milliseconds: 393), + //'z': Duration(seconds: 66, milliseconds: 802) +}; + +const Map blitzAverages = { + 'x': 600715, + 'u': 379418, + 'ss': 233740, + 's+': 158295, + 's': 125164, + 's-': 100933, + 'a+': 83593, + 'a': 68613, + 'a-': 60219, + 'b+': 51197, + 'b': 44171, + 'b-': 39045, + 'c+': 34130, + 'c': 28931, + 'c-': 25095, + 'd+': 22944, + 'd': 20728, + //'z': 72084 +}; \ 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 e4dd836..68f1d04 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,7 +1,12 @@ import 'dart:math'; import 'dart:typed_data'; -import 'tetrio.dart'; +import 'package:tetra_stats/data_objects/clears.dart'; +import 'package:tetra_stats/data_objects/end_context_multi.dart'; +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/finesse.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; // I want to implement those fancy TWC stats // So, i'm going to read replay for things diff --git a/lib/data_objects/tetrio_player.dart b/lib/data_objects/tetrio_player.dart new file mode 100644 index 0000000..63fd430 --- /dev/null +++ b/lib/data_objects/tetrio_player.dart @@ -0,0 +1,150 @@ +// ignore_for_file: hash_and_equals + +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:tetra_stats/data_objects/badge.dart'; +import 'package:tetra_stats/data_objects/connections.dart'; +import 'package:tetra_stats/data_objects/distinguishment.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; + +class TetrioPlayer { + late String userId; + late String username; + late DateTime state; + late String role; + int? avatarRevision; + int? bannerRevision; + DateTime? registrationTime; + List badges = []; + String? bio; + String? country; + late int friendCount; + late int gamesPlayed; + late int gamesWon; + late Duration gameTime; + late double xp; + late int supporterTier; + late bool verified; + bool? badstanding; + String? botmaster; + Connections? connections; + TetrioZen? zen; + Distinguishment? distinguishment; + DateTime? cachedUntil; + + TetrioPlayer({ + required this.userId, + required this.username, + required this.role, + required this.state, + this.avatarRevision, + this.bannerRevision, + this.registrationTime, + required this.badges, + this.bio, + this.country, + required this.friendCount, + required this.gamesPlayed, + required this.gamesWon, + required this.gameTime, + required this.xp, + required this.supporterTier, + required this.verified, + this.badstanding, + this.botmaster, + required this.connections, + this.zen, + this.distinguishment, + this.cachedUntil + }); + + double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1; + + TetrioPlayer.fromJson(Map json, DateTime stateTime, String id, String nick, [DateTime? cUntil]) { + //developer.log("TetrioPlayer.fromJson $stateTime: $json", name: "data_objects/tetrio"); + userId = id; + username = nick; + state = stateTime; + role = json['role']; + registrationTime = json['ts'] != null ? DateTime.parse(json['ts']) : DateTime.fromMillisecondsSinceEpoch(int.parse(id.substring(0, 8), radix: 16) * 1000); + if (json['badges'] != null) { + json['badges'].forEach((v) { + badges.add(Badge.fromJson(v)); + }); + } + xp = json['xp'] != null ? json['xp'].toDouble() : -1; + gamesPlayed = json['gamesplayed'] ?? -1; + gamesWon = json['gameswon'] ?? -1; + gameTime = json['gametime'] != null && json['gametime'] != -1 ? Duration(microseconds: (json['gametime'].toDouble() * 1000000).floor()) : const Duration(seconds: -1); + country = json['country']; + supporterTier = json['supporter_tier'] ?? 0; + verified = json['verified'] ?? false; + avatarRevision = json['avatar_revision']; + bannerRevision = json['banner_revision']; + bio = json['bio']; + if (json['connections'] != null && json['connections'].isNotEmpty) connections = Connections.fromJson(json['connections']); + distinguishment = json['distinguishment'] != null ? Distinguishment.fromJson(json['distinguishment']) : null; + friendCount = json['friend_count'] ?? 0; + badstanding = json['badstanding']; + botmaster = json['botmaster']; + cachedUntil = cUntil; + } + + Map toJson() { + final Map data = {}; + // data['_id'] = userId; + // data['username'] = username; + data['role'] = role; + if (registrationTime != null) data['ts'] = registrationTime?.toString(); + if (badges.isNotEmpty) data['badges'] = badges.map((v) => v.toJson()).toList(); + if (xp >= 0) data['xp'] = xp; + if (gamesPlayed >= 0) data['gamesplayed'] = gamesPlayed; + if (gamesWon >= 0) data['gameswon'] = gamesWon; + if (!gameTime.isNegative) data['gametime'] = gameTime.inMicroseconds / 1000000; + if (country != null) data['country'] = country; + if (supporterTier > 0) data['supporter_tier'] = supporterTier; + if (verified) data['verified'] = verified; + if (distinguishment != null) data['distinguishment'] = distinguishment?.toJson(); + if (avatarRevision != null) data['avatar_revision'] = avatarRevision; + if (bannerRevision != null) data['banner_revision'] = bannerRevision; + if (bio != null) data['bio'] = bio; + if (connections != null) data['connections'] = connections!.toJson(); + if (friendCount > 0) data['friend_count'] = friendCount; + if (badstanding != null) data['badstanding'] = badstanding; + if (botmaster != null) data['botmaster'] = botmaster; + //developer.log("TetrioPlayer.toJson: $data", name: "data_objects/tetrio"); + return data; + } + + bool isSameState(covariant TetrioPlayer other) { + if (userId != other.userId) return false; + if (username != other.username) return false; + if (role != other.role) return false; + if (listEquals(badges, other.badges) == false) return false; + //if (bio != other.bio) return false; + if (country != other.country) return false; + if (friendCount != other.friendCount) return false; + if (gamesPlayed != other.gamesPlayed) return false; + if (gamesWon != other.gamesWon) return false; + if (gameTime != other.gameTime) return false; + if (xp != other.xp) return false; + if (supporterTier != other.supporterTier) return false; + if (verified != other.verified) return false; + if (badstanding != other.badstanding) return false; + if (botmaster != other.botmaster) return false; + if (connections != other.connections) return false; + if (distinguishment != other.distinguishment) return false; + return true; + } + + @override + String toString() { + return "$username ($state)"; + } + + @override + int get hashCode => state.hashCode; + + @override + bool operator ==(covariant TetrioPlayer other) => isSameState(other) && state.isAtSameMomentAs(other.state); +} diff --git a/lib/data_objects/tetrio_player_from_leaderboard.dart b/lib/data_objects/tetrio_player_from_leaderboard.dart new file mode 100644 index 0000000..28a7fab --- /dev/null +++ b/lib/data_objects/tetrio_player_from_leaderboard.dart @@ -0,0 +1,145 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; + +class TetrioPlayerFromLeaderboard { + late String userId; + late String username; + late String role; + late double xp; + String? country; + late DateTime timestamp; + late int gamesPlayed; + late int gamesWon; + late double tr; + late double gxe; + late double? glicko; + late double? rd; + late String rank; + late String? bestRank; + late double apm; + late double pps; + late double vs; + late bool decaying; + late NerdStats nerdStats; + late EstTr estTr; + late Playstyle playstyle; + + TetrioPlayerFromLeaderboard( + this.userId, + this.username, + this.role, + this.xp, + this.country, + this.timestamp, + this.gamesPlayed, + this.gamesWon, + this.tr, + this.gxe, + this.glicko, + this.rd, + this.rank, + this.bestRank, + this.apm, + this.pps, + this.vs, + 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 - tr; + double get s1tr => gxe * 250; + + TetrioPlayerFromLeaderboard.fromJson(Map json, DateTime ts) { + userId = json['_id']; + username = json['username']; + role = json['role']; + xp = json['xp'].toDouble(); + country = json['country']; + timestamp = ts; + gamesPlayed = json['league']['gamesplayed'] as int; + gamesWon = json['league']['gameswon'] as int; + tr = json['league']['tr'] != null ? json['league']['tr'].toDouble() : 0; + gxe = json['league']['gxe']??-1; + glicko = json['league']['glicko']?.toDouble(); + rd = json['league']['rd']?.toDouble(); + rank = json['league']['rank']; + bestRank = json['league']['bestrank']; + apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00; + pps = json['league']['pps'] != null ? json['league']['pps'].toDouble() : 0.00; + vs = json['league']['vs'] != null ? json['league']['vs'].toDouble(): 0.00; + decaying = json['league']['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); + } + + num getStatByEnum(Stats stat){ + switch (stat) { + case Stats.tr: + return tr; + case Stats.glicko: + return glicko??-1; + case Stats.gxe: + return gxe; + case Stats.s1tr: + return s1tr; + case Stats.rd: + return rd??-1; + case Stats.gp: + return gamesPlayed; + case Stats.gw: + return gamesWon; + case Stats.wr: + return winrate*100; + case Stats.apm: + return apm; + case Stats.pps: + return pps; + case Stats.vs: + return vs; + case Stats.app: + return nerdStats.app; + case Stats.dss: + return nerdStats.dss; + case Stats.dsp: + return nerdStats.dsp; + case Stats.appdsp: + return nerdStats.appdsp; + case Stats.vsapm: + return nerdStats.vsapm; + case Stats.cheese: + return nerdStats.cheese; + case Stats.gbe: + return nerdStats.gbe; + case Stats.nyaapp: + return nerdStats.nyaapp; + case Stats.area: + return nerdStats.area; + case Stats.eTR: + return estTr.esttr; + case Stats.acceTR: + return esttracc; + case Stats.acceTRabs: + return esttracc.abs(); + case Stats.opener: + return playstyle.opener; + case Stats.plonk: + return playstyle.plonk; + case Stats.infDS: + return playstyle.infds; + case Stats.stride: + return playstyle.stride; + case Stats.stridemMinusPlonk: + return playstyle.stride - playstyle.plonk; + case Stats.openerMinusInfDS: + return playstyle.opener - playstyle.infds; + } + } +} diff --git a/lib/data_objects/tetrio_players_leaderboard.dart b/lib/data_objects/tetrio_players_leaderboard.dart new file mode 100644 index 0000000..1d2305d --- /dev/null +++ b/lib/data_objects/tetrio_players_leaderboard.dart @@ -0,0 +1,759 @@ +// ignore_for_file: hash_and_equals + +import 'dart:math'; +import 'package:tetra_stats/data_objects/leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; + +class TetrioPlayersLeaderboard { + late String type; + late DateTime timestamp; + late List leaderboard; + + TetrioPlayersLeaderboard(this.type, this.leaderboard); + + @override + String toString(){ + return "$type leaderboard: ${leaderboard.length} players"; + } + + List getStatRanking(List leaderboard, Stats stat, {bool reversed = false, String country = ""}){ + List lb = List.from(leaderboard); + if (country.isNotEmpty){ + lb.removeWhere((element) => element.country != country); + } + lb.sort(((a, b) { + if (a.getStatByEnum(stat).isNaN) return 1; + if (b.getStatByEnum(stat).isNaN) return -1; + if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){ + return reversed ? 1 : -1; + }else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){ + return 0; + }else{ + return reversed ? -1 : 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); + if (rank.isNotEmpty) { + filtredLeaderboard.removeWhere((element) => element.rank != rank); + } + if (filtredLeaderboard.isNotEmpty){ + double avgAPM = 0, + avgPPS = 0, + avgVS = 0, + avgTR = 0, + avgGlixare = 0, + avgGlicko = 0, + avgRD = 0, + avgAPP = 0, + avgVSAPM = 0, + avgDSS = 0, + avgDSP = 0, + avgAPPDSP = 0, + avgCheese = 0, + avgGBE = 0, + avgNyaAPP = 0, + avgArea = 0, + avgEstTR = 0, + avgEstAcc = 0, + avgOpener = 0, + avgPlonk = 0, + avgStride = 0, + avgInfDS = 0, + lowestTR = 25000, + lowestGlixare = double.infinity, + lowestGlicko = double.infinity, + lowestRD = double.infinity, + lowestWinrate = double.infinity, + lowestAPM = double.infinity, + lowestPPS = double.infinity, + lowestVS = double.infinity, + lowestAPP = double.infinity, + lowestVSAPM = double.infinity, + lowestDSS = double.infinity, + lowestDSP = double.infinity, + lowestAPPDSP = double.infinity, + lowestCheese = double.infinity, + lowestGBE = double.infinity, + lowestNyaAPP = double.infinity, + lowestArea = double.infinity, + lowestEstTR = double.infinity, + lowestEstAcc = double.infinity, + lowestOpener = double.infinity, + lowestPlonk = double.infinity, + lowestStride = double.infinity, + lowestInfDS = double.infinity, + highestTR = double.negativeInfinity, + highestGlixare = double.negativeInfinity, + highestGlicko = double.negativeInfinity, + highestRD = double.negativeInfinity, + highestWinrate = double.negativeInfinity, + highestAPM = double.negativeInfinity, + highestPPS = double.negativeInfinity, + highestVS = double.negativeInfinity, + highestAPP = double.negativeInfinity, + highestVSAPM = double.negativeInfinity, + highestDSS = double.negativeInfinity, + highestDSP = double.negativeInfinity, + highestAPPDSP = double.negativeInfinity, + highestCheese = double.negativeInfinity, + highestGBE = double.negativeInfinity, + highestNyaAPP = double.negativeInfinity, + highestArea = double.negativeInfinity, + highestEstTR = double.negativeInfinity, + highestEstAcc = double.negativeInfinity, + highestOpener = double.negativeInfinity, + highestPlonk = double.negativeInfinity, + highestStride = double.negativeInfinity, + highestInfDS = double.negativeInfinity; + int avgGamesPlayed = 0, + avgGamesWon = 0, + totalGamesPlayed = 0, + totalGamesWon = 0, + lowestGamesPlayed = pow(2, 53) as int, + lowestGamesWon = pow(2, 53) as int, + highestGamesPlayed = 0, + highestGamesWon = 0; + String lowestTRid = "", lowestTRnick = "", + lowestGlixareID = "", lowestGlixareNick = "", + lowestGlickoID = "", lowestGlickoNick = "", + lowestRdID = "", lowestRdNick = "", + lowestGamesPlayedID = "", lowestGamesPlayedNick = "", + lowestGamesWonID = "", lowestGamesWonNick = "", + lowestWinrateID = "", lowestWinrateNick = "", + lowestAPMid = "", lowestAPMnick = "", + lowestPPSid = "", lowestPPSnick = "", + lowestVSid = "", lowestVSnick = "", + lowestAPPid = "", lowestAPPnick = "", + lowestVSAPMid = "", lowestVSAPMnick = "", + lowestDSSid = "", lowestDSSnick = "", + lowestDSPid = "", lowestDSPnick = "", + lowestAPPDSPid = "", lowestAPPDSPnick = "", + lowestCheeseID = "", lowestCheeseNick = "", + lowestGBEid = "", lowestGBEnick = "", + lowestNyaAPPid = "", lowestNyaAPPnick = "", + lowestAreaID = "", lowestAreaNick = "", + lowestEstTRid = "", lowestEstTRnick = "", + lowestEstAccID = "", lowestEstAccNick = "", + lowestOpenerID = "", lowestOpenerNick = "", + lowestPlonkID = "", lowestPlonkNick = "", + lowestStrideID = "", lowestStrideNick = "", + lowestInfDSid = "", lowestInfDSnick = "", + highestTRid = "", highestTRnick = "", + highestGlixareID = "", highestGlixareNick = "", + highestGlickoID = "", highestGlickoNick = "", + highestRdID = "", highestRdNick = "", + highestGamesPlayedID = "", highestGamesPlayedNick = "", + highestGamesWonID = "", highestGamesWonNick = "", + highestWinrateID = "", highestWinrateNick = "", + highestAPMid = "", highestAPMnick = "", + highestPPSid = "", highestPPSnick = "", + highestVSid = "", highestVSnick = "", + highestAPPid = "", highestAPPnick = "", + highestVSAPMid = "", highestVSAPMnick = "", + highestDSSid = "", highestDSSnick = "", + highestDSPid = "", highestDSPnick = "", + highestAPPDSPid = "", highestAPPDSPnick = "", + highestCheeseID = "", highestCheeseNick = "", + highestGBEid = "", highestGBEnick = "", + highestNyaAPPid = "", highestNyaAPPnick = "", + highestAreaID = "", highestAreaNick = "", + highestEstTRid = "", highestEstTRnick = "", + highestEstAccID = "", highestEstAccNick = "", + highestOpenerID = "", highestOpenerNick = "", + highestPlonkID = "", highestPlonkNick = "", + highestStrideID = "", highestStrideNick = "", + highestInfDSid = "", highestInfDSnick = ""; + for (var entry in filtredLeaderboard){ + avgAPM += entry.apm; + avgPPS += entry.pps; + avgVS += entry.vs; + avgTR += entry.tr; + avgGlixare += entry.gxe; + if (entry.glicko != null) avgGlicko += entry.glicko!; + if (entry.rd != null) avgRD += entry.rd!; + avgAPP += entry.nerdStats.app; + avgVSAPM += entry.nerdStats.vsapm; + avgDSS += entry.nerdStats.dss; + avgDSP += entry.nerdStats.dsp; + avgAPPDSP += entry.nerdStats.appdsp; + avgCheese += entry.nerdStats.cheese; + avgGBE += entry.nerdStats.gbe; + avgNyaAPP += entry.nerdStats.nyaapp; + avgArea += entry.nerdStats.area; + avgEstTR += entry.estTr.esttr; + avgEstAcc += entry.esttracc; + avgOpener += entry.playstyle.opener; + avgPlonk += entry.playstyle.plonk; + avgStride += entry.playstyle.stride; + avgInfDS += entry.playstyle.infds; + totalGamesPlayed += entry.gamesPlayed; + totalGamesWon += entry.gamesWon; + if (entry.tr < lowestTR){ + lowestTR = entry.tr; + lowestTRid = entry.userId; + lowestTRnick = entry.username; + } + if (entry.gxe < lowestGlixare){ + lowestGlixare = entry.gxe; + lowestGlixareID = entry.userId; + lowestGlixareNick = entry.username; + } + if (entry.glicko != null && entry.glicko! < lowestGlicko){ + lowestGlicko = entry.glicko!; + lowestGlickoID = entry.userId; + lowestGlickoNick = entry.username; + } + if (entry.rd != null && entry.rd! < lowestRD){ + lowestRD = entry.rd!; + lowestRdID = entry.userId; + lowestRdNick = entry.username; + } + if (entry.gamesPlayed < lowestGamesPlayed){ + lowestGamesPlayed = entry.gamesPlayed; + lowestGamesPlayedID = entry.userId; + lowestGamesPlayedNick = entry.username; + } + if (entry.gamesWon < lowestGamesWon){ + lowestGamesWon = entry.gamesWon; + lowestGamesWonID = entry.userId; + lowestGamesWonNick = entry.username; + } + if (entry.winrate < lowestWinrate){ + lowestWinrate = entry.winrate; + lowestWinrateID = entry.userId; + lowestWinrateNick = entry.username; + } + if (entry.apm < lowestAPM){ + lowestAPM = entry.apm; + lowestAPMid = entry.userId; + lowestAPMnick = entry.username; + } + if (entry.pps < lowestPPS){ + lowestPPS = entry.pps; + lowestPPSid = entry.userId; + lowestPPSnick = entry.username; + } + if (entry.vs < lowestVS){ + lowestVS = entry.vs; + lowestVSid = entry.userId; + lowestVSnick = entry.username; + } + if (entry.nerdStats.app < lowestAPP){ + lowestAPP = entry.nerdStats.app; + lowestAPPid = entry.userId; + lowestAPPnick = entry.username; + } + if (entry.nerdStats.vsapm < lowestVSAPM){ + lowestVSAPM = entry.nerdStats.vsapm; + lowestVSAPMid = entry.userId; + lowestVSAPMnick = entry.username; + } + if (entry.nerdStats.dss < lowestDSS){ + lowestDSS = entry.nerdStats.dss; + lowestDSSid = entry.userId; + lowestDSSnick = entry.username; + } + if (entry.nerdStats.dsp < lowestDSP){ + lowestDSP = entry.nerdStats.dsp; + lowestDSPid = entry.userId; + lowestDSPnick = entry.username; + } + if (entry.nerdStats.appdsp < lowestAPPDSP){ + lowestAPPDSP = entry.nerdStats.appdsp; + lowestAPPDSPid = entry.userId; + lowestAPPDSPnick = entry.username; + } + if (entry.nerdStats.cheese < lowestCheese){ + lowestCheese = entry.nerdStats.cheese; + lowestCheeseID = entry.userId; + lowestCheeseNick = entry.username; + } + if (entry.nerdStats.gbe < lowestGBE){ + lowestGBE = entry.nerdStats.gbe; + lowestGBEid = entry.userId; + lowestGBEnick = entry.username; + } + if (entry.nerdStats.nyaapp < lowestNyaAPP){ + lowestNyaAPP = entry.nerdStats.nyaapp; + lowestNyaAPPid = entry.userId; + lowestNyaAPPnick = entry.username; + } + if (entry.nerdStats.area < lowestArea){ + lowestArea = entry.nerdStats.area; + lowestAreaID = entry.userId; + lowestAreaNick = entry.username; + } + if (entry.estTr.esttr < lowestEstTR){ + lowestEstTR = entry.estTr.esttr; + lowestEstTRid = entry.userId; + lowestEstTRnick = entry.username; + } + if (entry.esttracc < lowestEstAcc){ + lowestEstAcc = entry.esttracc; + lowestEstAccID = entry.userId; + lowestEstAccNick = entry.username; + } + if (entry.playstyle.opener < lowestOpener){ + lowestOpener = entry.playstyle.opener; + lowestOpenerID = entry.userId; + lowestOpenerNick = entry.username; + } + if (entry.playstyle.plonk < lowestPlonk){ + lowestPlonk = entry.playstyle.plonk; + lowestPlonkID = entry.userId; + lowestPlonkNick = entry.username; + } + if (entry.playstyle.stride < lowestStride){ + lowestStride = entry.playstyle.stride; + lowestStrideID = entry.userId; + lowestStrideNick = entry.username; + } + if (entry.playstyle.infds < lowestInfDS){ + lowestInfDS = entry.playstyle.infds; + lowestInfDSid = entry.userId; + lowestInfDSnick = entry.username; + } + if (entry.tr > highestTR){ + highestTR = entry.tr; + highestTRid = entry.userId; + highestTRnick = entry.username; + } + if (entry.gxe > highestGlixare){ + highestGlixare = entry.gxe; + highestGlixareID = entry.userId; + highestGlixareNick = entry.username; + } + if (entry.glicko != null && entry.glicko! > highestGlicko){ + highestGlicko = entry.glicko!; + highestGlickoID = entry.userId; + highestGlickoNick = entry.username; + } + if (entry.rd != null && entry.rd! > highestRD){ + highestRD = entry.rd!; + highestRdID = entry.userId; + highestRdNick = entry.username; + } + if (entry.gamesPlayed > highestGamesPlayed){ + highestGamesPlayed = entry.gamesPlayed; + highestGamesPlayedID = entry.userId; + highestGamesPlayedNick = entry.username; + } + if (entry.gamesWon > highestGamesWon){ + highestGamesWon = entry.gamesWon; + highestGamesWonID = entry.userId; + highestGamesWonNick = entry.username; + } + if (entry.winrate > highestWinrate){ + highestWinrate = entry.winrate; + highestWinrateID = entry.userId; + highestWinrateNick = entry.username; + } + if (entry.apm > highestAPM){ + highestAPM = entry.apm; + highestAPMid = entry.userId; + highestAPMnick = entry.username; + } + if (entry.pps > highestPPS){ + highestPPS = entry.pps; + highestPPSid = entry.userId; + highestPPSnick = entry.username; + } + if (entry.vs > highestVS){ + highestVS = entry.vs; + highestVSid = entry.userId; + highestVSnick = entry.username; + } + if (entry.nerdStats.app > highestAPP){ + highestAPP = entry.nerdStats.app; + highestAPPid = entry.userId; + highestAPPnick = entry.username; + } + if (entry.nerdStats.vsapm > highestVSAPM){ + highestVSAPM = entry.nerdStats.vsapm; + highestVSAPMid = entry.userId; + highestVSAPMnick = entry.username; + } + if (entry.nerdStats.dss > highestDSS){ + highestDSS = entry.nerdStats.dss; + highestDSSid = entry.userId; + highestDSSnick = entry.username; + } + if (entry.nerdStats.dsp > highestDSP){ + highestDSP = entry.nerdStats.dsp; + highestDSPid = entry.userId; + highestDSPnick = entry.username; + } + if (entry.nerdStats.appdsp > highestAPPDSP){ + highestAPPDSP = entry.nerdStats.appdsp; + highestAPPDSPid = entry.userId; + highestAPPDSPnick = entry.username; + } + if (entry.nerdStats.cheese > highestCheese){ + highestCheese = entry.nerdStats.cheese; + highestCheeseID = entry.userId; + highestCheeseNick = entry.username; + } + if (entry.nerdStats.gbe > highestGBE){ + highestGBE = entry.nerdStats.gbe; + highestGBEid = entry.userId; + highestGBEnick = entry.username; + } + if (entry.nerdStats.nyaapp > highestNyaAPP){ + highestNyaAPP = entry.nerdStats.nyaapp; + highestNyaAPPid = entry.userId; + highestNyaAPPnick = entry.username; + } + if (entry.nerdStats.area > highestArea){ + highestArea = entry.nerdStats.area; + highestAreaID = entry.userId; + highestAreaNick = entry.username; + } + if (entry.estTr.esttr > highestEstTR){ + highestEstTR = entry.estTr.esttr; + highestEstTRid = entry.userId; + highestEstTRnick = entry.username; + } + if (entry.esttracc > highestEstAcc){ + highestEstAcc = entry.esttracc; + highestEstAccID = entry.userId; + highestEstAccNick = entry.username; + } + if (entry.playstyle.opener > highestOpener){ + highestOpener = entry.playstyle.opener; + highestOpenerID = entry.userId; + highestOpenerNick = entry.username; + } + if (entry.playstyle.plonk > highestPlonk){ + highestPlonk = entry.playstyle.plonk; + highestPlonkID = entry.userId; + highestPlonkNick = entry.username; + } + if (entry.playstyle.stride > highestStride){ + highestStride = entry.playstyle.stride; + highestStrideID = entry.userId; + highestStrideNick = entry.username; + } + if (entry.playstyle.infds > highestInfDS){ + highestInfDS = entry.playstyle.infds; + highestInfDSid = entry.userId; + highestInfDSnick = entry.username; + } + } + avgAPM /= filtredLeaderboard.length; + avgPPS /= filtredLeaderboard.length; + avgVS /= filtredLeaderboard.length; + avgTR /= filtredLeaderboard.length; + avgGlixare /= filtredLeaderboard.length; + avgGlicko /= filtredLeaderboard.length; + avgRD /= filtredLeaderboard.length; + avgAPP /= filtredLeaderboard.length; + avgVSAPM /= filtredLeaderboard.length; + avgDSS /= filtredLeaderboard.length; + avgDSP /= filtredLeaderboard.length; + avgAPPDSP /= leaderboard.length; + avgCheese /= filtredLeaderboard.length; + avgGBE /= filtredLeaderboard.length; + avgNyaAPP /= filtredLeaderboard.length; + avgArea /= filtredLeaderboard.length; + avgEstTR /= filtredLeaderboard.length; + avgEstAcc /= filtredLeaderboard.length; + avgOpener /= filtredLeaderboard.length; + avgPlonk /= filtredLeaderboard.length; + avgStride /= filtredLeaderboard.length; + avgInfDS /= filtredLeaderboard.length; + avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor(); + avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor(); + return [TetraLeague(id: "", timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, gxe: avgGlixare, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason), + { + "everyone": rank == "", + "totalGamesPlayed": totalGamesPlayed, + "totalGamesWon": totalGamesWon, + "players": filtredLeaderboard.length, + "lowestTR": lowestTR, + "lowestTRid": lowestTRid, + "lowestTRnick": lowestTRnick, + "lowestGlixare": lowestGlixare, + "lowestGlixareID": lowestGlixareID, + "lowestGlixareNick": lowestGlixareNick, + "lowestS1tr": lowestGlixare * 250, + "lowestS1trID": lowestGlixareID, + "lowestS1trNick": lowestGlixareNick, + "lowestGlicko": lowestGlicko, + "lowestGlickoID": lowestGlickoID, + "lowestGlickoNick": lowestGlickoNick, + "lowestRD": lowestRD, + "lowestRdID": lowestRdID, + "lowestRdNick": lowestRdNick, + "lowestGamesPlayed": lowestGamesPlayed, + "lowestGamesPlayedID": lowestGamesPlayedID, + "lowestGamesPlayedNick": lowestGamesPlayedNick, + "lowestGamesWon": lowestGamesWon, + "lowestGamesWonID": lowestGamesWonID, + "lowestGamesWonNick": lowestGamesWonNick, + "lowestWinrate": lowestWinrate, + "lowestWinrateID": lowestWinrateID, + "lowestWinrateNick": lowestWinrateNick, + "lowestAPM": lowestAPM, + "lowestAPMid": lowestAPMid, + "lowestAPMnick": lowestAPMnick, + "lowestPPS": lowestPPS, + "lowestPPSid": lowestPPSid, + "lowestPPSnick": lowestPPSnick, + "lowestVS": lowestVS, + "lowestVSid": lowestVSid, + "lowestVSnick": lowestVSnick, + "lowestAPP": lowestAPP, + "lowestAPPid": lowestAPPid, + "lowestAPPnick": lowestAPPnick, + "lowestVSAPM": lowestVSAPM, + "lowestVSAPMid": lowestVSAPMid, + "lowestVSAPMnick": lowestVSAPMnick, + "lowestDSS": lowestDSS, + "lowestDSSid": lowestDSSid, + "lowestDSSnick": lowestDSSnick, + "lowestDSP": lowestDSP, + "lowestDSPid": lowestDSPid, + "lowestDSPnick": lowestDSPnick, + "lowestAPPDSP": lowestAPPDSP, + "lowestAPPDSPid": lowestAPPDSPid, + "lowestAPPDSPnick": lowestAPPDSPnick, + "lowestCheese": lowestCheese, + "lowestCheeseID": lowestCheeseID, + "lowestCheeseNick": lowestCheeseNick, + "lowestGBE": lowestGBE, + "lowestGBEid": lowestGBEid, + "lowestGBEnick": lowestGBEnick, + "lowestNyaAPP": lowestNyaAPP, + "lowestNyaAPPid": lowestNyaAPPid, + "lowestNyaAPPnick": lowestNyaAPPnick, + "lowestArea": lowestArea, + "lowestAreaID": lowestAreaID, + "lowestAreaNick": lowestAreaNick, + "lowestEstTR": lowestEstTR, + "lowestEstTRid": lowestEstTRid, + "lowestEstTRnick": lowestEstTRnick, + "lowestEstAcc": lowestEstAcc, + "lowestEstAccID": lowestEstAccID, + "lowestEstAccNick": lowestEstAccNick, + "lowestOpener": lowestOpener, + "lowestOpenerID": lowestOpenerID, + "lowestOpenerNick": lowestOpenerNick, + "lowestPlonk": lowestPlonk, + "lowestPlonkID": lowestPlonkID, + "lowestPlonkNick": lowestPlonkNick, + "lowestStride": lowestStride, + "lowestStrideID": lowestStrideID, + "lowestStrideNick": lowestStrideNick, + "lowestInfDS": lowestInfDS, + "lowestInfDSid": lowestInfDSid, + "lowestInfDSnick": lowestInfDSnick, + "highestTR": highestTR, + "highestTRid": highestTRid, + "highestTRnick": highestTRnick, + "highestGlixare": highestGlixare, + "highestGlixareID": highestGlixareID, + "highestGlixareNick": highestGlixareNick, + "highestS1tr": highestGlixare * 250, + "highestS1trID": highestGlixareID, + "highestS1trNick": highestGlixareNick, + "highestGlicko": highestGlicko, + "highestGlickoID": highestGlickoID, + "highestGlickoNick": highestGlickoNick, + "highestRD": highestRD, + "highestRdID": highestRdID, + "highestRdNick": highestRdNick, + "highestGamesPlayed": highestGamesPlayed, + "highestGamesPlayedID": highestGamesPlayedID, + "highestGamesPlayedNick": highestGamesPlayedNick, + "highestGamesWon": highestGamesWon, + "highestGamesWonID": highestGamesWonID, + "highestGamesWonNick": highestGamesWonNick, + "highestWinrate": highestWinrate, + "highestWinrateID": highestWinrateID, + "highestWinrateNick": highestWinrateNick, + "highestAPM": highestAPM, + "highestAPMid": highestAPMid, + "highestAPMnick": highestAPMnick, + "highestPPS": highestPPS, + "highestPPSid": highestPPSid, + "highestPPSnick": highestPPSnick, + "highestVS": highestVS, + "highestVSid": highestVSid, + "highestVSnick": highestVSnick, + "highestAPP": highestAPP, + "highestAPPid": highestAPPid, + "highestAPPnick": highestAPPnick, + "highestVSAPM": highestVSAPM, + "highestVSAPMid": highestVSAPMid, + "highestVSAPMnick": highestVSAPMnick, + "highestDSS": highestDSS, + "highestDSSid": highestDSSid, + "highestDSSnick": highestDSSnick, + "highestDSP": highestDSP, + "highestDSPid": highestDSPid, + "highestDSPnick": highestDSPnick, + "highestAPPDSP": highestAPPDSP, + "highestAPPDSPid": highestAPPDSPid, + "highestAPPDSPnick": highestAPPDSPnick, + "highestCheese": highestCheese, + "highestCheeseID": highestCheeseID, + "highestCheeseNick": highestCheeseNick, + "highestGBE": highestGBE, + "highestGBEid": highestGBEid, + "highestGBEnick": highestGBEnick, + "highestNyaAPP": highestNyaAPP, + "highestNyaAPPid": highestNyaAPPid, + "highestNyaAPPnick": highestNyaAPPnick, + "highestArea": highestArea, + "highestAreaID": highestAreaID, + "highestAreaNick": highestAreaNick, + "highestEstTR": highestEstTR, + "highestEstTRid": highestEstTRid, + "highestEstTRnick": highestEstTRnick, + "highestEstAcc": highestEstAcc, + "highestEstAccID": highestEstAccID, + "highestEstAccNick": highestEstAccNick, + "highestOpener": highestOpener, + "highestOpenerID": highestOpenerID, + "highestOpenerNick": highestOpenerNick, + "highestPlonk": highestPlonk, + "highestPlonkID": highestPlonkID, + "highestPlonkNick": highestPlonkNick, + "highestStride": highestStride, + "highestStrideID": highestStrideID, + "highestStrideNick": highestStrideNick, + "highestInfDS": highestInfDS, + "highestInfDSid": highestInfDSid, + "highestInfDSnick": highestInfDSnick, + "avgAPP": avgAPP, + "avgVSAPM": avgVSAPM, + "avgDSS": avgDSS, + "avgDSP": avgDSP, + "avgAPPDSP": avgAPPDSP, + "avgCheese": avgCheese, + "avgGBE": avgGBE, + "avgNyaAPP": avgNyaAPP, + "avgArea": avgArea, + "avgEstTR": avgEstTR, + "avgEstAcc": avgEstAcc, + "avgOpener": avgOpener, + "avgPlonk": avgPlonk, + "avgStride": avgStride, + "avgInfDS": avgInfDS, + "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].tr : lowestTR, + "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0, + "entries": filtredLeaderboard + }]; + }else{ + return [TetraLeague(id: "", timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, tr: 0, rank: rank, percentileRank: rank, gxe: -1, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason), + {"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0, "toEnterGlicko": 0}]; + } + } + + PlayerLeaderboardPosition? getLeaderboardPosition(Mapleague) { + if (league.values.first.gamesPlayed == 0) return null; + bool fakePositions = false; + late List copyOfLeaderboard; + if (leaderboard.indexWhere((element) => element.userId == league.keys.first) == -1){ + fakePositions =true; + copyOfLeaderboard = List.of(leaderboard); + copyOfLeaderboard.add(league.values.first.convertToPlayerFromLeaderboard(league.keys.first)); + } + 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(fakePositions ? copyOfLeaderboard : leaderboard, stat, reversed: stat == Stats.cheese ? true : false); + int position = sortedLeaderboard.indexWhere((element) => element.userId == league.keys.first) + 1; + if (position == 0) { + results.add(null); + } else { + results.add(LeaderboardPosition(fakePositions ? 1001 : position, position / sortedLeaderboard.length)); + } + } + return PlayerLeaderboardPosition.fromSearchResults(results); + } + + Map> get averages => { + 'x+': getAverageOfRank("x+"), + 'x': getAverageOfRank("x"), + 'u': getAverageOfRank("u"), + 'ss': getAverageOfRank("ss"), + 's+': getAverageOfRank("s+"), + 's': getAverageOfRank("s"), + 's-': getAverageOfRank("s-"), + 'a+': getAverageOfRank("a+"), + 'a': getAverageOfRank("a"), + 'a-': getAverageOfRank("a-"), + 'b+': getAverageOfRank("b+"), + 'b': getAverageOfRank("b"), + 'b-': getAverageOfRank("b-"), + 'c+': getAverageOfRank("c+"), + 'c': getAverageOfRank("c"), + 'c-': getAverageOfRank("c-"), + 'd+': getAverageOfRank("d+"), + 'd': getAverageOfRank("d"), + '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"] + }; + + Map get cutoffsGlicko => { + 'x': getAverageOfRank("x")[1]["toEnterGlicko"], + 'u': getAverageOfRank("u")[1]["toEnterGlicko"], + 'ss': getAverageOfRank("ss")[1]["toEnterGlicko"], + 's+': getAverageOfRank("s+")[1]["toEnterGlicko"], + 's': getAverageOfRank("s")[1]["toEnterGlicko"], + 's-': getAverageOfRank("s-")[1]["toEnterGlicko"], + 'a+': getAverageOfRank("a+")[1]["toEnterGlicko"], + 'a': getAverageOfRank("a")[1]["toEnterGlicko"], + 'a-': getAverageOfRank("a-")[1]["toEnterGlicko"], + 'b+': getAverageOfRank("b+")[1]["toEnterGlicko"], + 'b': getAverageOfRank("b")[1]["toEnterGlicko"], + 'b-': getAverageOfRank("b-")[1]["toEnterGlicko"], + 'c+': getAverageOfRank("c+")[1]["toEnterGlicko"], + 'c': getAverageOfRank("c")[1]["toEnterGlicko"], + 'c-': getAverageOfRank("c-")[1]["toEnterGlicko"], + 'd+': getAverageOfRank("d+")[1]["toEnterGlicko"], + 'd': getAverageOfRank("d")[1]["toEnterGlicko"] + }; + + TetrioPlayersLeaderboard.fromJson(List json, String t, DateTime ts) { + type = t; + timestamp = ts; + leaderboard = []; + for (Map entry in json) { + leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts)); + } + } + + addPlayers(List list){ + leaderboard.addAll(list); + } +} diff --git a/lib/data_objects/tetrio_zen.dart b/lib/data_objects/tetrio_zen.dart new file mode 100644 index 0000000..97e085c --- /dev/null +++ b/lib/data_objects/tetrio_zen.dart @@ -0,0 +1,24 @@ +// ignore_for_file: hash_and_equals + +import 'dart:math'; + +class TetrioZen { + late int level; + late int score; + + TetrioZen({required this.level, required this.score}); + + double get scoreRequirement => (10000 + 10000 * ((log(level + 1) / log(2)) - 1)); + + TetrioZen.fromJson(Map json) { + level = json['level']; + score = json['score']; + } + + Map toJson() { + final Map data = {}; + data['level'] = level; + data['score'] = score; + return data; + } +} diff --git a/lib/data_objects/user_records.dart b/lib/data_objects/user_records.dart new file mode 100644 index 0000000..cac93d4 --- /dev/null +++ b/lib/data_objects/user_records.dart @@ -0,0 +1,13 @@ +// ignore_for_file: hash_and_equals + +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; + +class UserRecords{ + String id; + RecordSingle? sprint; + RecordSingle? blitz; + TetrioZen zen; + + UserRecords(this.id, this.sprint, this.blitz, this.zen); +} diff --git a/lib/data_objects/zenith_results.dart b/lib/data_objects/zenith_results.dart new file mode 100644 index 0000000..d3efdb1 --- /dev/null +++ b/lib/data_objects/zenith_results.dart @@ -0,0 +1,36 @@ +// ignore_for_file: hash_and_equals + +class ZenithResults{ + late double altitude; + late double rank; + late double peakrank; + late double avgrankpts; + late int floor; + late double targetingfactor; + late double targetinggrace; + late double totalbonus; + late int revives; + late int revivesTotal; + late bool speedrun; + late bool speedrunSeen; + late List splits; + + ZenithResults.fromJson(Map json){ + altitude = json['altitude'].toDouble(); + rank = json['rank'].toDouble(); + peakrank = json['peakrank'].toDouble(); + avgrankpts = json['avgrankpts'].toDouble(); + floor = json['floor']; + targetingfactor = json['targetingfactor'].toDouble(); + targetinggrace = json['targetinggrace'].toDouble(); + totalbonus = json['totalbonus'].toDouble(); + revives = json['revives']; + revivesTotal = json['revivesTotal']; + speedrun = json['speedrun']; + speedrunSeen = json['speedrun_seen']; + splits = []; + for (int ms in json['splits']) { + splits.add(Duration(milliseconds: ms)); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index bbba219..a847a1e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 102dc89..2217ad6 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -5,17 +5,32 @@ import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; import 'package:path_provider/path_provider.dart'; -import 'package:sqflite/sql.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/data_objects/end_context_multi.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_stream.dart'; +import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; +import 'package:tetra_stats/data_objects/user_records.dart'; import 'package:tetra_stats/main.dart' show packageInfo; import 'package:flutter/foundation.dart'; import 'package:tetra_stats/services/custom_http_client.dart'; import 'package:http/http.dart' as http; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:csv/csv.dart'; const String dbName = "TetraStats.db"; diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart index 45c7749..603e4d6 100644 --- a/lib/views/calc_view.dart +++ b/lib/views/calc_view.dart @@ -2,7 +2,9 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/widgets/graphs.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 333df80..208f9b4 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -5,7 +5,10 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/utils/relative_timestamps.dart'; diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 9e52818..9a593a6 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -11,8 +11,25 @@ import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/data_objects/distinguishment.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/news_entry.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/record_extras.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_stream.dart'; +import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show prefs, teto; import 'package:tetra_stats/services/crud_exceptions.dart'; @@ -338,7 +355,7 @@ class _MainState extends State with TickerProviderStateMixin { appBar: AppBar( 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 + actions: widget.player == null ? [ // search bar and PopupMenuButton hidden if player provided _showSearchBar ? IconButton( onPressed: () { @@ -1555,7 +1572,7 @@ class _OtherThingy extends StatelessWidget { 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)), + MarkdownBody(data: bio!, styleSheet: MarkdownStyleSheet(textScaler: TextScaler.linear(1.5), textAlign: WrapAlignment.center)) // Text(bio!, style: const TextStyle(fontSize: 18)), ], ), ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index c749d5b..37aa103 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -6,7 +6,23 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; +import 'package:tetra_stats/data_objects/badge.dart'; +import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/data_objects/distinguishment.dart'; +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/news_entry.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; +import 'package:tetra_stats/data_objects/record_extras.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; @@ -20,9 +36,7 @@ import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/widgets/tl_progress_bar.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; @@ -594,6 +608,35 @@ class RecordSummary extends StatelessWidget{ } +class LeagueCard extends StatelessWidget{ + final TetraLeague league; + final bool showSeasonNumber; + + const LeagueCard({super.key, required this.league, this.showSeasonNumber = false}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(showSeasonNumber ? "Season ${league.season}" : "Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + TLRatingThingy(userID: "", tlData: league, showPositions: true), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Text("${league.apm != null ? f2.format(league.apm) : "-.--"} APM • ${league.pps != null ? f2.format(league.pps) : "-.--"} PPS • ${league.vs != null ? f2.format(league.vs) : "-.--"} VS • ${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP • ${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ); + } + +} + class _DestinationHomeState extends State { Cards rightCard = Cards.overview; CardMod cardMod = CardMod.info; @@ -645,23 +688,7 @@ class _DestinationHomeState extends State { ), ), ), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - TLRatingThingy(userID: "", tlData: summaries.league, showPositions: true), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("${summaries.league.apm != null ? f2.format(summaries.league.apm) : "-.--"} APM • ${summaries.league.pps != null ? f2.format(summaries.league.pps) : "-.--"} PPS • ${summaries.league.vs != null ? f2.format(summaries.league.vs) : "-.--"} VS • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.app) : "-.--"} APP • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), + LeagueCard(league: summaries.league), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -840,6 +867,7 @@ class _DestinationHomeState extends State { return Column( children: [ Card( + //surfaceTintColor: rankColors[data.rank], child: Padding( padding: const EdgeInsets.only(bottom: 4.0), child: Center( @@ -856,6 +884,7 @@ class _DestinationHomeState extends State { ), TetraLeagueThingy(league: data, cutoffs: cutoffs), if (data.nerdStats != null) Card( + //surfaceTintColor: rankColors[data.rank], child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -871,6 +900,31 @@ class _DestinationHomeState extends State { ); } + Widget getPreviousSeasonsList(Map pastLeague){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Previous Seasons", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + for (var key in pastLeague.keys) Card( + child: LeagueCard(league: pastLeague[key]!, showSeasonNumber: true), + ) + ], + ); + } + Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ return Column( children: [ @@ -1373,6 +1427,10 @@ class _DestinationHomeState extends State { value: CardMod.info, label: Text('Standing'), ), + const ButtonSegment( + value: CardMod.ex, // yeah i misusing my own Enum shut the fuck up + label: Text('Previous Seasons'), + ), const ButtonSegment( value: CardMod.records, label: Text('Recent Matches'), @@ -1436,10 +1494,10 @@ class _DestinationHomeState extends State { Column( mainAxisSize: MainAxisSize.min, children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), ), ], ) @@ -1524,6 +1582,7 @@ class _DestinationHomeState extends State { Cards.overview => getOverviewCard(snapshot.data!.summaries!), Cards.tetraLeague => switch (cardMod){ CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs), + CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), CardMod.records => getRecentTLrecords(widget.constraints), _ => const Center(child: Text("huh?")) }, @@ -1532,7 +1591,6 @@ class _DestinationHomeState extends State { CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), - _ => const Center(child: Text("huh?")) }, Cards.sprint => switch (cardMod){ CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), @@ -2356,6 +2414,7 @@ class TetraLeagueThingy extends StatelessWidget{ @override Widget build(BuildContext context) { return Card( + //surfaceTintColor: rankColors[league.rank], child: Column( children: [ TLRatingThingy(userID: "w", tlData: league), diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 14c6ad0..d9d4a42 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -3,7 +3,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/views/main_view.dart' show MainView; import 'package:window_manager/window_manager.dart'; diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index c2f6387..cd10535 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -1,7 +1,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 1eb96c5..6734b14 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:go_router/go_router.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; import 'package:tetra_stats/main.dart' show packageInfo, teto, prefs; import 'package:file_selector/file_selector.dart'; import 'package:file_picker/file_picker.dart'; diff --git a/lib/views/singleplayer_record_view.dart b/lib/views/singleplayer_record_view.dart index 2126c2f..0fe3bf0 100644 --- a/lib/views/singleplayer_record_view.dart +++ b/lib/views/singleplayer_record_view.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/widgets/singleplayer_record.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; diff --git a/lib/views/sprint_and_blitz_averages.dart b/lib/views/sprint_and_blitz_averages.dart index a37fbd9..9ac06ed 100644 --- a/lib/views/sprint_and_blitz_averages.dart +++ b/lib/views/sprint_and_blitz_averages.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart index b97f141..2e70d2f 100644 --- a/lib/views/state_view.dart +++ b/lib/views/state_view.dart @@ -2,11 +2,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; -import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:window_manager/window_manager.dart'; final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); @@ -41,13 +40,9 @@ class StateState extends State { super.dispose(); } - void _justUpdate() { - setState(() {}); - } - @override Widget build(BuildContext context) { - final t = Translations.of(context); + //final t = Translations.of(context); return Scaffold( appBar: AppBar( title: Text("State from ${timestamp(widget.state.timestamp)}"), diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart index bf0fc5f..021adf9 100644 --- a/lib/views/states_view.dart +++ b/lib/views/states_view.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/utils/numers_formats.dart'; diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index d7699ad..47945e9 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/views/main_view.dart'; diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index fc4b50d..6b43db3 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -1,6 +1,9 @@ // ignore_for_file: use_build_context_synchronously, type_literal_in_constant_pattern import 'dart:io'; +import 'package:tetra_stats/data_objects/beta_league_round.dart'; +import 'package:tetra_stats/data_objects/beta_league_stats.dart'; +import 'package:tetra_stats/data_objects/beta_record.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/views/compare_view.dart' show CompareThingy; @@ -10,7 +13,6 @@ import 'package:tetra_stats/widgets/vs_graphs.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/open_in_browser.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart index 721bce4..b7816e5 100644 --- a/lib/views/tracked_players_view.dart +++ b/lib/views/tracked_players_view.dart @@ -2,12 +2,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/utils/filesizes_converter.dart'; import 'package:tetra_stats/views/states_view.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:window_manager/window_manager.dart'; late String oldWindowTitle; diff --git a/lib/views/zenith_record_view.dart b/lib/views/zenith_record_view.dart index b9f5c29..7169bf7 100644 --- a/lib/views/zenith_record_view.dart +++ b/lib/views/zenith_record_view.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/zenith_thingy.dart'; diff --git a/lib/widgets/finesse_thingy.dart b/lib/widgets/finesse_thingy.dart index 937d767..c6600c2 100644 --- a/lib/widgets/finesse_thingy.dart +++ b/lib/widgets/finesse_thingy.dart @@ -1,7 +1,7 @@ // ignore_for_file: curly_braces_in_flow_control_structures import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/finesse.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart index 0f3bd6f..edfad86 100644 --- a/lib/widgets/gauget_num.dart +++ b/lib/widgets/gauget_num.dart @@ -2,7 +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/data_objects/leaderboard_position.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'; diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index 194496d..2e42f32 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -8,9 +8,11 @@ import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; import 'package:fl_chart/src/utils/canvas_wrapper.dart'; import 'package:fl_chart/src/utils/utils.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/main.dart' show prefs; import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; diff --git a/lib/widgets/lineclears_thingy.dart b/lib/widgets/lineclears_thingy.dart index 78745db..cb739b8 100644 --- a/lib/widgets/lineclears_thingy.dart +++ b/lib/widgets/lineclears_thingy.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/clears.dart'; import 'package:tetra_stats/gen/strings.g.dart'; class LineclearsThingy extends StatelessWidget{ diff --git a/lib/widgets/recent_sp_games.dart b/lib/widgets/recent_sp_games.dart index e3e1b7a..92a2ff9 100644 --- a/lib/widgets/recent_sp_games.dart +++ b/lib/widgets/recent_sp_games.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index f717ca2..62bec0d 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.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/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.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'; diff --git a/lib/widgets/sp_trailing_stats.dart b/lib/widgets/sp_trailing_stats.dart index fc679c6..b59f750 100644 --- a/lib/widgets/sp_trailing_stats.dart +++ b/lib/widgets/sp_trailing_stats.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index f837bb4..c9c0165 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/leaderboard_position.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'; diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index bc8f94f..ed23430 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/glicko.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index ac7d4c3..1a3fbd0 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show prefs; import 'package:tetra_stats/utils/numers_formats.dart'; diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 853adde..be40b7c 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -1,14 +1,11 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/tetra_league.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/utils/relative_timestamps.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'; @@ -80,8 +77,6 @@ class _TLThingyState extends State with TickerProviderStateMixin { return Column( children: [ if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - //if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft))) - //else Text(t.seasonEnded), if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)), textAlign: TextAlign.center,), if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), @@ -95,7 +90,7 @@ class _TLThingyState extends State with TickerProviderStateMixin { if (values.start.round() == 0){ currentTl = widget.tl; }else{ - currentTl = sortedStates[values.start.round()-1]!; + currentTl = sortedStates[values.start.round()-1]; } if (values.end.round() == 0){ oldTl = widget.tl; @@ -207,7 +202,6 @@ class _TLThingyState extends State with TickerProviderStateMixin { 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}"),], diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index f27e7a3..ee6dc92 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/views/compare_view.dart'; diff --git a/lib/widgets/vs_graphs.dart b/lib/widgets/vs_graphs.dart index 0b78adc..9dce22d 100644 --- a/lib/widgets/vs_graphs.dart +++ b/lib/widgets/vs_graphs.dart @@ -1,7 +1,9 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/widgets/graphs.dart' show MyRadarChart; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; class VsGraphs extends StatelessWidget{ diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index a5a43f3..d1954d0 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -1,6 +1,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/data_objects/record_extras.dart'; +import 'package:tetra_stats/data_objects/record_single.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'; From 858bb678019be3afe61dec9facbbfffd18a6bd16 Mon Sep 17 00:00:00 2001 From: Takathedinosaur Date: Mon, 9 Sep 2024 00:07:59 +0200 Subject: [PATCH 02/86] Fix discord ID search --- lib/services/tetrio_crud.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 2217ad6..7a468c6 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -1154,7 +1154,7 @@ class TetrioService extends DB { if (kIsWeb) { dUrl = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()}); } else { - dUrl = Uri.https('ch.tetr.io', 'api/users/search/${user.toLowerCase().trim()}'); + dUrl = Uri.https('ch.tetr.io', 'api/users/search/discord:${user.toLowerCase().trim()}'); } try{ final response = await client.get(dUrl); From 2f9cf0ee2041d92c032b622dff58c317d1163113 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 9 Sep 2024 01:10:51 +0300 Subject: [PATCH 03/86] small work --- lib/data_objects/summaries.dart | 49 +++++-- lib/data_objects/tetra_league.dart | 80 +++++++--- lib/data_objects/tetrio_constants.dart | 154 +++++++++++-------- lib/utils/colors_functions.dart | 4 + lib/utils/numers_formats.dart | 1 + lib/views/main_view_tiles.dart | 195 +++++++++++++++---------- lib/widgets/graphs.dart | 52 +++---- 7 files changed, 335 insertions(+), 200 deletions(-) diff --git a/lib/data_objects/summaries.dart b/lib/data_objects/summaries.dart index 08f5db6..31540b4 100644 --- a/lib/data_objects/summaries.dart +++ b/lib/data_objects/summaries.dart @@ -6,7 +6,7 @@ import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_zen.dart'; -class Summaries{ +class Summaries { late String id; RecordSingle? sprint; RecordSingle? blitz; @@ -21,19 +21,42 @@ class Summaries{ Summaries(this.id, this.league, this.zen); - Summaries.fromJson(Map json, String i){ + Summaries.fromJson(Map json, String i) { id = i; - if (json['40l']['record'] != null) sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank'], json['40l']['rank_local']); - if (json['blitz']['record'] != null) blitz = RecordSingle.fromJson(json['blitz']['record'], json['blitz']['rank'], json['40l']['rank_local']); - if (json['zenith']['record'] != null) zenith = RecordSingle.fromJson(json['zenith']['record'], json['zenith']['rank'], json['zenith']['rank_local']); - if (json['zenith']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenith']['best']['record'], json['zenith']['best']['rank'], -1); - if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); - if (json['zenithex']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenithex']['best']['record'], json['zenith']['best']['rank'], -1); - achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; - league = TetraLeague.fromJson(json['league'], DateTime.now(), currentSeason, i); - if (json['league']['past'].isNotEmpty) for (var key in json['league']['past'].keys){ - pastLeague[int.parse(key)] = TetraLeague.fromJson(json['league']['past'][key], DateTime(1970), int.parse(json['league']['past'][key]['season']), i); - } + if (json['40l']['record'] != null) + sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank'], + json['40l']['rank_local']); + if (json['blitz']['record'] != null) + blitz = RecordSingle.fromJson(json['blitz']['record'], + json['blitz']['rank'], json['40l']['rank_local']); + if (json['zenith']['record'] != null) + zenith = RecordSingle.fromJson(json['zenith']['record'], + json['zenith']['rank'], json['zenith']['rank_local']); + if (json['zenith']['best']['record'] != null) + zenithCareerBest = RecordSingle.fromJson( + json['zenith']['best']['record'], json['zenith']['best']['rank'], -1); + if (json['zenithex']['record'] != null) + zenithEx = RecordSingle.fromJson(json['zenithex']['record'], + json['zenithex']['rank'], json['zenithex']['rank_local']); + if (json['zenithex']['best']['record'] != null) + zenithCareerBest = RecordSingle.fromJson( + json['zenithex']['best']['record'], + json['zenith']['best']['rank'], + -1); + achievements = [ + for (var achievement in json['achievements']) + Achievement.fromJson(achievement) + ]; + league = + TetraLeague.fromJson(json['league'], DateTime.now(), currentSeason, i); + if (json['league']['past'].isNotEmpty) + for (var key in json['league']['past'].keys) { + pastLeague[int.parse(key)] = TetraLeague.fromJson( + json['league']['past'][key], + null, + int.parse(json['league']['past'][key]['season']), + i); + } zen = TetrioZen.fromJson(json['zen']); } } diff --git a/lib/data_objects/tetra_league.dart b/lib/data_objects/tetra_league.dart index 0419fd8..c509d07 100644 --- a/lib/data_objects/tetra_league.dart +++ b/lib/data_objects/tetra_league.dart @@ -57,22 +57,35 @@ class TetraLeague { this.apm, this.pps, this.vs, - required this.season}){ - nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; - estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; - playstyle =(nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; - } + required this.season}) { + nerdStats = (apm != null && pps != null && vs != null) + ? NerdStats(apm!, pps!, vs!) + : null; + estTr = (nerdStats != null) + ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, + nerdStats!.gbe) + : null; + playstyle = (nerdStats != null) + ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, + nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) + : null; + } double get winrate => gamesWon / gamesPlayed; double get s1tr => gxe * 250; - TetraLeague.fromJson(Map json, ts, int s, String i) { - timestamp = ts; + TetraLeague.fromJson( + Map json, DateTime? ts, int s, String i) { + timestamp = ts != null ? ts : seasonEnds[s - 1]; season = s; id = i; gamesPlayed = json['gamesplayed'] ?? 0; gamesWon = json['gameswon'] ?? 0; - tr = json['tr'] != null ? json['tr'].toDouble() : json['rating'] != null ? json['rating'].toDouble() : -1; + tr = json['tr'] != null + ? json['tr'].toDouble() + : json['rating'] != null + ? json['rating'].toDouble() + : -1; glicko = json['glicko']?.toDouble(); rd = json['rd'] != null ? json['rd']!.toDouble() : noTrRd; gxe = json['gxe'] != null ? json['gxe'].toDouble() : -1; @@ -81,38 +94,67 @@ class TetraLeague { apm = json['apm']?.toDouble(); pps = json['pps']?.toDouble(); vs = json['vs']?.toDouble(); - decaying = switch(json['decaying'].runtimeType){ + decaying = switch (json['decaying'].runtimeType) { int => json['decaying'] == 1, bool => json['decaying'], _ => false }; standing = json['standing'] ?? json['placement'] ?? -1; - percentile = json['percentile'] != null ? json['percentile'].toDouble() : rankCutoffs[rank]; + percentile = json['percentile'] != null + ? json['percentile'].toDouble() + : rankCutoffs[rank]; standingLocal = json['standing_local'] ?? -1; prevRank = json['prev_rank']; prevAt = json['prev_at'] ?? -1; nextRank = json['next_rank']; nextAt = json['next_at'] ?? -1; percentileRank = json['percentile_rank'] ?? rank; - nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; - estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; - playstyle = (nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; + nerdStats = (apm != null && pps != null && vs != null) + ? NerdStats(apm!, pps!, vs!) + : null; + estTr = (nerdStats != null) + ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, + nerdStats!.gbe) + : null; + playstyle = (nerdStats != null) + ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, + nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) + : null; } @override - bool operator ==(covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && rd == other.rd; + bool operator ==(covariant TetraLeague other) => + gamesPlayed == other.gamesPlayed && rd == other.rd; - bool lessStrictCheck (covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && glicko == other.glicko; + bool lessStrictCheck(covariant TetraLeague other) => + gamesPlayed == other.gamesPlayed && glicko == other.glicko; double? get esttracc => (estTr != null) ? estTr!.esttr - tr : null; - TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) => TetrioPlayerFromLeaderboard( - id, "", "user", -1, null, timestamp, gamesPlayed, gamesWon, - tr, gxe, glicko??0, rd??noTrRd, rank, bestRank, apm??0, pps??0, vs??0, decaying); + TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) => + TetrioPlayerFromLeaderboard( + id, + "", + "user", + -1, + null, + timestamp, + gamesPlayed, + gamesWon, + tr, + gxe, + glicko ?? 0, + rd ?? noTrRd, + rank, + bestRank, + apm ?? 0, + pps ?? 0, + vs ?? 0, + decaying); Map toJson() { final Map data = {}; - data['id'] = id+timestamp.millisecondsSinceEpoch.toRadixString(16); + data['id'] = id + timestamp.millisecondsSinceEpoch.toRadixString(16); if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed; if (gamesWon > 0) data['gameswon'] = gamesWon; if (tr >= 0) data['tr'] = tr; diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index b397d38..07bbcc3 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -13,7 +13,24 @@ const double vsapmWeight = 60; const double cheeseWeight = 1.25; const double gbeWeight = 315; const List ranks = [ - "d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x", "x+" + "d", + "d+", + "c-", + "c", + "c+", + "b-", + "b", + "b+", + "a-", + "a", + "a+", + "s-", + "s", + "s+", + "ss", + "u", + "x", + "x+" ]; const Map rankCutoffs = { "x+": 0.002, @@ -57,6 +74,7 @@ const Map rankTargets = { "d+": 800.00, "d": 0.00, }; + // DateTime seasonStart = DateTime.utc(2024, 08, 16, 18); //DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15); enum Stats { @@ -89,7 +107,7 @@ enum Stats { stride, stridemMinusPlonk, openerMinusInfDS - } +} const Map chartsShortTitles = { Stats.tr: "TR", @@ -120,69 +138,81 @@ const Map chartsShortTitles = { Stats.infDS: "Inf. DS", Stats.stride: "Stride", Stats.stridemMinusPlonk: "Stride - Plonk", - Stats.openerMinusInfDS: "Opener - Inf. DS" - }; - -const Map rankColors = { // thanks osk for const rankColors at https://ch.tetr.io/res/js/base.js:458 - 'x+': Color(0xFF643C8D), - 'x': Color(0xFFFF45FF), - 'u': Color(0xFFFF3813), - 'ss': Color(0xFFDB8B1F), - 's+': Color(0xFFD8AF0E), - 's': Color(0xFFE0A71B), - 's-': Color(0xFFB2972B), - 'a+': Color(0xFF1FA834), - 'a': Color(0xFF46AD51), - 'a-': Color(0xFF3BB687), - 'b+': Color(0xFF4F99C0), - 'b': Color(0xFF4F64C9), - 'b-': Color(0xFF5650C7), - 'c+': Color(0xFF552883), - 'c': Color(0xFF733E8F), - 'c-': Color(0xFF79558C), - 'd+': Color(0xFF8E6091), - 'd': Color(0xFF907591), - 'z': Color(0xFF375433) + Stats.openerMinusInfDS: "Opener - Inf. DS" }; -const Map sprintAverages = { // based on https://discord.com/channels/673303546107658242/674421736162197515/1244287342965952562 - 'x': Duration(seconds: 25, milliseconds: 144), - 'u': Duration(seconds: 36, milliseconds: 115), - 'ss': Duration(seconds: 46, milliseconds: 396), - 's+': Duration(seconds: 55, milliseconds: 056), - 's': Duration(seconds: 61, milliseconds: 892), - 's-': Duration(seconds: 68, milliseconds: 918), - 'a+': Duration(seconds: 76, milliseconds: 187), - 'a': Duration(seconds: 83, milliseconds: 529), - 'a-': Duration(seconds: 88, milliseconds: 608), - 'b+': Duration(seconds: 97, milliseconds: 626), - 'b': Duration(seconds: 104, milliseconds: 687), - 'b-': Duration(seconds: 113, milliseconds: 372), - 'c+': Duration(seconds: 123, milliseconds: 461), - 'c': Duration(seconds: 135, milliseconds: 326), - 'c-': Duration(seconds: 147, milliseconds: 056), - 'd+': Duration(seconds: 156, milliseconds: 757), - 'd': Duration(seconds: 167, milliseconds: 393), - //'z': Duration(seconds: 66, milliseconds: 802) +const Map rankColors = { + // thanks osk for const rankColors at https://ch.tetr.io/res/js/base.js:458 + 'x+': Color(0xFF643C8D), + 'x': Color(0xFFFF45FF), + 'u': Color(0xFFFF3813), + 'ss': Color(0xFFDB8B1F), + 's+': Color(0xFFD8AF0E), + 's': Color(0xFFE0A71B), + 's-': Color(0xFFB2972B), + 'a+': Color(0xFF1FA834), + 'a': Color(0xFF46AD51), + 'a-': Color(0xFF3BB687), + 'b+': Color(0xFF4F99C0), + 'b': Color(0xFF4F64C9), + 'b-': Color(0xFF5650C7), + 'c+': Color(0xFF552883), + 'c': Color(0xFF733E8F), + 'c-': Color(0xFF79558C), + 'd+': Color(0xFF8E6091), + 'd': Color(0xFF907591), + 'z': Color(0xFF375433) +}; + +const Map sprintAverages = { + // based on https://discord.com/channels/673303546107658242/674421736162197515/1244287342965952562 + 'x': Duration(seconds: 25, milliseconds: 144), + 'u': Duration(seconds: 36, milliseconds: 115), + 'ss': Duration(seconds: 46, milliseconds: 396), + 's+': Duration(seconds: 55, milliseconds: 056), + 's': Duration(seconds: 61, milliseconds: 892), + 's-': Duration(seconds: 68, milliseconds: 918), + 'a+': Duration(seconds: 76, milliseconds: 187), + 'a': Duration(seconds: 83, milliseconds: 529), + 'a-': Duration(seconds: 88, milliseconds: 608), + 'b+': Duration(seconds: 97, milliseconds: 626), + 'b': Duration(seconds: 104, milliseconds: 687), + 'b-': Duration(seconds: 113, milliseconds: 372), + 'c+': Duration(seconds: 123, milliseconds: 461), + 'c': Duration(seconds: 135, milliseconds: 326), + 'c-': Duration(seconds: 147, milliseconds: 056), + 'd+': Duration(seconds: 156, milliseconds: 757), + 'd': Duration(seconds: 167, milliseconds: 393), + //'z': Duration(seconds: 66, milliseconds: 802) }; const Map blitzAverages = { 'x': 600715, - 'u': 379418, - 'ss': 233740, - 's+': 158295, - 's': 125164, - 's-': 100933, - 'a+': 83593, - 'a': 68613, - 'a-': 60219, - 'b+': 51197, - 'b': 44171, - 'b-': 39045, - 'c+': 34130, - 'c': 28931, - 'c-': 25095, - 'd+': 22944, - 'd': 20728, - //'z': 72084 -}; \ No newline at end of file + 'u': 379418, + 'ss': 233740, + 's+': 158295, + 's': 125164, + 's-': 100933, + 'a+': 83593, + 'a': 68613, + 'a-': 60219, + 'b+': 51197, + 'b': 44171, + 'b-': 39045, + 'c+': 34130, + 'c': 28931, + 'c-': 25095, + 'd+': 22944, + 'd': 20728, + //'z': 72084 +}; + +List seasonStarts = [ + DateTime.utc(2020, DateTime.april, 18, 4), // Source = twitter or something + DateTime.utc( + 2024, DateTime.august, 16, 18, 41, 10) // Source = osk status page +]; + +List seasonEnds = [ + DateTime.utc(2024, DateTime.july, 26, 15) // Source - TETR.IO discord guild +]; diff --git a/lib/utils/colors_functions.dart b/lib/utils/colors_functions.dart index 70277d9..8ec5900 100644 --- a/lib/utils/colors_functions.dart +++ b/lib/utils/colors_functions.dart @@ -7,4 +7,8 @@ Color getColorOfRank(int rank){ if (rank <= 9) return Colors.blueAccent; if (rank <= 99) return Colors.greenAccent; return Colors.grey; +} + +Color getDifferenceColor(num diff){ + return diff.isNegative ? Colors.redAccent : Colors.greenAccent; } \ No newline at end of file diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index 097b705..f359edb 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -1,6 +1,7 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +final NumberFormat compareIntf = NumberFormat("+#,###;-#,###")..maximumFractionDigits = 0; final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2; final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 37aa103..7e17387 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -243,7 +243,7 @@ class _DestinationGraphsState extends State { bool _smooth = false; final 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"]; int _chartsIndex = 0; - late List>> chartsData; + late List>>> historyData; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override @@ -282,10 +282,7 @@ class _DestinationGraphsState extends State { super.initState(); } - Future>>> getChartsData(bool fetchHistory) async { - List states = []; - Set uniqueTL = {}; - + Future>>>> getHistoryData(bool fetchHistory) async { if(fetchHistory){ try{ var history = await teto.fetchAndsaveTLHistory(widget.searchFor); @@ -301,49 +298,47 @@ class _DestinationGraphsState extends State { } } - //states.addAll(await teto.getPlayer(widget.searchFor)); - // for (var element in states) { - // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); - // if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); - // } + List> states = await Future.wait>([ + teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), + ]); - if (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) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), - ]; + if (states.length >= 2){ + historyData = [for (List s in states) >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid + DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), + DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), + DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), + DropdownMenuItem(value: [for (var tl in s) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), + ]]; }else{ - chartsData = []; + historyData = []; } fetchData = false; - return chartsData; + return historyData; } @override Widget build(BuildContext context) { - return FutureBuilder>>>( - future: getChartsData(fetchData), + return FutureBuilder>>>>( + future: getHistoryData(fetchData), builder: (context, snapshot) { switch (snapshot.connectionState){ case ConnectionState.none: @@ -352,7 +347,7 @@ class _DestinationGraphsState extends State { return const Center(child: CircularProgressIndicator()); case ConnectionState.done: if (snapshot.hasData && snapshot.data!.isNotEmpty){ - List<_HistoryChartSpot> selectedGraph = snapshot.data![_chartsIndex].value!; + List<_HistoryChartSpot> selectedGraph = snapshot.data![currentSeason-1][_chartsIndex].value!; yAxisTitle = _historyShortTitles[_chartsIndex]; return SingleChildScrollView( scrollDirection: Axis.vertical, @@ -384,11 +379,11 @@ class _DestinationGraphsState extends State { children: [ const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), DropdownButton( - items: chartsData, - value: chartsData[_chartsIndex].value, + items: historyData[currentSeason-1], + value: historyData[currentSeason-1][_chartsIndex].value, onChanged: (value) { setState(() { - _chartsIndex = chartsData.indexWhere((element) => element.value == value); + _chartsIndex = historyData[currentSeason-1].indexWhere((element) => element.value == value); }); } ), @@ -411,7 +406,7 @@ class _DestinationGraphsState extends State { ], ), ), - if(chartsData[_chartsIndex].value!.length > 1) Card( + if(historyData[currentSeason-1][_chartsIndex].value!.length > 1) Card( child: SizedBox( width: MediaQuery.of(context).size.width - 88, height: MediaQuery.of(context).size.height - 60, @@ -427,7 +422,7 @@ class _DestinationGraphsState extends State { series: [ if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( enableTooltip: true, - dataSource: chartsData[_chartsIndex].value!, + dataSource: historyData[currentSeason-1][_chartsIndex].value!, animationDuration: 0, opacity: _smooth ? 0 : 1, xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, @@ -436,14 +431,14 @@ class _DestinationGraphsState extends State { trendlines:[ Trendline( isVisible: _smooth, - period: (chartsData[_chartsIndex].value!.length/175).floor(), + period: (historyData[currentSeason-1][_chartsIndex].value!.length/175).floor(), type: TrendlineType.movingAverage, color: Theme.of(context).colorScheme.primary) ], ) else StepLineSeries<_HistoryChartSpot, DateTime>( enableTooltip: true, - dataSource: chartsData[_chartsIndex].value!, + dataSource: historyData[currentSeason-1][_chartsIndex].value!, animationDuration: 0, opacity: _smooth ? 0 : 1, xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, @@ -452,7 +447,7 @@ class _DestinationGraphsState extends State { trendlines:[ Trendline( isVisible: _smooth, - period: (chartsData[_chartsIndex].value!.length/175).floor(), + period: (historyData[currentSeason-1][_chartsIndex].value!.length/175).floor(), type: TrendlineType.movingAverage, color: Theme.of(context).colorScheme.primary) ], @@ -462,7 +457,7 @@ class _DestinationGraphsState extends State { ) ), ) - else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column( + else if (historyData[currentSeason-1][_chartsIndex].value!.length <= 1) Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), @@ -525,11 +520,12 @@ class DestinationHome extends StatefulWidget{ class FetchResults{ bool success; TetrioPlayer? player; + List states; Summaries? summaries; Cutoffs? cutoffs; Exception? exception; - FetchResults(this.success, this.player, this.summaries, this.cutoffs, this.exception); + FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.exception); } class RecordSummary extends StatelessWidget{ @@ -623,7 +619,19 @@ class LeagueCard extends StatelessWidget{ child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(showSeasonNumber ? "Season ${league.season}" : "Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + if (showSeasonNumber) Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("Season ${league.season}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Spacer(), + Text( + "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey)), + ], + ) + else Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), const Divider(color: Color.fromARGB(50, 158, 158, 158)), TLRatingThingy(userID: "", tlData: league, showPositions: true), const Divider(color: Color.fromARGB(50, 158, 158, 158)), @@ -658,7 +666,7 @@ class _DestinationHomeState extends State { player = await teto.fetchPlayer(widget.searchFor); // Otherwise it's probably a user id or username } }on TetrioPlayerNotExist{ - return FetchResults(false, null, null, null, TetrioPlayerNotExist()); + return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); } late Summaries summaries; late Cutoffs cutoffs; @@ -666,9 +674,16 @@ class _DestinationHomeState extends State { teto.fetchSummaries(player.userId), teto.fetchCutoffsBeanserver(), ]); + List states = await teto.getStates(player.userId, season: currentSeason); summaries = requests[0]; cutoffs = requests[1]; - return FetchResults(true, player, summaries, cutoffs, null); + + bool isTracking = await teto.isPlayerTracking(player.userId); + if (isTracking){ // if tracked - save data to local DB + await teto.storeState(summaries.league); + } + + return FetchResults(true, player, states, summaries, cutoffs, null); } Widget getOverviewCard(Summaries summaries){ @@ -863,7 +878,8 @@ class _DestinationHomeState extends State { ); } - Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs){ + Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, List states){ + TetraLeague? toCompare = states.length >= 2 ? states.elementAtOrNull(states.length-2) : null; return Column( children: [ Card( @@ -876,13 +892,13 @@ class _DestinationHomeState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + //Text("${states.last.timestamp} ${states.last.tr}", textAlign: TextAlign.center) ], ), ), ), ), - TetraLeagueThingy(league: data, cutoffs: cutoffs), + TetraLeagueThingy(league: data, toCompare: toCompare, cutoffs: cutoffs), if (data.nerdStats != null) Card( //surfaceTintColor: rankColors[data.rank], child: Row( @@ -894,7 +910,7 @@ class _DestinationHomeState extends State { ], ), ), - if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!), + if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats), if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) ], ); @@ -1581,7 +1597,7 @@ class _DestinationHomeState extends State { child: switch (rightCard){ Cards.overview => getOverviewCard(snapshot.data!.summaries!), Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs), + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), CardMod.records => getRecentTLrecords(widget.constraints), _ => const Center(child: Text("huh?")) @@ -2065,12 +2081,10 @@ class BadgesThingy extends StatelessWidget{ icon: Image.asset( "res/tetrio_badges/${badge.badgeId}.png", height: 32, - width: 32, errorBuilder: (context, error, stackTrace) { return Image.network( kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png", height: 32, - width: 32, errorBuilder:(context, error, stackTrace) { return Image.asset("res/icons/kagari.png", height: 32, width: 32); } @@ -2407,9 +2421,10 @@ class _SearchDrawerState extends State { class TetraLeagueThingy extends StatelessWidget{ final TetraLeague league; + final TetraLeague? toCompare; final Cutoffs? cutoffs; - const TetraLeagueThingy({super.key, required this.league, this.cutoffs}); + const TetraLeagueThingy({super.key, required this.league, this.toCompare, this.cutoffs}); @override Widget build(BuildContext context) { @@ -2422,8 +2437,8 @@ class TetraLeagueThingy extends StatelessWidget{ tlData: league, previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, nextRankTRcutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.tr[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, - nextRankTRcutoffTarget: league.rank != "z" ? rankTargets[league.rank] : null, - previousRankTRcutoffTarget: (league.rank != "z" && league.rank != "x+") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(league.rank)+1)] : null, + previousRankTRcutoffTarget: league.rank != "z" ? rankTargets[league.rank] : null, + nextRankTRcutoffTarget: (league.rank != "z" && league.rank != "x+") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(league.rank)+1)] : null, previousGlickoCutoff: cutoffs != null ? cutoffs!.glicko[league.rank != "z" ? league.rank : league.percentileRank] : null, nextRankGlickoCutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.glicko[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, ), @@ -2438,16 +2453,19 @@ class TetraLeagueThingy extends StatelessWidget{ defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ - const Text("APM: ", style: TextStyle(fontSize: 21)), Text(f2.format(league.apm??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" APM", style: TextStyle(fontSize: 21)), + if (toCompare != null) Text(" (${comparef2.format(league.apm!-toCompare!.apm!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.apm!-toCompare!.apm!))) ]), TableRow(children: [ - const Text("PPS: ", style: TextStyle(fontSize: 21)), Text(f2.format(league.pps??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" PPS", style: TextStyle(fontSize: 21)), + if (toCompare != null) Text(" (${comparef2.format(league.pps!-toCompare!.pps!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.pps!-toCompare!.pps!))) ]), TableRow(children: [ - const Text("VS: ", style: TextStyle(fontSize: 21)), Text(f2.format(league.vs??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" VS", style: TextStyle(fontSize: 21)), + if (toCompare != null) Text(" (${comparef2.format(league.vs!-toCompare!.vs!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.vs!-toCompare!.vs!))) ]) ], ), @@ -2480,6 +2498,10 @@ class TetraLeagueThingy extends StatelessWidget{ GaugeAnnotation(widget: Container(child: Text(t.statCellNum.winrate, textAlign: TextAlign.center)), angle: 270,positionFactor: 0.4 + ), + if (toCompare != null) GaugeAnnotation(widget: Container(child: + Text(comparef2.format((league.winrate-toCompare!.winrate)*100), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(league.winrate-toCompare!.winrate)))), + angle: 90,positionFactor: 0.45 ) ], ) @@ -2495,17 +2517,20 @@ class TetraLeagueThingy extends StatelessWidget{ TableRow(children: [ //Text("VS: ", style: TextStyle(fontSize: 21)), Text("№ ${league.standingLocal.isNegative ? "---" : intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), - Text(" local", style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)) + Text(" local", style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), + if (toCompare != null) Text(" (${compareIntf.format(league.standingLocal-toCompare!.standingLocal)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.standingLocal-toCompare!.standingLocal))) ]), TableRow(children: [ //Text("APM: ", style: TextStyle(fontSize: 21)), Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Games", style: TextStyle(fontSize: 21)) + const Text(" Games", style: TextStyle(fontSize: 21)), + if (toCompare != null) Text(" (${comparef2.format(league.gamesPlayed-toCompare!.gamesPlayed)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)) ]), TableRow(children: [ //Text("PPS: ", style: TextStyle(fontSize: 21)), Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Won", style: TextStyle(fontSize: 21)) + const Text(" Won", style: TextStyle(fontSize: 21)), + if (toCompare != null) Text(" (${comparef2.format(league.gamesWon-toCompare!.gamesWon)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)) ]) ], ), @@ -2521,8 +2546,9 @@ class TetraLeagueThingy extends StatelessWidget{ class NerdStatsThingy extends StatelessWidget{ final NerdStats nerdStats; + final NerdStats? oldNerdStats; - const NerdStatsThingy({super.key, required this.nerdStats}); + const NerdStatsThingy({super.key, required this.nerdStats, this.oldNerdStats}); @override Widget build(BuildContext context) { @@ -2530,7 +2556,7 @@ class NerdStatsThingy extends StatelessWidget{ child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, @@ -2565,7 +2591,7 @@ class NerdStatsThingy extends StatelessWidget{ children: [ const TextSpan(text: "APP\n"), TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), - //TextSpan(text: "\nAPP"), + if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.app - oldNerdStats!.app)}", style: TextStyle(color: getDifferenceColor(nerdStats.app - oldNerdStats!.app))), ] ))), angle: 270,positionFactor: 0.5 @@ -2595,6 +2621,7 @@ class NerdStatsThingy extends StatelessWidget{ children: [ const TextSpan(text: "VS/APM\n"), TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), + if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.vsapm - oldNerdStats!.vsapm)}", style: TextStyle(color: getDifferenceColor(nerdStats.vsapm - oldNerdStats!.vsapm))), ] ))), angle: 90,positionFactor: 0.5 @@ -2608,15 +2635,17 @@ class NerdStatsThingy extends StatelessWidget{ Expanded( child: Wrap( alignment: WrapAlignment.center, - spacing: 10, + spacing: 10.0, + runSpacing: 10.0, + runAlignment: WrapAlignment.start, children: [ - GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2), - GaugetThingy(value: nerdStats.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1), + GaugetThingy(value: nerdStats.dss, oldValue: oldNerdStats?.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), + GaugetThingy(value: nerdStats.dsp, oldValue: oldNerdStats?.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), + GaugetThingy(value: nerdStats.appdsp, oldValue: oldNerdStats?.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), + GaugetThingy(value: nerdStats.cheese, oldValue: oldNerdStats?.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2, moreIsBetter: false), + GaugetThingy(value: nerdStats.gbe, oldValue: oldNerdStats?.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), + GaugetThingy(value: nerdStats.nyaapp, oldValue: oldNerdStats?.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), + GaugetThingy(value: nerdStats.area, oldValue: oldNerdStats?.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1, moreIsBetter: true), ], ), ) @@ -2667,12 +2696,14 @@ class GaugetThingy extends StatelessWidget{ final double value; final double min; final double max; + final double? oldValue; + final bool moreIsBetter; final double tickInterval; final String label; final double sideSize; final int fractionDigits; - GaugetThingy({super.key, required this.value, required this.min, required this.max, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits}); + GaugetThingy({super.key, required this.value, required this.min, required this.max, this.oldValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter}); @override Widget build(BuildContext context) { @@ -2704,6 +2735,10 @@ class GaugetThingy extends StatelessWidget{ GaugeAnnotation(widget: Container(child: Text(label, textAlign: TextAlign.center, style: const TextStyle(height: .9))), angle: 270,positionFactor: 0.4 + ), + if (oldValue != null) GaugeAnnotation(widget: Container(child: + Text(comparef2.format(value-oldValue!), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(moreIsBetter ? value-oldValue! : oldValue!-value)))), + angle: 90,positionFactor: 0.45 ) ], ) diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index 2e42f32..35e5153 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -166,17 +166,17 @@ class MyRadarChartPainter extends RadarChartPainter{ ); } - _ticksTextPaint - ..text = TextSpan( - text: percentage.format(tick), - style: Utils().getThemeAwareTextStyle(context, data.ticksTextStyle), - ) - ..textDirection = TextDirection.ltr - ..layout(maxWidth: size.width); - canvasWrapper.drawText( - _ticksTextPaint, - Offset(centerX + 5, centerY - tickRadius - _ticksTextPaint.height/2), - ); + // _ticksTextPaint + // ..text = TextSpan( + // text: percentage.format(tick), + // style: Utils().getThemeAwareTextStyle(context, data.ticksTextStyle), + // ) + // ..textDirection = TextDirection.ltr + // ..layout(maxWidth: size.width); + // canvasWrapper.drawText( + // _ticksTextPaint, + // Offset(centerX + 5, centerY - tickRadius - _ticksTextPaint.height/2), + // ); }, ); } @@ -302,12 +302,12 @@ class Graphs extends StatelessWidget{ width: 310, child: MyRadarChart( RadarChartData( - radarShape: RadarShape.polygon, + radarShape: RadarShape.circle, tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), getTitle: (index, angle) { switch (index) { case 0: @@ -336,7 +336,7 @@ class Graphs extends StatelessWidget{ }, dataSets: [ RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(100), + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), borderColor: Theme.of(context).colorScheme.primary, dataEntries: [ RadarEntry(value: apm * apmWeight), @@ -381,12 +381,12 @@ class Graphs extends StatelessWidget{ width: 310, child: MyRadarChart( RadarChartData( - radarShape: RadarShape.polygon, + radarShape: RadarShape.circle, tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), titleTextStyle: const TextStyle(height: 1.1), radarTouchData: RadarTouchData(), getTitle: (index, angle) { @@ -405,7 +405,7 @@ class Graphs extends StatelessWidget{ }, dataSets: [ RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(100), + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), borderColor: Theme.of(context).colorScheme.primary, dataEntries: [ RadarEntry(value: playstyle.opener), @@ -438,12 +438,12 @@ class Graphs extends StatelessWidget{ width: 310, child: MyRadarChart( RadarChartData( - radarShape: RadarShape.polygon, + radarShape: RadarShape.circle, tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), titleTextStyle: const TextStyle(height: 1.1), radarTouchData: RadarTouchData(), getTitle: (index, angle) { @@ -462,7 +462,7 @@ class Graphs extends StatelessWidget{ }, dataSets: [ RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(100), + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), borderColor: Theme.of(context).colorScheme.primary, dataEntries: [ RadarEntry(value: attack), From 5f404d350810a332c700e27ac0092911570f9e27 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 9 Sep 2024 01:23:02 +0300 Subject: [PATCH 04/86] i need to build fix, 1.6.10 gonna be out --- lib/main.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a847a1e..bbba219 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 5b7c6ad..8b5da74 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.6.9+35 +version: 1.6.10+36 environment: sdk: '>=3.0.0' From 500639df05004f5d4228ce2b64c76ca33346f1bb Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 10 Sep 2024 01:38:52 +0300 Subject: [PATCH 05/86] meh... --- lib/main.dart | 2 +- lib/views/main_view_tiles.dart | 420 ++++++++++++++++++------------- lib/widgets/tl_progress_bar.dart | 6 +- 3 files changed, 250 insertions(+), 178 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index bbba219..a847a1e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 7e17387..8136524 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -233,8 +233,13 @@ class DestinationGraphs extends StatefulWidget{ State createState() => _DestinationGraphsState(); } +enum Graph{ + history, + leagueState, + leagueCutoffs +} + class _DestinationGraphsState extends State { - Cards rightCard = Cards.tetraLeague; bool fetchData = false; bool _gamesPlayedInsteadOfDateAndTime = false; late ZoomPanBehavior _zoomPanBehavior; @@ -242,7 +247,9 @@ class _DestinationGraphsState extends State { String yAxisTitle = ""; bool _smooth = false; final 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"]; + Graph _graph = Graph.history; int _chartsIndex = 0; + int _season = currentSeason-1; late List>>> historyData; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @@ -337,163 +344,202 @@ class _DestinationGraphsState extends State { @override Widget build(BuildContext context) { - return FutureBuilder>>>>( - future: getHistoryData(fetchData), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData && snapshot.data!.isNotEmpty){ - List<_HistoryChartSpot> selectedGraph = snapshot.data![currentSeason-1][_chartsIndex].value!; - yAxisTitle = _historyShortTitles[_chartsIndex]; - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - 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) { - setState(() { - _gamesPlayedInsteadOfDateAndTime = value!; - }); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: historyData[currentSeason-1], - value: historyData[currentSeason-1][_chartsIndex].value, - onChanged: (value) { - setState(() { - _chartsIndex = historyData[currentSeason-1].indexWhere((element) => element.value == value); - }); - } - ), - ], - ), - if (selectedGraph.length > 300) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox(value: _smooth, - checkColor: Colors.black, - onChanged: ((value) { - setState(() { - _smooth = value!; - }); - })), - Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - ), - if(historyData[currentSeason-1][_chartsIndex].value!.length > 1) Card( - child: SizedBox( - width: MediaQuery.of(context).size.width - 88, - height: MediaQuery.of(context).size.height - 60, - child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), - child: SfCartesianChart( - tooltipBehavior: _tooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, - ), - margin: const EdgeInsets.all(0), - series: [ - if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( - enableTooltip: true, - dataSource: historyData[currentSeason-1][_chartsIndex].value!, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (historyData[currentSeason-1][_chartsIndex].value!.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ) - else StepLineSeries<_HistoryChartSpot, DateTime>( - enableTooltip: true, - dataSource: historyData[currentSeason-1][_chartsIndex].value!, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (historyData[currentSeason-1][_chartsIndex].value!.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ), - ], - ), - ) - ), - ) - else if (historyData[currentSeason-1][_chartsIndex].value!.length <= 1) Center(child: Column( + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder>>>>( + future: getHistoryData(fetchData), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData && snapshot.data!.isNotEmpty){ + List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_chartsIndex].value!; + yAxisTitle = _historyShortTitles[_chartsIndex]; + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - Text(t.errors.actionSuggestion), - TextButton(onPressed: (){setState(() { - fetchData = true; - });}, child: Text(t.fetchAndsaveTLHistory)) + Card( + child: Wrap( + spacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], + value: _season, + onChanged: (value) { + setState(() { + _season = value!; + }); + } + ), + ], + ), + 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) { + setState(() { + _gamesPlayedInsteadOfDateAndTime = value!; + }); + } + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: historyData[_season], + value: historyData[_season][_chartsIndex].value, + onChanged: (value) { + setState(() { + _chartsIndex = historyData[_season].indexWhere((element) => element.value == value); + }); + } + ), + ], + ), + if (selectedGraph.length > 300) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox(value: _smooth, + checkColor: Colors.black, + onChanged: ((value) { + setState(() { + _smooth = value!; + }); + })), + Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) + ], + ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) + ], + ), + ), + if(historyData[_season][_chartsIndex].value!.length > 1) Card( + child: SizedBox( + width: MediaQuery.of(context).size.width - 88, + height: MediaQuery.of(context).size.height - 96, + child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), + child: SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( + rangePadding: ChartRangePadding.additional, + ), + margin: const EdgeInsets.all(0), + series: [ + if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( + enableTooltip: true, + dataSource: historyData[_season][_chartsIndex].value!, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (historyData[_season][_chartsIndex].value!.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ) + else StepLineSeries<_HistoryChartSpot, DateTime>( + enableTooltip: true, + dataSource: historyData[_season][_chartsIndex].value!, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (historyData[_season][_chartsIndex].value!.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ), + ], + ), + ) + ), + ) + else if (historyData[_season][_chartsIndex].value!.length <= 1) Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + Text(t.errors.actionSuggestion), + TextButton(onPressed: (){setState(() { + fetchData = true; + });}, child: Text(t.fetchAndsaveTLHistory)) + ], + )) ], - )) - ], - ), - ); - } - if (snapshot.hasError || snapshot.data!.isEmpty){ - return Center(child: - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error != null ? snapshot.error.toString() : t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), - ), - ], - ) - ); - } - } - return const Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("lol", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - ], - )); - }, + ), + ); + } + if (snapshot.hasError || snapshot.data!.isEmpty){ + return Center(child: + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error != null ? snapshot.error.toString() : t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + return const Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("lol", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + ], + )); + }, + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: Graph.history, + label: Text('Player History')), + ButtonSegment( + value: Graph.leagueState, + label: Text('League State')), + ButtonSegment( + value: Graph.leagueCutoffs, + label: Text('League Cutoffs'), + ), + ], + selected: {_graph}, + onSelectionChanged: (Set newSelection) { + setState(() { + _graph = newSelection.first; + });}) + ], ); } } @@ -2432,7 +2478,7 @@ class TetraLeagueThingy extends StatelessWidget{ //surfaceTintColor: rankColors[league.rank], child: Column( children: [ - TLRatingThingy(userID: "w", tlData: league), + TLRatingThingy(userID: "w", tlData: league, oldTl: toCompare, showPositions: true), TLProgress( tlData: league, previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, @@ -2514,12 +2560,6 @@ class TetraLeagueThingy extends StatelessWidget{ child: Table( defaultColumnWidth:const IntrinsicColumnWidth(), children: [ - TableRow(children: [ - //Text("VS: ", style: TextStyle(fontSize: 21)), - Text("№ ${league.standingLocal.isNegative ? "---" : intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), - Text(" local", style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), - if (toCompare != null) Text(" (${compareIntf.format(league.standingLocal-toCompare!.standingLocal)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.standingLocal-toCompare!.standingLocal))) - ]), TableRow(children: [ //Text("APM: ", style: TextStyle(fontSize: 21)), Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), @@ -2531,7 +2571,13 @@ class TetraLeagueThingy extends StatelessWidget{ Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), const Text(" Won", style: TextStyle(fontSize: 21)), if (toCompare != null) Text(" (${comparef2.format(league.gamesWon-toCompare!.gamesWon)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)) - ]) + ]), + TableRow(children: [ + //Text("VS: ", style: TextStyle(fontSize: 21)), + Tooltip(child: Text("${league.gxe.isNegative ? "---" : f3.format(league.gxe)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), message: "${f2.format(league.s1tr)}",), + Text(" GLIXARE", style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), + if (toCompare != null) Text(" (${comparef.format(league.gxe-toCompare!.gxe)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.standingLocal-toCompare!.standingLocal))) + ]), ], ), ), @@ -2952,7 +2998,7 @@ class TLRatingThingy extends StatelessWidget{ children: [ RichText( text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white), + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white, height: 0.9), children: (tlData.gamesPlayed > 9) ? switch(prefs.getInt("ratingMode")){ 1 => [ TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), @@ -2972,17 +3018,43 @@ class TLRatingThingy extends StatelessWidget{ } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),] ) ), - if (oldTl != null) Text( - switch(prefs.getInt("ratingMode")){ - 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", - 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", - _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" - }, + if (oldTl != null) RichText( textAlign: TextAlign.center, - style: TextStyle( - color: tlData.tr - oldTl!.tr < 0 ? - Colors.red : - Colors.green + softWrap: true, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan(text: switch(prefs.getInt("ratingMode")){ + 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", + 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", + _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" + }, + style: TextStyle( + color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ + 1 => tlData.glicko! - oldTl!.glicko!, + 2 => tlData.percentile - oldTl!.percentile, + _ => tlData.tr - oldTl!.tr + }) + ), + ), + const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), + TextSpan(text: switch(prefs.getInt("ratingMode")){ + 1 => "${fDiff.format(tlData.tr - oldTl!.tr)} TR", + _ => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko" + }, + style: TextStyle( + color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ + 1 => tlData.tr - oldTl!.tr, + _ => tlData.glicko! - oldTl!.glicko! + }) + ), + ), + const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), + TextSpan( + text: "${fDiff.format(tlData.rd! - oldTl!.rd!)} RD", + style: TextStyle(color: getDifferenceColor(oldTl!.rd! - tlData.rd!)) + ) + ], ), ), if (tlData.gamesPlayed > 9) Column( diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index ed23430..9e5c492 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -74,12 +74,12 @@ class TLProgress extends StatelessWidget{ ranges: [ if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.tr)!, color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross) else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross), - if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent, position: LinearElementPosition.inside), - if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside) + if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent.withAlpha(175), position: LinearElementPosition.inside), + if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent.withAlpha(175), position: LinearElementPosition.inside) ], markerPointers: [ LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), - if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) + //if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) ], isMirrored: true, showTicks: true, From 6b84e67f335ea1846563574e31344c25e41f8e77 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 11 Sep 2024 00:22:17 +0300 Subject: [PATCH 06/86] Thinking about animations... --- lib/views/main_view_tiles.dart | 432 ++++++++++++++++++--------------- 1 file changed, 243 insertions(+), 189 deletions(-) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 8136524..56690f9 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -42,6 +42,8 @@ import 'package:tetra_stats/widgets/tl_progress_bar.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; var fDiff = NumberFormat("+#,###.####;-#,###.####"); +late Future _data; +late Future _newsData; class MainView extends StatefulWidget { final String? player; @@ -78,12 +80,44 @@ class _MainState extends State with TickerProviderStateMixin { void initState() { teto.open(); controller = ScrollController(); + changePlayer(_searchFor); super.initState(); } + Future _getData() async { + TetrioPlayer player; + try{ + if (_searchFor.startsWith("ds:")){ + player = await teto.fetchPlayer(_searchFor.substring(3), isItDiscordID: true); // we trying to get him with that + }else{ + player = await teto.fetchPlayer(_searchFor); // Otherwise it's probably a user id or username + } + }on TetrioPlayerNotExist{ + return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); + } + late Summaries summaries; + late Cutoffs cutoffs; + List requests = await Future.wait([ + teto.fetchSummaries(player.userId), + teto.fetchCutoffsBeanserver(), + ]); + List states = await teto.getStates(player.userId, season: currentSeason); + summaries = requests[0]; + cutoffs = requests[1]; + + bool isTracking = await teto.isPlayerTracking(player.userId); + if (isTracking){ // if tracked - save data to local DB + await teto.storeState(summaries.league); + } + + return FetchResults(true, player, states, summaries, cutoffs, null); + } + void changePlayer(String player) { setState(() { _searchFor = player; + _data = _getData(); + _newsData = teto.fetchNews(_searchFor); }); } @@ -114,36 +148,47 @@ class _MainState extends State with TickerProviderStateMixin { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - NavigationRail( - leading: FloatingActionButton( - elevation: 0, - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - child: const Icon(Icons.search), - ), - trailing: IconButton( - onPressed: () { - // Add your onPressed code here! - }, - icon: const Icon(Icons.more_horiz_rounded), - ), - destinations: [ - getDestinationButton(Icons.home, "Home"), - getDestinationButton(Icons.data_thresholding_outlined, "Graphs"), - getDestinationButton(Icons.leaderboard, "Leaderboards"), - getDestinationButton(Icons.compress, "Cutoffs"), - getDestinationButton(Icons.calculate, "Calc"), - getDestinationButton(Icons.storage, "Saved Data"), - getDestinationButton(Icons.settings, "Settings"), - ], - selectedIndex: destination, - onDestinationSelected: (value) { - setState(() { - destination = value; - }); - }, - ), + TweenAnimationBuilder( + child: NavigationRail( + leading: FloatingActionButton( + elevation: 0, + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + child: const Icon(Icons.search), + ), + trailing: IconButton( + onPressed: () { + // Add your onPressed code here! + }, + icon: const Icon(Icons.more_horiz_rounded), + ), + destinations: [ + getDestinationButton(Icons.home, "Home"), + getDestinationButton(Icons.data_thresholding_outlined, "Graphs"), + getDestinationButton(Icons.leaderboard, "Leaderboards"), + getDestinationButton(Icons.compress, "Cutoffs"), + getDestinationButton(Icons.calculate, "Calc"), + getDestinationButton(Icons.storage, "Saved Data"), + getDestinationButton(Icons.settings, "Settings"), + ], + selectedIndex: destination, + onDestinationSelected: (value) { + setState(() { + destination = value; + }); + }, + ), + duration: Durations.long4, + tween: Tween(begin: 0, end: 1), + curve: Easing.emphasizedDecelerate, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(-80+value*80, 0, 0), + child: child, + ); + }, + ), Expanded( child: switch (destination){ 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), @@ -691,7 +736,7 @@ class LeagueCard extends StatelessWidget{ } -class _DestinationHomeState extends State { +class _DestinationHomeState extends State with SingleTickerProviderStateMixin { Cards rightCard = Cards.overview; CardMod cardMod = CardMod.info; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @@ -700,38 +745,10 @@ class _DestinationHomeState extends State { late bool blitzBetterThanClosestAverage; late MapEntry? closestAverageSprint; late bool sprintBetterThanClosestAverage; + late AnimationController _transition; bool? sprintBetterThanRankAverage; bool? blitzBetterThanRankAverage; - Future _getData() async { - TetrioPlayer player; - try{ - if (widget.searchFor.startsWith("ds:")){ - player = await teto.fetchPlayer(widget.searchFor.substring(3), isItDiscordID: true); // we trying to get him with that - }else{ - player = await teto.fetchPlayer(widget.searchFor); // Otherwise it's probably a user id or username - } - }on TetrioPlayerNotExist{ - return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); - } - late Summaries summaries; - late Cutoffs cutoffs; - List requests = await Future.wait([ - teto.fetchSummaries(player.userId), - teto.fetchCutoffsBeanserver(), - ]); - List states = await teto.getStates(player.userId, season: currentSeason); - summaries = requests[0]; - cutoffs = requests[1]; - - bool isTracking = await teto.isPlayerTracking(player.userId); - if (isTracking){ // if tracked - save data to local DB - await teto.storeState(summaries.league); - } - - return FetchResults(true, player, states, summaries, cutoffs, null); - } - Widget getOverviewCard(Summaries summaries){ return Column( children: [ @@ -1537,13 +1554,22 @@ class _DestinationHomeState extends State { ) ] }; + + _transition = AnimationController(vsync: this, value: 0, duration: Durations.long4); + + _transition.addListener((){ + setState(() { + + }); + }); + super.initState(); } @override Widget build(BuildContext context) { return FutureBuilder( - future: _getData(), + future: _data, builder: (context, snapshot) { switch (snapshot.connectionState){ case ConnectionState.none: @@ -1576,141 +1602,169 @@ class _DestinationHomeState extends State { closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; } - return Row( - children: [ - SizedBox( - width: 450, - child: Column( - children: [ - NewUserThingy(player: snapshot.data!.player!, showStateTimestamp: false, setState: setState), - if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), - if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), - if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), - if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) - else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), - if (snapshot.data!.player!.bio != null) Card( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ], - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), - ) - ], + return TweenAnimationBuilder( + duration: Durations.long4, + tween: Tween(begin: 0, end: 1), + curve: Easing.emphasizedDecelerate, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(0, 600-value*600, 0), + child: child, + ); + }, + child: Row( + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + NewUserThingy(player: snapshot.data!.player!, showStateTimestamp: false, setState: setState), + if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), + if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), + if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), + if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) + else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), + if (snapshot.data!.player!.bio != null) Card( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], + ), ), - ), - //if (testNews != null && testNews!.news.isNotEmpty) - Expanded( - child: FutureBuilder( - future: teto.fetchNews(widget.searchFor), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Card(child: Center(child: CircularProgressIndicator())); - case ConnectionState.done: - if (snapshot.hasData){ - return NewsThingy(snapshot.data!); - }else if (snapshot.hasError){ - return Card(child: Column(children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Text(snapshot.stackTrace.toString()) - ] - )); + //if (testNews != null && testNews!.news.isNotEmpty) + Expanded( + child: FutureBuilder( + future: _newsData, + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Card(child: Center(child: CircularProgressIndicator())); + case ConnectionState.done: + if (snapshot.hasData){ + return NewsThingy(snapshot.data!); + }else if (snapshot.hasError){ + return Card(child: Column(children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Text(snapshot.stackTrace.toString()) + ] + )); + } } + return const Text("what?"); } - return const Text("what?"); - } + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: Column( + children: [ + SizedBox( + height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, + child: SingleChildScrollView( + child: DualTransitionBuilder( + animation: _transition, + forwardBuilder: (context, animation, child){ + print(animation); + return Container( + transform: Matrix4.translationValues(600-animation.value*600, 0, 0), + child: child! + ); + }, + reverseBuilder: (context, animation, child){ + return Container( + transform: Matrix4.translationValues(-600+animation.value*600, 0, 0), + child: child! + ); + }, + child: switch (rightCard){ + Cards.overview => getOverviewCard(snapshot.data!.summaries!), + Cards.tetraLeague => switch (cardMod){ + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), + CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), + CardMod.records => getRecentTLrecords(widget.constraints), + _ => const Center(child: Text("huh?")) + }, + Cards.quickPlay => switch (cardMod){ + CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), + CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), + CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), + CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), + }, + Cards.sprint => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + Cards.blitz => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + }, + ), + ), ), - ) - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: Column( - children: [ - SizedBox( - height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, - child: SingleChildScrollView( - child: switch (rightCard){ - Cards.overview => getOverviewCard(snapshot.data!.summaries!), - Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), - CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), - CardMod.records => getRecentTLrecords(widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.quickPlay => switch (cardMod){ - CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), - CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), - CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), - CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), - }, - Cards.sprint => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.blitz => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, + if (modeButtons[rightCard]!.length > 1) SegmentedButton( + showSelectedIcon: false, + selected: {cardMod}, + segments: modeButtons[rightCard]!, + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + //_transition.; + }); }, ), - ), - if (modeButtons[rightCard]!.length > 1) SegmentedButton( - showSelectedIcon: false, - selected: {cardMod}, - segments: modeButtons[rightCard]!, - onSelectionChanged: (p0) { - setState(() { - cardMod = p0.first; - }); - }, - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: Cards.overview, - //label: Text('Overview'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Cards.tetraLeague, - //label: Text('Tetra League'), - icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.quickPlay, - //label: Text('Quick Play'), - icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.sprint, - //label: Text('40 Lines'), - icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.blitz, - //label: Text('Blitz'), - icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ], - selected: {rightCard}, - onSelectionChanged: (Set newSelection) { - setState(() { - cardMod = CardMod.info; - rightCard = newSelection.first; - });}) - ], + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: Cards.overview, + //label: Text('Overview'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + //label: Text('Tetra League'), + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.quickPlay, + //label: Text('Quick Play'), + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.sprint, + //label: Text('40 Lines'), + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.blitz, + //label: Text('Blitz'), + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ], + selected: {rightCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + cardMod = CardMod.info; + rightCard = newSelection.first; + });}) + ], + ) ) - ) - ], + ], + ), ); } } From 4a13b99f011a8a6b3b40c0175049c5b6c6b77267 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:53:17 +0800 Subject: [PATCH 07/86] Create strings_zh-hans.i18n.json --- res/i18n/strings_zh-hans.i18n.json | 640 +++++++++++++++++++++++++++++ 1 file changed, 640 insertions(+) create mode 100644 res/i18n/strings_zh-hans.i18n.json diff --git a/res/i18n/strings_zh-hans.i18n.json b/res/i18n/strings_zh-hans.i18n.json new file mode 100644 index 0000000..dbc2d04 --- /dev/null +++ b/res/i18n/strings_zh-hans.i18n.json @@ -0,0 +1,640 @@ +{ + "locales(map)": { + "en": "英语 (English)", + "ru": "俄语 (Русский)", + "zh-cn": "简体中文" + }, + "tetraLeague": "Tetra联赛", + "tlRecords": "Tetra联赛记录", + "history": "历史", + "sprint": "40行竞速", + "blitz": "闪电战", + "recent": "最近", + "recentRuns": "最近游玩局数", + "blitzScore": "$p 分", + "openSPreplay": "在TETR.IO打开回放", + "downloadSPreplay": "下载回放", + "other": "其他", + "distinguishment": "区别", + "zen": "禅意模式", + "bio": "个人简介", + "news": "新闻", + "newsParts": { + "leaderboardStart": "取得 ", + "leaderboardMiddle": "on ", + "personalbest": "在 ", + "personalbestMiddle": " 中取得了新的个人最好成绩 ", + "badgeStart": "获得勋章 ", + "badgeEnd": "", + "rankupStart": "达成 ", + "rankupMiddle": "${r} 段位 ", + "rankupEnd": "", + "tetoSupporter": "TETR.IO 会员", + "supporterStart": "成为了 ", + "supporterGiftStart": "被赠送了 ", + "unknownNews": "未知新闻 ${type}" + }, + "openSearch": "搜索玩家", + "closeSearch": "关闭搜索", + "searchHint": "昵称,ID或Discord用户ID(需要 \"ds:\" 前缀)", + "refresh": "刷新", + "fetchAndsaveTLHistory": "获取玩家历史", + "fetchAndSaveOldTLmatches": "获取玩家Tetra联赛历史", + "fetchAndsaveTLHistoryResult": "找到 ${number} 个状态", + "fetchAndSaveOldTLmatchesResult": "找到 ${number} 场Tetra联赛比赛", + "showStoredData": "显示获得的数据", + "statsCalc": "统计计算器", + "settings": "设置", + "track": "添加到\n跟踪列表", + "stopTracking": "从跟踪列表\n中移除", + "becameTracked": "已添加到跟踪列表!", + "compare": "对比", + "stoppedBeingTracked": "已从跟踪列表中移除!", + "tlLeaderboard": "Tetra联赛排行榜", + "noRecords": "无记录", + "noOldRecords": { + "zero": "无记录", + "one": "只有 $n 个记录", + "two": "只有 $n 个记录", + "few": "只有 $n 个记录", + "many": "只有 $n 个记录", + "other": "只有 $n 个记录" + }, + "noRecord": "只有 $n 个记录", + "botRecord": "机器人不予参加排位赛", + "anonRecord": "匿名用户不予参加排位赛", + "notEnoughData": "没有足够的数据", + "noHistorySaved": "没有保存历史", + "pseudoTooltipHeaderInit": "将鼠标放在点上", + "pseudoTooltipFooterInit": "以查看详细信息", + "obtainDate": "在 ${date} 获得", + "fetchDate": "Fetched ${date}", + "exactGametime": "实际游玩时长", + "bigRedBanned": "该账号封禁中", + "normalBanned": "封禁", + "bigRedBadStanding": "信誉不佳", + "copiedToClipboard": "已复制", + "playerRoleAccount": "账号", + "wasFromBeginning": "that was from very beginning", + "created": "创建于", + "botCreatedBy": "", + "notSupporter": "非会员", + "assignedManualy": "该勋章由 TETR.IO 管理员手动分配", + "supporter": "会员等级 ${tier}", + "comparingWith": "${newDate} 时的数据与 ${oldDate} 比较", + "top": "前", + "topRank": "最高段位", + "verdictGeneral": "比 $rank 段平均数据$verdict $n", + "verdictBetter": "好", + "verdictWorse": "差", + "smooth": "平滑", + "postSeason": "淡季", + "seasonStarts": "下一赛即将开始于:", + "nanow": "暂未完成,敬请等待!", + "seasonEnds": "赛季将会在 ${countdown} 后结束", + "seasonEnded": "Season has ended", + "gamesUntilRanked": "还有 ${left} 场比赛获取段位", + "numOfVictories": "~${wins} 场胜局", + "promotionOnNextWin": "下一次胜利即可升段", + "numOfdefeats": "~${losses} 场败局", + "demotionOnNextLoss": "下一次失败即可掉段", + "nerdStats": "详细信息", + "playersYouTrack": "Players you track", + "formula": "公式", + "exactValue": "实际值", + "neverPlayedTL": "此用户没有参与Tetra联赛", + "botTL": "机器人不予参加Tetra联赛", + "anonTL": "匿名用户不予参加Tetra联赛", + "quickPlay": "快速游戏", + "expert": "专家", + "withMods": "带着模组", + "withModsPlural": { + "zero": "带着 $n 个模组", + "one": "带着 $n 个模组", + "two": "带着 $n 个模组", + "few": "带着 $n 个模组", + "many": "带着 $n 个模组", + "other": "带着 $n 个模组" + }, + "exportDB": "导出本地数据", + "exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.", + "desktopExportAlertTitle": "Desktop export", + "desktopExportText": "It seems like you using this app on desktop. Check your documents folder, you should find \"TetraStats.db\". Copy it somewhere", + "androidExportAlertTitle": "Android export", + "androidExportText": "导出成功\n${exportedDB}", + "importDB": "导入本地数据", + "importDBDescription": "Restore your backup. Notice that already stored database will be overwritten.", + "importWrongFileType": "文件类型错误", + "importCancelled": "Operation was cancelled", + "importSuccess": "导入成功", + "yourID": "你的 TETR.IO 用户", + "yourIDAlertTitle": "你的 TETR.IO 昵称", + "yourIDText": "当程序加载,它将显示此用户的数据", + "language": "语言", + "updateInBackground": "自动升级数据", + "updateInBackgroundDescription": "While Tetra Stats is running, it can update stats of the current player when cache expires", + "customization": "自定义", + "customizationDescription": "Change appearance of different things in Tetra Stats UI", + "oskKagari": "Osk Kagari gimmick", + "oskKagariDescription": "If on, osk's rank on main view will be rendered as :kagari:", + "AccentColor": "主题色", + "AccentColorDescription": "Almost all interactive UI elements highlighted with this color", + "timestamps": "时间", + "timestampsDescription": "You can choose, in which way timestamps shows time", + "timestampsAbsoluteGMT": "绝对 (GMT)", + "timestampsAbsoluteLocalTime": "绝对 (你的时区)", + "timestampsRelative": "相对", + "rating": "Main representation of rating", + "ratingDescription": "TR 不是线性的,而 Glicko 没有边界,百分位数易挥发", + "ratingLBposition": "LB 位置", + "sheetbotGraphs": "Sheetbot式雷达图", + "sheetbotGraphsDescription": "若开启,雷达图上的点为负时可以出现在对面", + "lbStats": "显示基于排行榜的数据", + "lbStatsDescription": "这会影响加载时间,但允许您通过统计数据查看排行榜上的位置并与平均值进行比较", + "aboutApp": "关于", + "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", + "stateViewTitle": "${nickname} 在 ${date}", + "statesViewTitle": "${nickname} 的 ${number} 个状态", + "matchesViewTitle": "${nickname} 的Tetra联赛历史", + "statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} 次游戏", + "stateRemoved": "成功移除 ${date} 的状态!", + "matchRemoved": "成功移除 ${date} 的比赛!", + "viewAllMatches": "查看所有比赛", + "trackedPlayersViewTitle": "获取的数据", + "trackedPlayersZeroEntrys": "列表为空。 Press \"Track\" button in previous view to add current player here", + "trackedPlayersOneEntry": "只有 1 个玩家", + "trackedPlayersManyEntrys": "${numberOfPlayers} 个玩家", + "trackedPlayersEntry": "${nickname}:${numberOfStates} 个状态", + "trackedPlayersDescription": "从 ${firstStateDate} 到 ${lastStateDate}", + "trackedPlayersStatesDeleted": "${nickname} states was removed from database!", + "duplicatedFix": "删除重复的 TL 匹配项", + "compressDB": "Compress DB", + "SpaceSaved": "Space saved: ${size}", + "averageXrank": "平均 ${rankLetter} 段", + "vs": "vs", + "inTLmatch": "在Tetra联赛中", + "downloadReplay": "下载 .ttrm 回放", + "openReplay": "在 TETR.IO 打开回放", + "replaySaved": "已保存回放至 ${path}", + "match": "Match", + "timeWeightedmatch": "Match (time-weighted)", + "roundNumber": "第 $n 回合", + "statsFor": "数据:", + "numberOfRounds": "回合数", + "matchLength": "比赛时长", + "roundLength": "回合时长", + "matchStats": "比赛数据", + "timeWeightedmatchStats": "时间加权比赛数据", + "replayIssue": "Can't process replay", + "matchIsTooOld": "无回放", + "winner": "赢家", + "registred": "Registred", + "playedTL": "游玩过Tetra联赛", + "winChance": "胜利机会", + "byGlicko": "靠Glicko", + "byEstTR": "靠预测TR", + "compareViewNoValues": "请输入用户名,用户IO,APM-PPS-VS值 (分隔符不重要,只需要顺序)或者$avgR(R是一个段位)到两个Please, enter username, user ID, APM-PPS-VS values (divider doesn't matter, only order matter) or $avgR (where R is rank) to both fields", + "compareViewWrongValue": "获取 ${value} 失败", + "mostRecentOne": "最接近的", + "yes": "是", + "no": "否", + "daysLater": "天后", + "dayseBefore": "天前", + "fromBeginning": "开服", + "calc": "计算器", + "calcViewNoValues": "输入值以计算数据", + "rankAveragesViewTitle": "段位分隔符", + "sprintAndBlitsViewTitle": "竞速与闪电战平均数据", + "sprintAndBlitsRelevance": "数据来自${date}", + "rank": "段位", + "averages": "平均", + "lbViewZeroEntrys": "空", + "lbViewOneEntry": "只有一个玩家", + "lbViewManyEntrys": "有 ${numberOfPlayers}", + "everyoneAverages": "Tetra联赛散点图", + "sortBy": "排序依据", + "reversed": "反向", + "country": "地区", + "rankAverages": "$rank段位散点图", + "players": { + "zero": "$n 个玩家", + "one": "$n 个玩家", + "two": "$n 个玩家", + "few": "$n 个玩家", + "many": "$n 个玩家", + "other": "$n 个玩家" + }, + "games": { + "zero": "$n 场游戏", + "one": "$n 场游戏", + "two": "$n 场游戏", + "few": "$n 场游戏", + "many": "$n 场游戏", + "other": "$n 场游戏" + }, + "gamesPlayed": "$games 场游戏", + "chart": "列表", + "entries": "条目", + "minimums": "最小值", + "maximums": "最大值", + "lowestValues": "最小值", + "averageValues": "平均值", + "highestValues": "最大值", + "forPlayer": "来自用户 $username", + "currentAxis": "$axis 轴:", + "p1nkl0bst3rAlert": "That data was retrived from third party API maintained by p1nkl0bst3r", + "notForWeb": "Function is not available for web version", + "graphs": { + "attack": "Attack", + "speed": "Speed", + "defense": "Defense", + "cheese": "Cheese" + }, + "statCellNum": { + "xpLevel": "XP等级", + "xpProgress": "Progress to next level", + "xpFrom0ToLevel": "从 0 到 $n 等级的进度", + "xpLeft": "XP 还有", + "hoursPlayed": "小时游玩", + "onlineGames": "在线游戏场次", + "gamesWon": "获胜场次", + "totalGames": "总在线游戏场次", + "totalWon": "总在线游戏获胜场次", + "friends": "好友", + "apm": "每分\n发送垃圾行", + "vs": "VS\n分数", + "recordLB": "名次", + "lbp": "名次", + "lbpShort": "名次", + "lbpc": "地区\n名次", + "lbpcShort": "地区名次", + "gamesPlayed": "游戏\n场次", + "gamesWonTL": "获胜\n场次", + "winrate": "胜率", + "level": "等级", + "score": "分数", + "spp": "每块\n得分", + "pieces": "放置\n块数", + "pps": "每秒\n放置块数", + "finesseFaults": "非极简\n操作", + "finessePercentage": "极简率", + "keys": "按键", + "kpp": "每块\n按键", + "kps": "每秒\n按键", + "tr": "Tetra分数", + "rd": "偏移值", + "app": "每块发送垃圾行数", + "appDescription": "(Abbreviated as APP) Main efficiency metric. Tells how many attack you producing per piece", + "vsapmDescription": "Basically, tells how much and how efficient you using garbage in your attacks", + "dss": "每秒\n挖掘", + "dssDescription": "(Abbreviated as DS/S) Downstack per Second measures how many garbage lines you clear in a second.", + "dsp": "每块\n挖掘", + "dspDescription": "(Abbreviated as DS/P) Downstack per Piece measures how many garbage lines you clear per piece.", + "appdsp": "APP + DS/P", + "appdspDescription": "Just a sum of Attack per Piece and Downstack per Piece.", + "cheese": "奶酪层\n指数", + "cheeseDescription": "(Abbreviated as Cheese) Cheese Index is an approximation how much clean / cheese garbage player sends. Lower = more clean. Higher = more cheese.\nInvented by kerrmunism", + "gbe": "垃圾行\n效率", + "gbeDescription": "(Abbreviated as Gb Eff.) Garbage Efficiency measures how well player uses their garbage. Higher = better or they use their garbage more. Lower = they mostly send their garbage back at cheese or rarely clear garbage.\nInvented by Zepheniah and Dragonboy.", + "nyaapp": "加权\nAPP", + "nyaappDescription": "(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.", + "area": "面积", + "areaDescription": "How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections", + "estOfTR": "预测 TR", + "estOfTRShort": "预测 TR", + "accOfEst": "预测实际差量", + "accOfEstShort": "预测实际差量" + }, + "playerRole(map)": { + "user": "用户", + "banned": "封禁", + "bot": "机器人", + "sysop": "System operator", + "admin": "管理员", + "mod": "Moderator", + "halfmod": "Community moderator", + "anon": "匿名" + }, + "numOfGameActions": { + "pc": "全消数", + "hold": "暂存数", + "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": "取消", + "submit": "确定", + "ok": "彳亍" + }, + "errors": { + "connection": "连接错误: ${code} ${message}", + "noSuchUser": "没有这样的用户", + "noSuchUserSub": "检查用户名的拼写是否错误,也许用户不存在", + "discordNotAssigned": "没有用户绑定到该Discord ID", + "discordNotAssignedSub": "您必须输入合法的ID", + "history": "此玩家没有历史", + "actionSuggestion": "你也许想", + "p1nkl0bst3rTLmatches": "没有比赛", + "clientException": "连接不到网络", + "forbidden": "你的IP地址被封禁", + "forbiddenSub": "请关闭您的VPN。若问题仍然存在,请联系 $nickname", + "tooManyRequests": "您申请的请求过多", + "tooManyRequestsSub": "等一会再试吧", + "internal": "tetr.io 似乎出错了", + "internalSub": "osk,也许,要被", + "internalWebVersion": "tetr.io 或者 oskware_bridge 似乎出错了", + "internalWebVersionSub": "如果 osk 说没有什么问题,请让dan63047知道", + "oskwareBridge": "oskware_bridge 似乎出错了", + "oskwareBridgeSub": "请让 dan63047 知道", + "p1nkl0bst3rForbidden": "第三方API封禁了你的IP地址", + "p1nkl0bst3rTooManyRequests": "第三方API……太多请求了。", + "p1nkl0bst3rinternal": "p1nkl0bst3r 那边似乎出错了", + "p1nkl0bst3rinternalWebVersion": "p1nkl0bst3r (或 on oskware_bridge, 其实我并不知道) 那边似乎出错了", + "replayAlreadySaved": "你已保存此回放", + "replayExpired": "回放已过期", + "replayRejected": "第三方API封禁了你的IP地址" + }, + "countries(map)": { + "": "无", + "AF": "阿富汗", + "AX": "奥兰群岛", + "AL": "阿尔巴尼亚", + "DZ": "阿尔及利亚", + "AS": "美属萨摩亚", + "AD": "安道尔", + "AO": "安哥拉", + "AI": "安圭拉", + "AQ": "南极洲", + "AG": "安提瓜和巴布达", + "AR": "阿根廷", + "AM": "亚美尼亚", + "AW": "阿鲁巴", + "AU": "澳大利亚", + "AT": "奥地利", + "AZ": "阿塞拜疆", + "BS": "巴哈马", + "BH": "巴林", + "BD": "孟加拉国", + "BB": "巴巴多斯", + "BY": "白俄罗斯", + "BE": "比利时", + "BZ": "伯利兹", + "BJ": "贝宁", + "BM": "百慕大", + "BT": "不丹", + "BO": "玻利维亚多民族国", + "BA": "波斯尼亚和黑塞哥维那", + "BW": "博茨瓦纳", + "BV": "布韦岛", + "BR": "巴西", + "IO": "英属印度洋领地", + "BN": "文莱达鲁萨兰国", + "BG": "保加利亚", + "BF": "布基纳法索", + "BI": "布隆迪", + "KH": "柬埔寨", + "CM": "喀麦隆", + "CA": "加拿大", + "CV": "佛得角", + "BQ": "荷兰加勒比区", + "KY": "开曼群岛", + "CF": "中非", + "TD": "乍得", + "CL": "智利", + "CN": "中国", + "CX": "圣诞岛", + "CC": "科科斯(基林)群岛", + "CO": "哥伦比亚", + "KM": "科摩罗", + "CG": "刚果(布)", + "CD": "刚果(金)/民主刚果", + "CK": "库克群岛", + "CR": "哥斯达黎加", + "CI": "科特迪瓦", + "HR": "克罗地亚", + "CU": "古巴", + "CW": "库拉索", + "CY": "塞浦路斯", + "CZ": "捷克", + "DK": "丹麦", + "DJ": "吉布提", + "DM": "多米尼加", + "DO": "多米尼加共和国", + "EC": "厄瓜多尔", + "EG": "埃及", + "SV": "萨尔瓦多", + "GB-ENG": "英格兰", + "GQ": "赤道几内亚", + "ER": "厄立特里亚", + "EE": "爱沙尼亚", + "ET": "埃塞俄比亚", + "EU": "欧洲", + "FK": "福克兰群岛/马尔维纳斯群岛", + "FO": "法罗群岛", + "FJ": "斐济", + "FI": "芬兰", + "FR": "法国", + "GF": "法属圭亚那", + "PF": "法属波利尼西亚", + "TF": "法属南部领地", + "GA": "加蓬", + "GM": "冈比亚", + "GE": "格鲁吉亚", + "DE": "德国", + "GH": "加纳", + "GI": "直布罗陀", + "GR": "希腊", + "GL": "格陵兰岛", + "GD": "格林纳达", + "GP": "瓜德罗普岛", + "GU": "关岛", + "GT": "危地马拉", + "GG": "根西岛", + "GN": "几内亚", + "GW": "几内亚比绍", + "GY": "圭亚那", + "HT": "海地", + "HM": "赫德岛和麦克唐纳群岛", + "VA": "梵蒂冈", + "HN": "洪都拉斯", + "HK": "中国香港", + "HU": "匈牙利", + "IS": "冰岛", + "IN": "印度", + "ID": "印度尼西亚", + "IR": "伊朗", + "IQ": "伊拉克", + "IE": "爱尔兰", + "IM": "马恩岛", + "IL": "以色列", + "IT": "意大利", + "JM": "牙买加", + "JP": "日本", + "JE": "Jersey", + "JO": "约旦", + "KZ": "哈萨克斯坦", + "KE": "肯尼亚", + "KI": "基里巴斯", + "KP": "朝鲜", + "KR": "韩国", + "XK": "科索沃", + "KW": "科威特", + "KG": "吉尔吉斯斯坦", + "LA": "老挝", + "LV": "拉脱维亚", + "LB": "黎巴嫩", + "LS": "莱索托", + "LR": "利比里亚", + "LY": "利比亚", + "LI": "列支敦士登", + "LT": "立陶宛", + "LU": "卢森堡", + "MO": "中国澳门", + "MK": "马其顿", + "MG": "马达加斯加", + "MW": "马拉维", + "MY": "马来西亚", + "MV": "马尔代夫", + "ML": "马里", + "MT": "马耳他", + "MH": "马绍尔群岛", + "MQ": "马提尼克岛", + "MR": "毛里塔尼亚", + "MU": "毛里求斯", + "YT": "马约特岛", + "MX": "墨西哥", + "FM": "密克罗尼西亚联邦", + "MD": "摩尔多瓦共和国", + "MC": "摩纳哥", + "ME": "黑山", + "MA": "摩洛哥", + "MN": "蒙古", + "MS": "蒙特塞拉特", + "MZ": "莫桑比克", + "MM": "缅甸", + "NA": "纳米比亚", + "NR": "瑙鲁", + "NP": "尼泊尔", + "NL": "尼德兰", + "AN": "荷属安的列斯", + "NC": "新喀里多尼亚", + "NZ": "新西兰", + "NI": "尼加拉瓜", + "NE": "尼日尔", + "NG": "尼日利亚", + "NU": "纽埃", + "NF": "诺福克岛", + "GB-NIR": "北爱尔兰", + "MP": "北马里亚纳群岛", + "NO": "挪威", + "OM": "阿曼", + "PK": "巴基斯坦", + "PW": "帕劳", + "PS": "巴勒斯坦", + "PA": "巴拿马", + "PG": "巴布亚新几内亚", + "PY": "巴拉圭", + "PE": "秘鲁", + "PH": "菲律宾", + "PN": "皮特凯恩", + "PL": "波兰", + "PT": "葡萄牙", + "PR": "波多黎各", + "QA": "卡塔尔", + "RE": "留尼汪", + "RO": "罗马尼亚", + "RU": "俄罗斯联邦", + "RW": "卢旺达", + "BL": "圣巴泰勒米", + "SH": "圣赫勒拿,阿森松和特里斯坦-达库尼亚", + "KN": "圣基茨和尼维斯", + "LC": "圣卢西亚", + "MF": "圣马丁", + "PM": "圣皮埃尔和密克隆群岛", + "VC": "圣文森特和格林纳丁斯", + "WS": "萨摩亚", + "SM": "圣马力诺", + "ST": "圣多美和普林西比", + "SA": "沙特阿拉伯", + "GB-SCT": "苏格兰", + "SN": "塞内加尔", + "RS": "塞尔维亚", + "SC": "塞舌尔", + "SL": "塞拉利昂", + "SG": "新加坡", + "SX": "荷属圣马丁", + "SK": "斯洛伐克", + "SI": "斯洛文尼亚", + "SB": "所罗门群岛", + "SO": "索马里", + "ZA": "南非", + "GS": "南乔治亚和南桑威奇群岛", + "SS": "南苏丹", + "ES": "西班牙", + "LK": "斯里兰卡", + "SD": "苏丹", + "SR": "苏里南", + "SJ": "斯瓦尔巴和扬马延群岛", + "SZ": "斯威士兰", + "SE": "瑞典", + "CH": "瑞士", + "SY": "叙利亚", + "TW": "中国台湾", + "TJ": "塔吉克斯坦", + "TZ": "坦桑尼亚", + "TH": "泰国", + "TL": "东帝汶", + "TG": "多哥", + "TK": "托克劳", + "TO": "汤加", + "TT": "特立尼达和多巴哥", + "TN": "突尼斯", + "TR": "土耳其", + "TM": "土库曼斯坦", + "TC": "特克斯和凯科斯群岛", + "TV": "图瓦卢", + "UG": "乌干达", + "UA": "乌克兰", + "AE": "阿拉伯联合酋长国", + "GB": "英国", + "US": "美国", + "UY": "乌拉圭", + "UM": "美国小岛屿", + "UZ": "乌兹别克斯坦", + "VU": "瓦努阿图", + "VE": "委内瑞拉玻利瓦尔共和国", + "VN": "越南", + "VG": "英属维尔京群岛", + "VI": "美属维尔京群岛", + "GB-WLS": "威尔士", + "WF": "瓦利斯和富图纳群岛", + "EH": "西撒哈拉", + "YE": "也门", + "ZM": "赞比亚", + "ZW": "津巴布韦", + "XX": "未知", + "XM": "月球" + } +} From 969cda5e9cb464b87a2c2882742f7185408d947e Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:53:35 +0800 Subject: [PATCH 08/86] Update strings_zh-hans.i18n.json --- res/i18n/strings_zh-hans.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings_zh-hans.i18n.json b/res/i18n/strings_zh-hans.i18n.json index dbc2d04..ec39a3f 100644 --- a/res/i18n/strings_zh-hans.i18n.json +++ b/res/i18n/strings_zh-hans.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "英语 (English)", "ru": "俄语 (Русский)", - "zh-cn": "简体中文" + "zh-hans": "简体中文" }, "tetraLeague": "Tetra联赛", "tlRecords": "Tetra联赛记录", From 6ca96b16b3c7af20017bf5d17d0d7294f0e976a5 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:56:28 +0800 Subject: [PATCH 09/86] Update strings.i18n.json --- res/i18n/strings.i18n.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 1f3ba9a..6e44823 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -1,7 +1,8 @@ { "locales(map)": { "en": "English", - "ru": "Russian (Русский)" + "ru": "Russian (Русский)", + "zh-hans": "Chinese Simplified (简体中文)" }, "tetraLeague": "Tetra League", "tlRecords": "TL Records", @@ -662,4 +663,4 @@ "XX": "Unknown", "XM": "The Moon" } - } \ No newline at end of file + } From 02c75f24549a4718b60972edbfef4264a3ab2363 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:56:35 +0800 Subject: [PATCH 10/86] Update strings_ru.i18n.json --- res/i18n/strings_ru.i18n.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 5f48489..bc00d8d 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -1,7 +1,8 @@ { "locales(map)": { "en": "Английский (English)", - "ru": "Русский" + "ru": "Русский", + "zh-hans": "Упрощенный Китайский (简体中文)" }, "tetraLeague": "Тетра Лига", "tlRecords": "Матчи ТЛ", @@ -662,4 +663,4 @@ "XX": "Неизвестно", "XM": "Луна" } - } \ No newline at end of file + } From ac5913594c9a92341ba7cca902c097dcd7933f27 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:10:18 +0800 Subject: [PATCH 11/86] Update strings_zh-hans.i18n.json --- res/i18n/strings_zh-hans.i18n.json | 54 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/res/i18n/strings_zh-hans.i18n.json b/res/i18n/strings_zh-hans.i18n.json index ec39a3f..01a56da 100644 --- a/res/i18n/strings_zh-hans.i18n.json +++ b/res/i18n/strings_zh-hans.i18n.json @@ -21,7 +21,7 @@ "news": "新闻", "newsParts": { "leaderboardStart": "取得 ", - "leaderboardMiddle": "on ", + "leaderboardMiddle": "于 ", "personalbest": "在 ", "personalbestMiddle": " 中取得了新的个人最好成绩 ", "badgeStart": "获得勋章 ", @@ -75,7 +75,7 @@ "bigRedBadStanding": "信誉不佳", "copiedToClipboard": "已复制", "playerRoleAccount": "账号", - "wasFromBeginning": "that was from very beginning", + "wasFromBeginning": "在很久很久以前", "created": "创建于", "botCreatedBy": "", "notSupporter": "非会员", @@ -99,7 +99,7 @@ "numOfdefeats": "~${losses} 场败局", "demotionOnNextLoss": "下一次失败即可掉段", "nerdStats": "详细信息", - "playersYouTrack": "Players you track", + "playersYouTrack": "跟踪", "formula": "公式", "exactValue": "实际值", "neverPlayedTL": "此用户没有参与Tetra联赛", @@ -117,34 +117,34 @@ "other": "带着 $n 个模组" }, "exportDB": "导出本地数据", - "exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.", - "desktopExportAlertTitle": "Desktop export", - "desktopExportText": "It seems like you using this app on desktop. Check your documents folder, you should find \"TetraStats.db\". Copy it somewhere", - "androidExportAlertTitle": "Android export", + "exportDBDescription": "它包含跟踪的玩家的状态和Tetra联赛记录和跟踪列表。", + "desktopExportAlertTitle": "桌面导出", + "desktopExportText": "您好像在使用桌面版。请查看你的“文档”文件夹,你应该能找到“TetraStats.db”。把它复制到一个地方", + "androidExportAlertTitle": "安卓导出", "androidExportText": "导出成功\n${exportedDB}", "importDB": "导入本地数据", - "importDBDescription": "Restore your backup. Notice that already stored database will be overwritten.", + "importDBDescription": "恢复您的备份。请注意,已存储的数据库将被覆盖。", "importWrongFileType": "文件类型错误", - "importCancelled": "Operation was cancelled", + "importCancelled": "已取消", "importSuccess": "导入成功", "yourID": "你的 TETR.IO 用户", "yourIDAlertTitle": "你的 TETR.IO 昵称", "yourIDText": "当程序加载,它将显示此用户的数据", "language": "语言", "updateInBackground": "自动升级数据", - "updateInBackgroundDescription": "While Tetra Stats is running, it can update stats of the current player when cache expires", + "updateInBackgroundDescription": "当 Tetra Stats 运行时,它可以在缓存过期时更新当前玩家的统计数据", "customization": "自定义", - "customizationDescription": "Change appearance of different things in Tetra Stats UI", - "oskKagari": "Osk Kagari gimmick", - "oskKagariDescription": "If on, osk's rank on main view will be rendered as :kagari:", + "customizationDescription": "更改 Tetra Stats UI 中不同事物的外观", + "oskKagari": "osk 特有的 Kagari 段位", + "oskKagariDescription": "如果打开,主视图上的 osk 段位将显示为 :kagari:", "AccentColor": "主题色", - "AccentColorDescription": "Almost all interactive UI elements highlighted with this color", + "AccentColorDescription": "几乎所有交互式 UI 元素都用此颜色突出显示", "timestamps": "时间", - "timestampsDescription": "You can choose, in which way timestamps shows time", + "timestampsDescription": "您可以选择显示时间的方式", "timestampsAbsoluteGMT": "绝对 (GMT)", "timestampsAbsoluteLocalTime": "绝对 (你的时区)", "timestampsRelative": "相对", - "rating": "Main representation of rating", + "rating": "评级主要表现", "ratingDescription": "TR 不是线性的,而 Glicko 没有边界,百分位数易挥发", "ratingLBposition": "LB 位置", "sheetbotGraphs": "Sheetbot式雷达图", @@ -152,7 +152,7 @@ "lbStats": "显示基于排行榜的数据", "lbStatsDescription": "这会影响加载时间,但允许您通过统计数据查看排行榜上的位置并与平均值进行比较", "aboutApp": "关于", - "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", + "aboutAppText": "${appName} (${packageName}) ${version} 版 Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy", "stateViewTitle": "${nickname} 在 ${date}", "statesViewTitle": "${nickname} 的 ${number} 个状态", "matchesViewTitle": "${nickname} 的Tetra联赛历史", @@ -161,23 +161,23 @@ "matchRemoved": "成功移除 ${date} 的比赛!", "viewAllMatches": "查看所有比赛", "trackedPlayersViewTitle": "获取的数据", - "trackedPlayersZeroEntrys": "列表为空。 Press \"Track\" button in previous view to add current player here", + "trackedPlayersZeroEntrys": "列表为空。 点击 “添加到跟踪列表” 可以将玩家放在这里", "trackedPlayersOneEntry": "只有 1 个玩家", "trackedPlayersManyEntrys": "${numberOfPlayers} 个玩家", "trackedPlayersEntry": "${nickname}:${numberOfStates} 个状态", "trackedPlayersDescription": "从 ${firstStateDate} 到 ${lastStateDate}", - "trackedPlayersStatesDeleted": "${nickname} states was removed from database!", + "trackedPlayersStatesDeleted": "成功从数据库中移除 ${nickname} 个状态!", "duplicatedFix": "删除重复的 TL 匹配项", - "compressDB": "Compress DB", - "SpaceSaved": "Space saved: ${size}", + "compressDB": "压缩数据库", + "SpaceSaved": "保存空白:${size}", "averageXrank": "平均 ${rankLetter} 段", "vs": "vs", "inTLmatch": "在Tetra联赛中", "downloadReplay": "下载 .ttrm 回放", "openReplay": "在 TETR.IO 打开回放", "replaySaved": "已保存回放至 ${path}", - "match": "Match", - "timeWeightedmatch": "Match (time-weighted)", + "match": "比赛", + "timeWeightedmatch": "比赛(时间加权)", "roundNumber": "第 $n 回合", "statsFor": "数据:", "numberOfRounds": "回合数", @@ -185,10 +185,10 @@ "roundLength": "回合时长", "matchStats": "比赛数据", "timeWeightedmatchStats": "时间加权比赛数据", - "replayIssue": "Can't process replay", + "replayIssue": "无法处理回放", "matchIsTooOld": "无回放", "winner": "赢家", - "registred": "Registred", + "registred": "注册日期", "playedTL": "游玩过Tetra联赛", "winChance": "胜利机会", "byGlicko": "靠Glicko", @@ -252,7 +252,7 @@ }, "statCellNum": { "xpLevel": "XP等级", - "xpProgress": "Progress to next level", + "xpProgress": "到下一等级的进度", "xpFrom0ToLevel": "从 0 到 $n 等级的进度", "xpLeft": "XP 还有", "hoursPlayed": "小时游玩", @@ -284,7 +284,7 @@ "tr": "Tetra分数", "rd": "偏移值", "app": "每块发送垃圾行数", - "appDescription": "(Abbreviated as APP) Main efficiency metric. Tells how many attack you producing per piece", + "appDescription": "(简称APP) 主要效率指标。表示玩家每块可以发动多少次攻击", "vsapmDescription": "Basically, tells how much and how efficient you using garbage in your attacks", "dss": "每秒\n挖掘", "dssDescription": "(Abbreviated as DS/S) Downstack per Second measures how many garbage lines you clear in a second.", From 99fa3fc59b562aeabf167e8d6324df609e3ec687 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:50:52 +0800 Subject: [PATCH 12/86] Update strings_zh-hans.i18n.json --- res/i18n/strings_zh-hans.i18n.json | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/res/i18n/strings_zh-hans.i18n.json b/res/i18n/strings_zh-hans.i18n.json index 01a56da..16368e1 100644 --- a/res/i18n/strings_zh-hans.i18n.json +++ b/res/i18n/strings_zh-hans.i18n.json @@ -152,7 +152,7 @@ "lbStats": "显示基于排行榜的数据", "lbStatsDescription": "这会影响加载时间,但允许您通过统计数据查看排行榜上的位置并与平均值进行比较", "aboutApp": "关于", - "aboutAppText": "${appName} (${packageName}) ${version} 版 Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy", + "aboutAppText": "${appName} (${packageName}) ${version} 版 Build ${buildNumber}\n\n由 dan63047 制作\n由 kerrmunism 提供公式\n由 p1nkl0bst3r 提供历史\nTETR.IO 回放抓取器 API 由 szy 制作", "stateViewTitle": "${nickname} 在 ${date}", "statesViewTitle": "${nickname} 的 ${number} 个状态", "matchesViewTitle": "${nickname} 的Tetra联赛历史", @@ -193,7 +193,7 @@ "winChance": "胜利机会", "byGlicko": "靠Glicko", "byEstTR": "靠预测TR", - "compareViewNoValues": "请输入用户名,用户IO,APM-PPS-VS值 (分隔符不重要,只需要顺序)或者$avgR(R是一个段位)到两个Please, enter username, user ID, APM-PPS-VS values (divider doesn't matter, only order matter) or $avgR (where R is rank) to both fields", + "compareViewNoValues": "请输入用户名,用户IO,APM-PPS-VS值 (分隔符不重要,只需要顺序)或者$avgR(R是一个段位)到两个字段", "compareViewWrongValue": "获取 ${value} 失败", "mostRecentOne": "最接近的", "yes": "是", @@ -242,13 +242,13 @@ "highestValues": "最大值", "forPlayer": "来自用户 $username", "currentAxis": "$axis 轴:", - "p1nkl0bst3rAlert": "That data was retrived from third party API maintained by p1nkl0bst3r", - "notForWeb": "Function is not available for web version", + "p1nkl0bst3rAlert": "该数据是从 p1nkl0bst3r 维护的第三方 API 中检索的", + "notForWeb": "浏览器版本暂不支持函数", "graphs": { - "attack": "Attack", - "speed": "Speed", - "defense": "Defense", - "cheese": "Cheese" + "attack": "攻击", + "speed": "速度", + "defense": "防御", + "cheese": "奶酪层" }, "statCellNum": { "xpLevel": "XP等级", @@ -284,22 +284,22 @@ "tr": "Tetra分数", "rd": "偏移值", "app": "每块发送垃圾行数", - "appDescription": "(简称APP) 主要效率指标。表示玩家每块可以发动多少次攻击", - "vsapmDescription": "Basically, tells how much and how efficient you using garbage in your attacks", + "appDescription": "(Attack per Piece, 简称APP) 主要效率指标。表示玩家每块可以发动多少次攻击", + "vsapmDescription": "基本上可以告诉你在攻击中利用垃圾行的效率", "dss": "每秒\n挖掘", - "dssDescription": "(Abbreviated as DS/S) Downstack per Second measures how many garbage lines you clear in a second.", + "dssDescription": "(Downstack per Second, 简称 DS/S) 测量一秒钟内清除多少条垃圾行。", "dsp": "每块\n挖掘", - "dspDescription": "(Abbreviated as DS/P) Downstack per Piece measures how many garbage lines you clear per piece.", + "dspDescription": "(Downstack per Piece, 简称 DS/P) 测量每一块清除多少条垃圾行。", "appdsp": "APP + DS/P", - "appdspDescription": "Just a sum of Attack per Piece and Downstack per Piece.", - "cheese": "奶酪层\n指数", - "cheeseDescription": "(Abbreviated as Cheese) Cheese Index is an approximation how much clean / cheese garbage player sends. Lower = more clean. Higher = more cheese.\nInvented by kerrmunism", + "appdspDescription": "只是每块发送垃圾行数与每块挖掘之和。", + "cheese": "垃圾行\n混乱指数", + "cheeseDescription": "(Cheese Index, 简称Cheese) 是玩家发出的垃圾行有多整齐/混乱的近似值。低数据代表整齐,高数据代表混乱。\n由 kerrmunism 发明", "gbe": "垃圾行\n效率", - "gbeDescription": "(Abbreviated as Gb Eff.) Garbage Efficiency measures how well player uses their garbage. Higher = better or they use their garbage more. Lower = they mostly send their garbage back at cheese or rarely clear garbage.\nInvented by Zepheniah and Dragonboy.", + "gbeDescription": "(Garbage Efficity, 简称Gb Eff.) 测量玩家如何很好地利用他们收到的垃圾行。高数据代表更好或者他们更多地用TA的垃圾行,低数据代表TA大多将垃圾行送回奶酪层,或者很少清理垃圾行。\n由 Zepheniah 与 Dragonboy 发明。", "nyaapp": "加权\nAPP", - "nyaappDescription": "(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.", + "nyaappDescription": "(Weighted APP, 简称wAPP) 在本质上是在衡量您在保持高 APP 的同时发送奶酪的能力。\n由 Wertj 发明。", "area": "面积", - "areaDescription": "How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections", + "areaDescription": 如果排除 Cheese 和 vs/apm 部分,您的形状在图表上占据了多少空间", "estOfTR": "预测 TR", "estOfTRShort": "预测 TR", "accOfEst": "预测实际差量", @@ -309,10 +309,10 @@ "user": "用户", "banned": "封禁", "bot": "机器人", - "sysop": "System operator", + "sysop": "系统管理员", "admin": "管理员", - "mod": "Moderator", - "halfmod": "Community moderator", + "mod": "管理员", + "halfmod": "社区管理员", "anon": "匿名" }, "numOfGameActions": { @@ -428,8 +428,8 @@ "CC": "科科斯(基林)群岛", "CO": "哥伦比亚", "KM": "科摩罗", - "CG": "刚果(布)", - "CD": "刚果(金)/民主刚果", + "CG": "刚果(布)/刚果共和国", + "CD": "刚果(金)/刚果民主共和国", "CK": "库克群岛", "CR": "哥斯达黎加", "CI": "科特迪瓦", From 498a05008823b2b3d3a67a69183f2f3fde8ac3a3 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:57:22 +0800 Subject: [PATCH 13/86] Update and rename strings_zh-hans.i18n.json to strings_zh.i18n.json --- res/i18n/{strings_zh-hans.i18n.json => strings_zh.i18n.json} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename res/i18n/{strings_zh-hans.i18n.json => strings_zh.i18n.json} (99%) diff --git a/res/i18n/strings_zh-hans.i18n.json b/res/i18n/strings_zh.i18n.json similarity index 99% rename from res/i18n/strings_zh-hans.i18n.json rename to res/i18n/strings_zh.i18n.json index 16368e1..0e84890 100644 --- a/res/i18n/strings_zh-hans.i18n.json +++ b/res/i18n/strings_zh.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "英语 (English)", "ru": "俄语 (Русский)", - "zh-hans": "简体中文" + "zh": "中文" }, "tetraLeague": "Tetra联赛", "tlRecords": "Tetra联赛记录", From ee24e8b873e44a09b7ac53cf1dcaffa3e354e294 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:59:02 +0800 Subject: [PATCH 14/86] Update strings_ru.i18n.json --- res/i18n/strings_ru.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index bc00d8d..c2c79fb 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "Английский (English)", "ru": "Русский", - "zh-hans": "Упрощенный Китайский (简体中文)" + "zh": "Китайский (中文)" }, "tetraLeague": "Тетра Лига", "tlRecords": "Матчи ТЛ", From b193b22e1235eb23d0025ee7564928c95322b801 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:59:18 +0800 Subject: [PATCH 15/86] Update strings.i18n.json --- res/i18n/strings.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 6e44823..c2e615c 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "English", "ru": "Russian (Русский)", - "zh-hans": "Chinese Simplified (简体中文)" + "zh": "Chinese (简体中文)" }, "tetraLeague": "Tetra League", "tlRecords": "TL Records", From 3043e1c1dda6b0f79f7095b9b32d997457b5f786 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:10:04 +0800 Subject: [PATCH 16/86] Added launguage zh-cn --- res/i18n/strings.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index c2e615c..cbb466b 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "English", "ru": "Russian (Русский)", - "zh": "Chinese (简体中文)" + "zh-cn": "Simplified Chinese (简体中文)" }, "tetraLeague": "Tetra League", "tlRecords": "TL Records", From b015e495a99333764ba1f7e02079c54e571d77a2 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:11:03 +0800 Subject: [PATCH 17/86] Added a launguage zh-cn --- res/i18n/strings_ru.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index c2c79fb..01f144c 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "Английский (English)", "ru": "Русский", - "zh": "Китайский (中文)" + "zh-cn": "Упрощенный Китайский (简体中文)" }, "tetraLeague": "Тетра Лига", "tlRecords": "Матчи ТЛ", From 6ad49a9abe232d5f22693b9710e1dbecd01e69c4 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:11:41 +0800 Subject: [PATCH 18/86] Update strings_zh.i18n.json --- res/i18n/strings_zh.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings_zh.i18n.json b/res/i18n/strings_zh.i18n.json index 0e84890..4c6974e 100644 --- a/res/i18n/strings_zh.i18n.json +++ b/res/i18n/strings_zh.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "英语 (English)", "ru": "俄语 (Русский)", - "zh": "中文" + "zh-cn": "简体中文" }, "tetraLeague": "Tetra联赛", "tlRecords": "Tetra联赛记录", From 017ee753374042b00fe7731e260253b6ffd0b484 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:11:54 +0800 Subject: [PATCH 19/86] Rename strings_zh.i18n.json to strings_zh-cn.i18n.json --- res/i18n/{strings_zh.i18n.json => strings_zh-cn.i18n.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename res/i18n/{strings_zh.i18n.json => strings_zh-cn.i18n.json} (100%) diff --git a/res/i18n/strings_zh.i18n.json b/res/i18n/strings_zh-cn.i18n.json similarity index 100% rename from res/i18n/strings_zh.i18n.json rename to res/i18n/strings_zh-cn.i18n.json From 27034de084461850ccf443915f1a2c3e2c7b1625 Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:07:03 +0800 Subject: [PATCH 20/86] Whoops --- res/i18n/strings_zh-cn.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings_zh-cn.i18n.json b/res/i18n/strings_zh-cn.i18n.json index 4c6974e..17fcb88 100644 --- a/res/i18n/strings_zh-cn.i18n.json +++ b/res/i18n/strings_zh-cn.i18n.json @@ -299,7 +299,7 @@ "nyaapp": "加权\nAPP", "nyaappDescription": "(Weighted APP, 简称wAPP) 在本质上是在衡量您在保持高 APP 的同时发送奶酪的能力。\n由 Wertj 发明。", "area": "面积", - "areaDescription": 如果排除 Cheese 和 vs/apm 部分,您的形状在图表上占据了多少空间", + "areaDescription": "如果排除 Cheese 和 vs/apm 部分,您的形状在图表上占据了多少空间", "estOfTR": "预测 TR", "estOfTRShort": "预测 TR", "accOfEst": "预测实际差量", From 9c3a32f6f1680be381a0648625634a2ee57890a0 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 11 Sep 2024 18:24:02 +0300 Subject: [PATCH 21/86] Things to make CN locale work --- README.md | 1 + lib/gen/strings.g.dart | 1353 ++++++++++++++++- res/i18n/strings.i18n.json | 2 +- res/i18n/strings_ru.i18n.json | 2 +- ...h-cn.i18n.json => strings_zh-CN.i18n.json} | 2 +- 5 files changed, 1353 insertions(+), 7 deletions(-) rename res/i18n/{strings_zh-cn.i18n.json => strings_zh-CN.i18n.json} (99%) diff --git a/README.md b/README.md index 093c477..f6c729e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ You can [download an app](https://github.com/dan63047/TetraStats/releases), or [ # Special thanks - **kerrmunism** — formulas - **p1nkl0bst3r** — providing players history and peak TR +- **neko_ab4093** — Simplified Chinese localization - **osk** and his team — TETR.IO ## Legal note diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index a8b8209..83f9021 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -3,10 +3,10 @@ /// Original: res/i18n /// To regenerate, run: `dart run slang` /// -/// Locales: 2 -/// Strings: 1210 (605 per locale) +/// Locales: 3 +/// Strings: 1818 (606 per locale) /// -/// Built on 2024-09-04 at 20:41 UTC +/// Built on 2024-09-11 at 14:14 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -26,7 +26,8 @@ const AppLocale _baseLocale = AppLocale.en; /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { en(languageCode: 'en', build: Translations.build), - ru(languageCode: 'ru', build: _StringsRu.build); + ru(languageCode: 'ru', build: _StringsRu.build), + zhCn(languageCode: 'zh', countryCode: 'CN', build: _StringsZhCn.build); const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element @@ -151,6 +152,7 @@ class Translations implements BaseTranslations { Map get locales => { 'en': 'English', 'ru': 'Russian (Русский)', + 'zh-CN': 'Simplified Chinese (简体中文)', }; String get tetraLeague => 'Tetra League'; String get tlRecords => 'TL Records'; @@ -862,6 +864,7 @@ class _StringsRu implements Translations { @override Map get locales => { 'en': 'Английский (English)', 'ru': 'Русский', + 'zh-CN': 'Упрощенный Китайский (简体中文)', }; @override String get tetraLeague => 'Тетра Лига'; @override String get tlRecords => 'Матчи ТЛ'; @@ -1546,6 +1549,718 @@ class _StringsErrorsRu implements _StringsErrorsEn { @override String get replayRejected => 'Стороннее API заблокировало ваш IP адрес'; } +// Path: +class _StringsZhCn implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsZhCn.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.zhCn, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + @override late final _StringsZhCn _root = this; // ignore: unused_field + + // Translations + @override Map get locales => { + 'en': '英语 (English)', + 'ru': '俄语 (Русский)', + 'zh-CN': '简体中文', + }; + @override String get tetraLeague => 'Tetra联赛'; + @override String get tlRecords => 'Tetra联赛记录'; + @override String get history => '历史'; + @override String get sprint => '40行竞速'; + @override String get blitz => '闪电战'; + @override String get recent => '最近'; + @override String get recentRuns => '最近游玩局数'; + @override String blitzScore({required Object p}) => '${p} 分'; + @override String get openSPreplay => '在TETR.IO打开回放'; + @override String get downloadSPreplay => '下载回放'; + @override String get other => '其他'; + @override String get distinguishment => '区别'; + @override String get zen => '禅意模式'; + @override String get bio => '个人简介'; + @override String get news => '新闻'; + @override late final _StringsNewsPartsZhCn newsParts = _StringsNewsPartsZhCn._(_root); + @override String get openSearch => '搜索玩家'; + @override String get closeSearch => '关闭搜索'; + @override String get searchHint => '昵称,ID或Discord用户ID(需要 "ds:" 前缀)'; + @override String get refresh => '刷新'; + @override String get fetchAndsaveTLHistory => '获取玩家历史'; + @override String get fetchAndSaveOldTLmatches => '获取玩家Tetra联赛历史'; + @override String fetchAndsaveTLHistoryResult({required Object number}) => '找到 ${number} 个状态'; + @override String fetchAndSaveOldTLmatchesResult({required Object number}) => '找到 ${number} 场Tetra联赛比赛'; + @override String get showStoredData => '显示获得的数据'; + @override String get statsCalc => '统计计算器'; + @override String get settings => '设置'; + @override String get track => '添加到\n跟踪列表'; + @override String get stopTracking => '从跟踪列表\n中移除'; + @override String get becameTracked => '已添加到跟踪列表!'; + @override String get compare => '对比'; + @override String get stoppedBeingTracked => '已从跟踪列表中移除!'; + @override String get tlLeaderboard => 'Tetra联赛排行榜'; + @override String get noRecords => '无记录'; + @override String noOldRecords({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '无记录', + one: '只有 ${n} 个记录', + two: '只有 ${n} 个记录', + few: '只有 ${n} 个记录', + many: '只有 ${n} 个记录', + other: '只有 ${n} 个记录', + ); + @override String get noRecord => '只有 个记录'; + @override String get botRecord => '机器人不予参加排位赛'; + @override String get anonRecord => '匿名用户不予参加排位赛'; + @override String get notEnoughData => '没有足够的数据'; + @override String get noHistorySaved => '没有保存历史'; + @override String get pseudoTooltipHeaderInit => '将鼠标放在点上'; + @override String get pseudoTooltipFooterInit => '以查看详细信息'; + @override String obtainDate({required Object date}) => '在 ${date} 获得'; + @override String fetchDate({required Object date}) => 'Fetched ${date}'; + @override String get exactGametime => '实际游玩时长'; + @override String get bigRedBanned => '该账号封禁中'; + @override String get normalBanned => '封禁'; + @override String get bigRedBadStanding => '信誉不佳'; + @override String get copiedToClipboard => '已复制'; + @override String get playerRoleAccount => '账号'; + @override String get wasFromBeginning => '在很久很久以前'; + @override String get created => '创建于'; + @override String get botCreatedBy => ''; + @override String get notSupporter => '非会员'; + @override String get assignedManualy => '该勋章由 TETR.IO 管理员手动分配'; + @override String supporter({required Object tier}) => '会员等级 ${tier}'; + @override String comparingWith({required Object newDate, required Object oldDate}) => '${newDate} 时的数据与 ${oldDate} 比较'; + @override String get top => '前'; + @override String get topRank => '最高段位'; + @override String verdictGeneral({required Object rank, required Object verdict, required Object n}) => '比 ${rank} 段平均数据${verdict} ${n}'; + @override String get verdictBetter => '好'; + @override String get verdictWorse => '差'; + @override String get smooth => '平滑'; + @override String get postSeason => '淡季'; + @override String get seasonStarts => '下一赛即将开始于:'; + @override String get nanow => '暂未完成,敬请等待!'; + @override String seasonEnds({required Object countdown}) => '赛季将会在 ${countdown} 后结束'; + @override String get seasonEnded => 'Season has ended'; + @override String gamesUntilRanked({required Object left}) => '还有 ${left} 场比赛获取段位'; + @override String numOfVictories({required Object wins}) => '~${wins} 场胜局'; + @override String get promotionOnNextWin => '下一次胜利即可升段'; + @override String numOfdefeats({required Object losses}) => '~${losses} 场败局'; + @override String get demotionOnNextLoss => '下一次失败即可掉段'; + @override String get nerdStats => '详细信息'; + @override String get playersYouTrack => '跟踪'; + @override String get formula => '公式'; + @override String get exactValue => '实际值'; + @override String get neverPlayedTL => '此用户没有参与Tetra联赛'; + @override String get botTL => '机器人不予参加Tetra联赛'; + @override String get anonTL => '匿名用户不予参加Tetra联赛'; + @override String get quickPlay => '快速游戏'; + @override String get expert => '专家'; + @override String get withMods => '带着模组'; + @override String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '带着 ${n} 个模组', + one: '带着 ${n} 个模组', + two: '带着 ${n} 个模组', + few: '带着 ${n} 个模组', + many: '带着 ${n} 个模组', + other: '带着 ${n} 个模组', + ); + @override String get exportDB => '导出本地数据'; + @override String get exportDBDescription => '它包含跟踪的玩家的状态和Tetra联赛记录和跟踪列表。'; + @override String get desktopExportAlertTitle => '桌面导出'; + @override String get desktopExportText => '您好像在使用桌面版。请查看你的“文档”文件夹,你应该能找到“TetraStats.db”。把它复制到一个地方'; + @override String get androidExportAlertTitle => '安卓导出'; + @override String androidExportText({required Object exportedDB}) => '导出成功\n${exportedDB}'; + @override String get importDB => '导入本地数据'; + @override String get importDBDescription => '恢复您的备份。请注意,已存储的数据库将被覆盖。'; + @override String get importWrongFileType => '文件类型错误'; + @override String get importCancelled => '已取消'; + @override String get importSuccess => '导入成功'; + @override String get yourID => '你的 TETR.IO 用户'; + @override String get yourIDAlertTitle => '你的 TETR.IO 昵称'; + @override String get yourIDText => '当程序加载,它将显示此用户的数据'; + @override String get language => '语言'; + @override String get updateInBackground => '自动升级数据'; + @override String get updateInBackgroundDescription => '当 Tetra Stats 运行时,它可以在缓存过期时更新当前玩家的统计数据'; + @override String get customization => '自定义'; + @override String get customizationDescription => '更改 Tetra Stats UI 中不同事物的外观'; + @override String get oskKagari => 'osk 特有的 Kagari 段位'; + @override String get oskKagariDescription => '如果打开,主视图上的 osk 段位将显示为 :kagari:'; + @override String get AccentColor => '主题色'; + @override String get AccentColorDescription => '几乎所有交互式 UI 元素都用此颜色突出显示'; + @override String get timestamps => '时间'; + @override String get timestampsDescription => '您可以选择显示时间的方式'; + @override String get timestampsAbsoluteGMT => '绝对 (GMT)'; + @override String get timestampsAbsoluteLocalTime => '绝对 (你的时区)'; + @override String get timestampsRelative => '相对'; + @override String get rating => '评级主要表现'; + @override String get ratingDescription => 'TR 不是线性的,而 Glicko 没有边界,百分位数易挥发'; + @override String get ratingLBposition => 'LB 位置'; + @override String get sheetbotGraphs => 'Sheetbot式雷达图'; + @override String get sheetbotGraphsDescription => '若开启,雷达图上的点为负时可以出现在对面'; + @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} 版 Build ${buildNumber}\n\n由 dan63047 制作\n由 kerrmunism 提供公式\n由 p1nkl0bst3r 提供历史\nTETR.IO 回放抓取器 API 由 szy 制作'; + @override String stateViewTitle({required Object nickname, required Object date}) => '${nickname} 在 ${date}'; + @override String statesViewTitle({required Object nickname, required Object number}) => '${nickname} 的 ${number} 个状态'; + @override String matchesViewTitle({required Object nickname}) => '${nickname} 的Tetra联赛历史'; + @override String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} 次游戏'; + @override String stateRemoved({required Object date}) => '成功移除 ${date} 的状态!'; + @override String matchRemoved({required Object date}) => '成功移除 ${date} 的比赛!'; + @override String get viewAllMatches => '查看所有比赛'; + @override String get trackedPlayersViewTitle => '获取的数据'; + @override String get trackedPlayersZeroEntrys => '列表为空。 点击 “添加到跟踪列表” 可以将玩家放在这里'; + @override String get trackedPlayersOneEntry => '只有 1 个玩家'; + @override String trackedPlayersManyEntrys({required Object numberOfPlayers}) => '${numberOfPlayers} 个玩家'; + @override String trackedPlayersEntry({required Object nickname, required Object numberOfStates}) => '${nickname}:${numberOfStates} 个状态'; + @override String trackedPlayersDescription({required Object firstStateDate, required Object lastStateDate}) => '从 ${firstStateDate} 到 ${lastStateDate}'; + @override String trackedPlayersStatesDeleted({required Object nickname}) => '成功从数据库中移除 ${nickname} 个状态!'; + @override String get duplicatedFix => '删除重复的 TL 匹配项'; + @override String get compressDB => '压缩数据库'; + @override String SpaceSaved({required Object size}) => '保存空白:${size}'; + @override String averageXrank({required Object rankLetter}) => '平均 ${rankLetter} 段'; + @override String get vs => 'vs'; + @override String get inTLmatch => '在Tetra联赛中'; + @override String get downloadReplay => '下载 .ttrm 回放'; + @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 => '时间加权比赛数据'; + @override String get replayIssue => '无法处理回放'; + @override String get matchIsTooOld => '无回放'; + @override String get winner => '赢家'; + @override String get registred => '注册日期'; + @override String get playedTL => '游玩过Tetra联赛'; + @override String get winChance => '胜利机会'; + @override String get byGlicko => '靠Glicko'; + @override String get byEstTR => '靠预测TR'; + @override String compareViewNoValues({required Object avgR}) => '请输入用户名,用户IO,APM-PPS-VS值 (分隔符不重要,只需要顺序)或者${avgR}(R是一个段位)到两个字段'; + @override String compareViewWrongValue({required Object value}) => '获取 ${value} 失败'; + @override String get mostRecentOne => '最接近的'; + @override String get yes => '是'; + @override String get no => '否'; + @override String get daysLater => '天后'; + @override String get dayseBefore => '天前'; + @override String get fromBeginning => '开服'; + @override String get calc => '计算器'; + @override String get calcViewNoValues => '输入值以计算数据'; + @override String get rankAveragesViewTitle => '段位分隔符'; + @override String get sprintAndBlitsViewTitle => '竞速与闪电战平均数据'; + @override String sprintAndBlitsRelevance({required Object date}) => '数据来自${date}'; + @override String get rank => '段位'; + @override String get averages => '平均'; + @override String get lbViewZeroEntrys => '空'; + @override String get lbViewOneEntry => '只有一个玩家'; + @override String lbViewManyEntrys({required Object numberOfPlayers}) => '有 ${numberOfPlayers}'; + @override String get everyoneAverages => 'Tetra联赛散点图'; + @override String get sortBy => '排序依据'; + @override String get reversed => '反向'; + @override String get country => '地区'; + @override String rankAverages({required Object rank}) => '${rank}段位散点图'; + @override String players({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 个玩家', + one: '${n} 个玩家', + two: '${n} 个玩家', + few: '${n} 个玩家', + many: '${n} 个玩家', + other: '${n} 个玩家', + ); + @override String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(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 => '最小值'; + @override String get maximums => '最大值'; + @override String get lowestValues => '最小值'; + @override String get averageValues => '平均值'; + @override String get highestValues => '最大值'; + @override String forPlayer({required Object username}) => '来自用户 ${username}'; + @override String currentAxis({required Object axis}) => '${axis} 轴:'; + @override String get p1nkl0bst3rAlert => '该数据是从 p1nkl0bst3r 维护的第三方 API 中检索的'; + @override String get notForWeb => '浏览器版本暂不支持函数'; + @override late final _StringsGraphsZhCn graphs = _StringsGraphsZhCn._(_root); + @override late final _StringsStatCellNumZhCn statCellNum = _StringsStatCellNumZhCn._(_root); + @override Map get playerRole => { + 'user': '用户', + 'banned': '封禁', + 'bot': '机器人', + 'sysop': '系统管理员', + 'admin': '管理员', + 'mod': '管理员', + 'halfmod': '社区管理员', + 'anon': '匿名', + }; + @override late final _StringsNumOfGameActionsZhCn numOfGameActions = _StringsNumOfGameActionsZhCn._(_root); + @override late final _StringsPopupActionsZhCn popupActions = _StringsPopupActionsZhCn._(_root); + @override late final _StringsErrorsZhCn errors = _StringsErrorsZhCn._(_root); + @override Map get countries => { + '': '无', + 'AF': '阿富汗', + 'AX': '奥兰群岛', + 'AL': '阿尔巴尼亚', + 'DZ': '阿尔及利亚', + 'AS': '美属萨摩亚', + 'AD': '安道尔', + 'AO': '安哥拉', + 'AI': '安圭拉', + 'AQ': '南极洲', + 'AG': '安提瓜和巴布达', + 'AR': '阿根廷', + 'AM': '亚美尼亚', + 'AW': '阿鲁巴', + 'AU': '澳大利亚', + 'AT': '奥地利', + 'AZ': '阿塞拜疆', + 'BS': '巴哈马', + 'BH': '巴林', + 'BD': '孟加拉国', + 'BB': '巴巴多斯', + 'BY': '白俄罗斯', + 'BE': '比利时', + 'BZ': '伯利兹', + 'BJ': '贝宁', + 'BM': '百慕大', + 'BT': '不丹', + 'BO': '玻利维亚多民族国', + 'BA': '波斯尼亚和黑塞哥维那', + 'BW': '博茨瓦纳', + 'BV': '布韦岛', + 'BR': '巴西', + 'IO': '英属印度洋领地', + 'BN': '文莱达鲁萨兰国', + 'BG': '保加利亚', + 'BF': '布基纳法索', + 'BI': '布隆迪', + 'KH': '柬埔寨', + 'CM': '喀麦隆', + 'CA': '加拿大', + 'CV': '佛得角', + 'BQ': '荷兰加勒比区', + 'KY': '开曼群岛', + 'CF': '中非', + 'TD': '乍得', + 'CL': '智利', + 'CN': '中国', + 'CX': '圣诞岛', + 'CC': '科科斯(基林)群岛', + 'CO': '哥伦比亚', + 'KM': '科摩罗', + 'CG': '刚果(布)/刚果共和国', + 'CD': '刚果(金)/刚果民主共和国', + 'CK': '库克群岛', + 'CR': '哥斯达黎加', + 'CI': '科特迪瓦', + 'HR': '克罗地亚', + 'CU': '古巴', + 'CW': '库拉索', + 'CY': '塞浦路斯', + 'CZ': '捷克', + 'DK': '丹麦', + 'DJ': '吉布提', + 'DM': '多米尼加', + 'DO': '多米尼加共和国', + 'EC': '厄瓜多尔', + 'EG': '埃及', + 'SV': '萨尔瓦多', + 'GB-ENG': '英格兰', + 'GQ': '赤道几内亚', + 'ER': '厄立特里亚', + 'EE': '爱沙尼亚', + 'ET': '埃塞俄比亚', + 'EU': '欧洲', + 'FK': '福克兰群岛/马尔维纳斯群岛', + 'FO': '法罗群岛', + 'FJ': '斐济', + 'FI': '芬兰', + 'FR': '法国', + 'GF': '法属圭亚那', + 'PF': '法属波利尼西亚', + 'TF': '法属南部领地', + 'GA': '加蓬', + 'GM': '冈比亚', + 'GE': '格鲁吉亚', + 'DE': '德国', + 'GH': '加纳', + 'GI': '直布罗陀', + 'GR': '希腊', + 'GL': '格陵兰岛', + 'GD': '格林纳达', + 'GP': '瓜德罗普岛', + 'GU': '关岛', + 'GT': '危地马拉', + 'GG': '根西岛', + 'GN': '几内亚', + 'GW': '几内亚比绍', + 'GY': '圭亚那', + 'HT': '海地', + 'HM': '赫德岛和麦克唐纳群岛', + 'VA': '梵蒂冈', + 'HN': '洪都拉斯', + 'HK': '中国香港', + 'HU': '匈牙利', + 'IS': '冰岛', + 'IN': '印度', + 'ID': '印度尼西亚', + 'IR': '伊朗', + 'IQ': '伊拉克', + 'IE': '爱尔兰', + 'IM': '马恩岛', + 'IL': '以色列', + 'IT': '意大利', + 'JM': '牙买加', + 'JP': '日本', + 'JE': 'Jersey', + 'JO': '约旦', + 'KZ': '哈萨克斯坦', + 'KE': '肯尼亚', + 'KI': '基里巴斯', + 'KP': '朝鲜', + 'KR': '韩国', + 'XK': '科索沃', + 'KW': '科威特', + 'KG': '吉尔吉斯斯坦', + 'LA': '老挝', + 'LV': '拉脱维亚', + 'LB': '黎巴嫩', + 'LS': '莱索托', + 'LR': '利比里亚', + 'LY': '利比亚', + 'LI': '列支敦士登', + 'LT': '立陶宛', + 'LU': '卢森堡', + 'MO': '中国澳门', + 'MK': '马其顿', + 'MG': '马达加斯加', + 'MW': '马拉维', + 'MY': '马来西亚', + 'MV': '马尔代夫', + 'ML': '马里', + 'MT': '马耳他', + 'MH': '马绍尔群岛', + 'MQ': '马提尼克岛', + 'MR': '毛里塔尼亚', + 'MU': '毛里求斯', + 'YT': '马约特岛', + 'MX': '墨西哥', + 'FM': '密克罗尼西亚联邦', + 'MD': '摩尔多瓦共和国', + 'MC': '摩纳哥', + 'ME': '黑山', + 'MA': '摩洛哥', + 'MN': '蒙古', + 'MS': '蒙特塞拉特', + 'MZ': '莫桑比克', + 'MM': '缅甸', + 'NA': '纳米比亚', + 'NR': '瑙鲁', + 'NP': '尼泊尔', + 'NL': '尼德兰', + 'AN': '荷属安的列斯', + 'NC': '新喀里多尼亚', + 'NZ': '新西兰', + 'NI': '尼加拉瓜', + 'NE': '尼日尔', + 'NG': '尼日利亚', + 'NU': '纽埃', + 'NF': '诺福克岛', + 'GB-NIR': '北爱尔兰', + 'MP': '北马里亚纳群岛', + 'NO': '挪威', + 'OM': '阿曼', + 'PK': '巴基斯坦', + 'PW': '帕劳', + 'PS': '巴勒斯坦', + 'PA': '巴拿马', + 'PG': '巴布亚新几内亚', + 'PY': '巴拉圭', + 'PE': '秘鲁', + 'PH': '菲律宾', + 'PN': '皮特凯恩', + 'PL': '波兰', + 'PT': '葡萄牙', + 'PR': '波多黎各', + 'QA': '卡塔尔', + 'RE': '留尼汪', + 'RO': '罗马尼亚', + 'RU': '俄罗斯联邦', + 'RW': '卢旺达', + 'BL': '圣巴泰勒米', + 'SH': '圣赫勒拿,阿森松和特里斯坦-达库尼亚', + 'KN': '圣基茨和尼维斯', + 'LC': '圣卢西亚', + 'MF': '圣马丁', + 'PM': '圣皮埃尔和密克隆群岛', + 'VC': '圣文森特和格林纳丁斯', + 'WS': '萨摩亚', + 'SM': '圣马力诺', + 'ST': '圣多美和普林西比', + 'SA': '沙特阿拉伯', + 'GB-SCT': '苏格兰', + 'SN': '塞内加尔', + 'RS': '塞尔维亚', + 'SC': '塞舌尔', + 'SL': '塞拉利昂', + 'SG': '新加坡', + 'SX': '荷属圣马丁', + 'SK': '斯洛伐克', + 'SI': '斯洛文尼亚', + 'SB': '所罗门群岛', + 'SO': '索马里', + 'ZA': '南非', + 'GS': '南乔治亚和南桑威奇群岛', + 'SS': '南苏丹', + 'ES': '西班牙', + 'LK': '斯里兰卡', + 'SD': '苏丹', + 'SR': '苏里南', + 'SJ': '斯瓦尔巴和扬马延群岛', + 'SZ': '斯威士兰', + 'SE': '瑞典', + 'CH': '瑞士', + 'SY': '叙利亚', + 'TW': '中国台湾', + 'TJ': '塔吉克斯坦', + 'TZ': '坦桑尼亚', + 'TH': '泰国', + 'TL': '东帝汶', + 'TG': '多哥', + 'TK': '托克劳', + 'TO': '汤加', + 'TT': '特立尼达和多巴哥', + 'TN': '突尼斯', + 'TR': '土耳其', + 'TM': '土库曼斯坦', + 'TC': '特克斯和凯科斯群岛', + 'TV': '图瓦卢', + 'UG': '乌干达', + 'UA': '乌克兰', + 'AE': '阿拉伯联合酋长国', + 'GB': '英国', + 'US': '美国', + 'UY': '乌拉圭', + 'UM': '美国小岛屿', + 'UZ': '乌兹别克斯坦', + 'VU': '瓦努阿图', + 'VE': '委内瑞拉玻利瓦尔共和国', + 'VN': '越南', + 'VG': '英属维尔京群岛', + 'VI': '美属维尔京群岛', + 'GB-WLS': '威尔士', + 'WF': '瓦利斯和富图纳群岛', + 'EH': '西撒哈拉', + 'YE': '也门', + 'ZM': '赞比亚', + 'ZW': '津巴布韦', + 'XX': '未知', + 'XM': '月球', + }; +} + +// Path: newsParts +class _StringsNewsPartsZhCn implements _StringsNewsPartsEn { + _StringsNewsPartsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get leaderboardStart => '取得 '; + @override String get leaderboardMiddle => '于 '; + @override String get personalbest => '在 '; + @override String get personalbestMiddle => ' 中取得了新的个人最好成绩 '; + @override String get badgeStart => '获得勋章 '; + @override String get badgeEnd => ''; + @override String get rankupStart => '达成 '; + @override String rankupMiddle({required Object r}) => '${r} 段位 '; + @override String get rankupEnd => ''; + @override String get tetoSupporter => 'TETR.IO 会员'; + @override String get supporterStart => '成为了 '; + @override String get supporterGiftStart => '被赠送了 '; + @override String unknownNews({required Object type}) => '未知新闻 ${type}'; +} + +// Path: graphs +class _StringsGraphsZhCn implements _StringsGraphsEn { + _StringsGraphsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get attack => '攻击'; + @override String get speed => '速度'; + @override String get defense => '防御'; + @override String get cheese => '奶酪层'; +} + +// Path: statCellNum +class _StringsStatCellNumZhCn implements _StringsStatCellNumEn { + _StringsStatCellNumZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get xpLevel => 'XP等级'; + @override String get xpProgress => '到下一等级的进度'; + @override String xpFrom0ToLevel({required Object n}) => '从 0 到 ${n} 等级的进度'; + @override String get xpLeft => 'XP 还有'; + @override String get hoursPlayed => '小时游玩'; + @override String get onlineGames => '在线游戏场次'; + @override String get gamesWon => '获胜场次'; + @override String get totalGames => '总在线游戏场次'; + @override String get totalWon => '总在线游戏获胜场次'; + @override String get friends => '好友'; + @override String get apm => '每分\n发送垃圾行'; + @override String get vs => 'VS\n分数'; + @override String get recordLB => '名次'; + @override String get lbp => '名次'; + @override String get lbpShort => '名次'; + @override String get lbpc => '地区\n名次'; + @override String get lbpcShort => '地区名次'; + @override String get gamesPlayed => '游戏\n场次'; + @override String get gamesWonTL => '获胜\n场次'; + @override String get winrate => '胜率'; + @override String get level => '等级'; + @override String get score => '分数'; + @override String get spp => '每块\n得分'; + @override String get pieces => '放置\n块数'; + @override String get pps => '每秒\n放置块数'; + @override String get finesseFaults => '非极简\n操作'; + @override String get finessePercentage => '极简率'; + @override String get keys => '按键'; + @override String get kpp => '每块\n按键'; + @override String get kps => '每秒\n按键'; + @override String get tr => 'Tetra分数'; + @override String get rd => '偏移值'; + @override String get app => '每块发送垃圾行数'; + @override String get appDescription => '(Attack per Piece, 简称APP) 主要效率指标。表示玩家每块可以发动多少次攻击'; + @override String get vsapmDescription => '基本上可以告诉你在攻击中利用垃圾行的效率'; + @override String get dss => '每秒\n挖掘'; + @override String get dssDescription => '(Downstack per Second, 简称 DS/S) 测量一秒钟内清除多少条垃圾行。'; + @override String get dsp => '每块\n挖掘'; + @override String get dspDescription => '(Downstack per Piece, 简称 DS/P) 测量每一块清除多少条垃圾行。'; + @override String get appdsp => 'APP + DS/P'; + @override String get appdspDescription => '只是每块发送垃圾行数与每块挖掘之和。'; + @override String get cheese => '垃圾行\n混乱指数'; + @override String get cheeseDescription => '(Cheese Index, 简称Cheese) 是玩家发出的垃圾行有多整齐/混乱的近似值。低数据代表整齐,高数据代表混乱。\n由 kerrmunism 发明'; + @override String get gbe => '垃圾行\n效率'; + @override String get gbeDescription => '(Garbage Efficity, 简称Gb Eff.) 测量玩家如何很好地利用他们收到的垃圾行。高数据代表更好或者他们更多地用TA的垃圾行,低数据代表TA大多将垃圾行送回奶酪层,或者很少清理垃圾行。\n由 Zepheniah 与 Dragonboy 发明。'; + @override String get nyaapp => '加权\nAPP'; + @override String get nyaappDescription => '(Weighted APP, 简称wAPP) 在本质上是在衡量您在保持高 APP 的同时发送奶酪的能力。\n由 Wertj 发明。'; + @override String get area => '面积'; + @override String get areaDescription => '如果排除 Cheese 和 vs/apm 部分,您的形状在图表上占据了多少空间'; + @override String get estOfTR => '预测 TR'; + @override String get estOfTRShort => '预测 TR'; + @override String get accOfEst => '预测实际差量'; + @override String get accOfEstShort => '预测实际差量'; +} + +// Path: numOfGameActions +class _StringsNumOfGameActionsZhCn implements _StringsNumOfGameActionsEn { + _StringsNumOfGameActionsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get pc => '全消数'; + @override String get hold => '暂存数'; + @override String inputs({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 次按键', + one: '${n} 次按键', + two: '${n} 次按键', + few: '${n} 次按键', + many: '${n} 次按键', + other: '${n} 次按键', + ); + @override String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(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('zh'))(n, + zero: '清除了 ${n} 行', + one: '清除了 ${n} 行', + two: '清除了 ${n} 行', + few: '清除了 ${n} 行', + many: '清除了 ${n} 行', + other: '清除了 ${n} 行', + ); +} + +// Path: popupActions +class _StringsPopupActionsZhCn implements _StringsPopupActionsEn { + _StringsPopupActionsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get cancel => '取消'; + @override String get submit => '确定'; + @override String get ok => '彳亍'; +} + +// Path: errors +class _StringsErrorsZhCn implements _StringsErrorsEn { + _StringsErrorsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // 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地址被封禁'; + @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 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 (或 on oskware_bridge, 其实我并不知道) 那边似乎出错了'; + @override String get replayAlreadySaved => '你已保存此回放'; + @override String get replayExpired => '回放已过期'; + @override String get replayRejected => '第三方API封禁了你的IP地址'; +} + /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. @@ -1554,6 +2269,7 @@ extension on Translations { switch (path) { case 'locales.en': return 'English'; case 'locales.ru': return 'Russian (Русский)'; + case 'locales.zh-CN': return 'Simplified Chinese (简体中文)'; case 'tetraLeague': return 'Tetra League'; case 'tlRecords': return 'TL Records'; case 'history': return 'History'; @@ -2181,6 +2897,7 @@ extension on _StringsRu { switch (path) { case 'locales.en': return 'Английский (English)'; case 'locales.ru': return 'Русский'; + case 'locales.zh-CN': return 'Упрощенный Китайский (简体中文)'; case 'tetraLeague': return 'Тетра Лига'; case 'tlRecords': return 'Матчи ТЛ'; case 'history': return 'История'; @@ -2802,3 +3519,631 @@ extension on _StringsRu { } } } + +extension on _StringsZhCn { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'locales.en': return '英语 (English)'; + case 'locales.ru': return '俄语 (Русский)'; + case 'locales.zh-CN': return '简体中文'; + case 'tetraLeague': return 'Tetra联赛'; + case 'tlRecords': return 'Tetra联赛记录'; + case 'history': return '历史'; + case 'sprint': return '40行竞速'; + case 'blitz': return '闪电战'; + case 'recent': return '最近'; + case 'recentRuns': return '最近游玩局数'; + case 'blitzScore': return ({required Object p}) => '${p} 分'; + case 'openSPreplay': return '在TETR.IO打开回放'; + case 'downloadSPreplay': return '下载回放'; + case 'other': return '其他'; + case 'distinguishment': return '区别'; + case 'zen': return '禅意模式'; + case 'bio': return '个人简介'; + case 'news': return '新闻'; + case 'newsParts.leaderboardStart': return '取得 '; + case 'newsParts.leaderboardMiddle': return '于 '; + case 'newsParts.personalbest': return '在 '; + case 'newsParts.personalbestMiddle': return ' 中取得了新的个人最好成绩 '; + case 'newsParts.badgeStart': return '获得勋章 '; + case 'newsParts.badgeEnd': return ''; + case 'newsParts.rankupStart': return '达成 '; + case 'newsParts.rankupMiddle': return ({required Object r}) => '${r} 段位 '; + case 'newsParts.rankupEnd': return ''; + case 'newsParts.tetoSupporter': return 'TETR.IO 会员'; + case 'newsParts.supporterStart': return '成为了 '; + case 'newsParts.supporterGiftStart': return '被赠送了 '; + case 'newsParts.unknownNews': return ({required Object type}) => '未知新闻 ${type}'; + case 'openSearch': return '搜索玩家'; + case 'closeSearch': return '关闭搜索'; + case 'searchHint': return '昵称,ID或Discord用户ID(需要 "ds:" 前缀)'; + case 'refresh': return '刷新'; + case 'fetchAndsaveTLHistory': return '获取玩家历史'; + case 'fetchAndSaveOldTLmatches': return '获取玩家Tetra联赛历史'; + case 'fetchAndsaveTLHistoryResult': return ({required Object number}) => '找到 ${number} 个状态'; + case 'fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '找到 ${number} 场Tetra联赛比赛'; + case 'showStoredData': return '显示获得的数据'; + case 'statsCalc': return '统计计算器'; + case 'settings': return '设置'; + case 'track': return '添加到\n跟踪列表'; + case 'stopTracking': return '从跟踪列表\n中移除'; + case 'becameTracked': return '已添加到跟踪列表!'; + case 'compare': return '对比'; + case 'stoppedBeingTracked': return '已从跟踪列表中移除!'; + case 'tlLeaderboard': return 'Tetra联赛排行榜'; + case 'noRecords': return '无记录'; + case 'noOldRecords': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '无记录', + one: '只有 ${n} 个记录', + two: '只有 ${n} 个记录', + few: '只有 ${n} 个记录', + many: '只有 ${n} 个记录', + other: '只有 ${n} 个记录', + ); + case 'noRecord': return ({required Object n}) => '只有 ${n} 个记录'; + case 'botRecord': return '机器人不予参加排位赛'; + case 'anonRecord': return '匿名用户不予参加排位赛'; + case 'notEnoughData': return '没有足够的数据'; + case 'noHistorySaved': return '没有保存历史'; + case 'pseudoTooltipHeaderInit': return '将鼠标放在点上'; + case 'pseudoTooltipFooterInit': return '以查看详细信息'; + case 'obtainDate': return ({required Object date}) => '在 ${date} 获得'; + case 'fetchDate': return ({required Object date}) => 'Fetched ${date}'; + case 'exactGametime': return '实际游玩时长'; + case 'bigRedBanned': return '该账号封禁中'; + case 'normalBanned': return '封禁'; + case 'bigRedBadStanding': return '信誉不佳'; + case 'copiedToClipboard': return '已复制'; + case 'playerRoleAccount': return '账号'; + case 'wasFromBeginning': return '在很久很久以前'; + case 'created': return '创建于'; + case 'botCreatedBy': return ''; + case 'notSupporter': return '非会员'; + case 'assignedManualy': return '该勋章由 TETR.IO 管理员手动分配'; + case 'supporter': return ({required Object tier}) => '会员等级 ${tier}'; + case 'comparingWith': return ({required Object newDate, required Object oldDate}) => '${newDate} 时的数据与 ${oldDate} 比较'; + case 'top': return '前'; + case 'topRank': return '最高段位'; + case 'verdictGeneral': return ({required Object rank, required Object verdict, required Object n}) => '比 ${rank} 段平均数据${verdict} ${n}'; + case 'verdictBetter': return '好'; + case 'verdictWorse': return '差'; + case 'smooth': return '平滑'; + case 'postSeason': return '淡季'; + case 'seasonStarts': return '下一赛即将开始于:'; + case 'nanow': return '暂未完成,敬请等待!'; + case 'seasonEnds': return ({required Object countdown}) => '赛季将会在 ${countdown} 后结束'; + case 'seasonEnded': return 'Season has ended'; + case 'gamesUntilRanked': return ({required Object left}) => '还有 ${left} 场比赛获取段位'; + case 'numOfVictories': return ({required Object wins}) => '~${wins} 场胜局'; + case 'promotionOnNextWin': return '下一次胜利即可升段'; + case 'numOfdefeats': return ({required Object losses}) => '~${losses} 场败局'; + case 'demotionOnNextLoss': return '下一次失败即可掉段'; + case 'nerdStats': return '详细信息'; + case 'playersYouTrack': return '跟踪'; + case 'formula': return '公式'; + case 'exactValue': return '实际值'; + case 'neverPlayedTL': return '此用户没有参与Tetra联赛'; + case 'botTL': return '机器人不予参加Tetra联赛'; + case 'anonTL': return '匿名用户不予参加Tetra联赛'; + case 'quickPlay': return '快速游戏'; + case 'expert': return '专家'; + case 'withMods': return '带着模组'; + case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '带着 ${n} 个模组', + one: '带着 ${n} 个模组', + two: '带着 ${n} 个模组', + few: '带着 ${n} 个模组', + many: '带着 ${n} 个模组', + other: '带着 ${n} 个模组', + ); + case 'exportDB': return '导出本地数据'; + case 'exportDBDescription': return '它包含跟踪的玩家的状态和Tetra联赛记录和跟踪列表。'; + case 'desktopExportAlertTitle': return '桌面导出'; + case 'desktopExportText': return '您好像在使用桌面版。请查看你的“文档”文件夹,你应该能找到“TetraStats.db”。把它复制到一个地方'; + case 'androidExportAlertTitle': return '安卓导出'; + case 'androidExportText': return ({required Object exportedDB}) => '导出成功\n${exportedDB}'; + case 'importDB': return '导入本地数据'; + case 'importDBDescription': return '恢复您的备份。请注意,已存储的数据库将被覆盖。'; + case 'importWrongFileType': return '文件类型错误'; + case 'importCancelled': return '已取消'; + case 'importSuccess': return '导入成功'; + case 'yourID': return '你的 TETR.IO 用户'; + case 'yourIDAlertTitle': return '你的 TETR.IO 昵称'; + case 'yourIDText': return '当程序加载,它将显示此用户的数据'; + case 'language': return '语言'; + case 'updateInBackground': return '自动升级数据'; + case 'updateInBackgroundDescription': return '当 Tetra Stats 运行时,它可以在缓存过期时更新当前玩家的统计数据'; + case 'customization': return '自定义'; + case 'customizationDescription': return '更改 Tetra Stats UI 中不同事物的外观'; + case 'oskKagari': return 'osk 特有的 Kagari 段位'; + case 'oskKagariDescription': return '如果打开,主视图上的 osk 段位将显示为 :kagari:'; + case 'AccentColor': return '主题色'; + case 'AccentColorDescription': return '几乎所有交互式 UI 元素都用此颜色突出显示'; + case 'timestamps': return '时间'; + case 'timestampsDescription': return '您可以选择显示时间的方式'; + case 'timestampsAbsoluteGMT': return '绝对 (GMT)'; + case 'timestampsAbsoluteLocalTime': return '绝对 (你的时区)'; + case 'timestampsRelative': return '相对'; + case 'rating': return '评级主要表现'; + case 'ratingDescription': return 'TR 不是线性的,而 Glicko 没有边界,百分位数易挥发'; + case 'ratingLBposition': return 'LB 位置'; + case 'sheetbotGraphs': return 'Sheetbot式雷达图'; + case 'sheetbotGraphsDescription': 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} 版 Build ${buildNumber}\n\n由 dan63047 制作\n由 kerrmunism 提供公式\n由 p1nkl0bst3r 提供历史\nTETR.IO 回放抓取器 API 由 szy 制作'; + case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} 在 ${date}'; + case 'statesViewTitle': return ({required Object nickname, required Object number}) => '${nickname} 的 ${number} 个状态'; + case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} 的Tetra联赛历史'; + case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} 次游戏'; + case 'stateRemoved': return ({required Object date}) => '成功移除 ${date} 的状态!'; + case 'matchRemoved': return ({required Object date}) => '成功移除 ${date} 的比赛!'; + case 'viewAllMatches': return '查看所有比赛'; + case 'trackedPlayersViewTitle': return '获取的数据'; + case 'trackedPlayersZeroEntrys': return '列表为空。 点击 “添加到跟踪列表” 可以将玩家放在这里'; + case 'trackedPlayersOneEntry': return '只有 1 个玩家'; + case 'trackedPlayersManyEntrys': return ({required Object numberOfPlayers}) => '${numberOfPlayers} 个玩家'; + case 'trackedPlayersEntry': return ({required Object nickname, required Object numberOfStates}) => '${nickname}:${numberOfStates} 个状态'; + case 'trackedPlayersDescription': return ({required Object firstStateDate, required Object lastStateDate}) => '从 ${firstStateDate} 到 ${lastStateDate}'; + case 'trackedPlayersStatesDeleted': return ({required Object nickname}) => '成功从数据库中移除 ${nickname} 个状态!'; + case 'duplicatedFix': return '删除重复的 TL 匹配项'; + case 'compressDB': return '压缩数据库'; + case 'SpaceSaved': return ({required Object size}) => '保存空白:${size}'; + case 'averageXrank': return ({required Object rankLetter}) => '平均 ${rankLetter} 段'; + case 'vs': return 'vs'; + case 'inTLmatch': return '在Tetra联赛中'; + case 'downloadReplay': return '下载 .ttrm 回放'; + 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 '时间加权比赛数据'; + case 'replayIssue': return '无法处理回放'; + case 'matchIsTooOld': return '无回放'; + case 'winner': return '赢家'; + case 'registred': return '注册日期'; + case 'playedTL': return '游玩过Tetra联赛'; + case 'winChance': return '胜利机会'; + case 'byGlicko': return '靠Glicko'; + case 'byEstTR': return '靠预测TR'; + case 'compareViewNoValues': return ({required Object avgR}) => '请输入用户名,用户IO,APM-PPS-VS值 (分隔符不重要,只需要顺序)或者${avgR}(R是一个段位)到两个字段'; + case 'compareViewWrongValue': return ({required Object value}) => '获取 ${value} 失败'; + case 'mostRecentOne': return '最接近的'; + case 'yes': return '是'; + case 'no': return '否'; + case 'daysLater': return '天后'; + case 'dayseBefore': return '天前'; + case 'fromBeginning': return '开服'; + case 'calc': return '计算器'; + case 'calcViewNoValues': return '输入值以计算数据'; + case 'rankAveragesViewTitle': return '段位分隔符'; + case 'sprintAndBlitsViewTitle': return '竞速与闪电战平均数据'; + case 'sprintAndBlitsRelevance': return ({required Object date}) => '数据来自${date}'; + case 'rank': return '段位'; + case 'averages': return '平均'; + case 'lbViewZeroEntrys': return '空'; + case 'lbViewOneEntry': return '只有一个玩家'; + case 'lbViewManyEntrys': return ({required Object numberOfPlayers}) => '有 ${numberOfPlayers}'; + case 'everyoneAverages': return 'Tetra联赛散点图'; + case 'sortBy': return '排序依据'; + case 'reversed': return '反向'; + case 'country': return '地区'; + case 'rankAverages': return ({required Object rank}) => '${rank}段位散点图'; + case 'players': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 个玩家', + one: '${n} 个玩家', + two: '${n} 个玩家', + few: '${n} 个玩家', + many: '${n} 个玩家', + other: '${n} 个玩家', + ); + case 'games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(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 '最小值'; + case 'maximums': return '最大值'; + case 'lowestValues': return '最小值'; + case 'averageValues': return '平均值'; + case 'highestValues': return '最大值'; + case 'forPlayer': return ({required Object username}) => '来自用户 ${username}'; + case 'currentAxis': return ({required Object axis}) => '${axis} 轴:'; + case 'p1nkl0bst3rAlert': return '该数据是从 p1nkl0bst3r 维护的第三方 API 中检索的'; + case 'notForWeb': return '浏览器版本暂不支持函数'; + case 'graphs.attack': return '攻击'; + case 'graphs.speed': return '速度'; + case 'graphs.defense': return '防御'; + case 'graphs.cheese': return '奶酪层'; + case 'statCellNum.xpLevel': return 'XP等级'; + case 'statCellNum.xpProgress': return '到下一等级的进度'; + case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => '从 0 到 ${n} 等级的进度'; + case 'statCellNum.xpLeft': return 'XP 还有'; + case 'statCellNum.hoursPlayed': return '小时游玩'; + case 'statCellNum.onlineGames': return '在线游戏场次'; + case 'statCellNum.gamesWon': return '获胜场次'; + case 'statCellNum.totalGames': return '总在线游戏场次'; + case 'statCellNum.totalWon': return '总在线游戏获胜场次'; + case 'statCellNum.friends': return '好友'; + case 'statCellNum.apm': return '每分\n发送垃圾行'; + case 'statCellNum.vs': return 'VS\n分数'; + case 'statCellNum.recordLB': return '名次'; + case 'statCellNum.lbp': return '名次'; + case 'statCellNum.lbpShort': return '名次'; + case 'statCellNum.lbpc': return '地区\n名次'; + case 'statCellNum.lbpcShort': return '地区名次'; + case 'statCellNum.gamesPlayed': return '游戏\n场次'; + case 'statCellNum.gamesWonTL': return '获胜\n场次'; + case 'statCellNum.winrate': return '胜率'; + case 'statCellNum.level': return '等级'; + case 'statCellNum.score': return '分数'; + case 'statCellNum.spp': return '每块\n得分'; + case 'statCellNum.pieces': return '放置\n块数'; + case 'statCellNum.pps': return '每秒\n放置块数'; + case 'statCellNum.finesseFaults': return '非极简\n操作'; + case 'statCellNum.finessePercentage': return '极简率'; + case 'statCellNum.keys': return '按键'; + case 'statCellNum.kpp': return '每块\n按键'; + case 'statCellNum.kps': return '每秒\n按键'; + case 'statCellNum.tr': return 'Tetra分数'; + case 'statCellNum.rd': return '偏移值'; + case 'statCellNum.app': return '每块发送垃圾行数'; + case 'statCellNum.appDescription': return '(Attack per Piece, 简称APP) 主要效率指标。表示玩家每块可以发动多少次攻击'; + case 'statCellNum.vsapmDescription': return '基本上可以告诉你在攻击中利用垃圾行的效率'; + case 'statCellNum.dss': return '每秒\n挖掘'; + case 'statCellNum.dssDescription': return '(Downstack per Second, 简称 DS/S) 测量一秒钟内清除多少条垃圾行。'; + case 'statCellNum.dsp': return '每块\n挖掘'; + case 'statCellNum.dspDescription': return '(Downstack per Piece, 简称 DS/P) 测量每一块清除多少条垃圾行。'; + case 'statCellNum.appdsp': return 'APP + DS/P'; + case 'statCellNum.appdspDescription': return '只是每块发送垃圾行数与每块挖掘之和。'; + case 'statCellNum.cheese': return '垃圾行\n混乱指数'; + case 'statCellNum.cheeseDescription': return '(Cheese Index, 简称Cheese) 是玩家发出的垃圾行有多整齐/混乱的近似值。低数据代表整齐,高数据代表混乱。\n由 kerrmunism 发明'; + case 'statCellNum.gbe': return '垃圾行\n效率'; + case 'statCellNum.gbeDescription': return '(Garbage Efficity, 简称Gb Eff.) 测量玩家如何很好地利用他们收到的垃圾行。高数据代表更好或者他们更多地用TA的垃圾行,低数据代表TA大多将垃圾行送回奶酪层,或者很少清理垃圾行。\n由 Zepheniah 与 Dragonboy 发明。'; + case 'statCellNum.nyaapp': return '加权\nAPP'; + case 'statCellNum.nyaappDescription': return '(Weighted APP, 简称wAPP) 在本质上是在衡量您在保持高 APP 的同时发送奶酪的能力。\n由 Wertj 发明。'; + case 'statCellNum.area': return '面积'; + case 'statCellNum.areaDescription': return '如果排除 Cheese 和 vs/apm 部分,您的形状在图表上占据了多少空间'; + case 'statCellNum.estOfTR': return '预测 TR'; + case 'statCellNum.estOfTRShort': return '预测 TR'; + case 'statCellNum.accOfEst': return '预测实际差量'; + case 'statCellNum.accOfEstShort': return '预测实际差量'; + case 'playerRole.user': return '用户'; + case 'playerRole.banned': return '封禁'; + case 'playerRole.bot': return '机器人'; + case 'playerRole.sysop': return '系统管理员'; + case 'playerRole.admin': return '管理员'; + case 'playerRole.mod': return '管理员'; + case 'playerRole.halfmod': return '社区管理员'; + case 'playerRole.anon': return '匿名'; + case 'numOfGameActions.pc': return '全消数'; + case 'numOfGameActions.hold': return '暂存数'; + case 'numOfGameActions.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(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('zh'))(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('zh'))(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 '彳亍'; + 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地址被封禁'; + 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.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 (或 on oskware_bridge, 其实我并不知道) 那边似乎出错了'; + case 'errors.replayAlreadySaved': return '你已保存此回放'; + case 'errors.replayExpired': return '回放已过期'; + case 'errors.replayRejected': return '第三方API封禁了你的IP地址'; + case 'countries.': return '无'; + case 'countries.AF': return '阿富汗'; + case 'countries.AX': return '奥兰群岛'; + case 'countries.AL': return '阿尔巴尼亚'; + case 'countries.DZ': return '阿尔及利亚'; + case 'countries.AS': return '美属萨摩亚'; + case 'countries.AD': return '安道尔'; + case 'countries.AO': return '安哥拉'; + case 'countries.AI': return '安圭拉'; + case 'countries.AQ': return '南极洲'; + case 'countries.AG': return '安提瓜和巴布达'; + case 'countries.AR': return '阿根廷'; + case 'countries.AM': return '亚美尼亚'; + case 'countries.AW': return '阿鲁巴'; + case 'countries.AU': return '澳大利亚'; + case 'countries.AT': return '奥地利'; + case 'countries.AZ': return '阿塞拜疆'; + case 'countries.BS': return '巴哈马'; + case 'countries.BH': return '巴林'; + case 'countries.BD': return '孟加拉国'; + case 'countries.BB': return '巴巴多斯'; + case 'countries.BY': return '白俄罗斯'; + case 'countries.BE': return '比利时'; + case 'countries.BZ': return '伯利兹'; + case 'countries.BJ': return '贝宁'; + case 'countries.BM': return '百慕大'; + case 'countries.BT': return '不丹'; + case 'countries.BO': return '玻利维亚多民族国'; + case 'countries.BA': return '波斯尼亚和黑塞哥维那'; + case 'countries.BW': return '博茨瓦纳'; + case 'countries.BV': return '布韦岛'; + case 'countries.BR': return '巴西'; + case 'countries.IO': return '英属印度洋领地'; + case 'countries.BN': return '文莱达鲁萨兰国'; + case 'countries.BG': return '保加利亚'; + case 'countries.BF': return '布基纳法索'; + case 'countries.BI': return '布隆迪'; + case 'countries.KH': return '柬埔寨'; + case 'countries.CM': return '喀麦隆'; + case 'countries.CA': return '加拿大'; + case 'countries.CV': return '佛得角'; + case 'countries.BQ': return '荷兰加勒比区'; + case 'countries.KY': return '开曼群岛'; + case 'countries.CF': return '中非'; + case 'countries.TD': return '乍得'; + case 'countries.CL': return '智利'; + case 'countries.CN': return '中国'; + case 'countries.CX': return '圣诞岛'; + case 'countries.CC': return '科科斯(基林)群岛'; + case 'countries.CO': return '哥伦比亚'; + case 'countries.KM': return '科摩罗'; + case 'countries.CG': return '刚果(布)/刚果共和国'; + case 'countries.CD': return '刚果(金)/刚果民主共和国'; + case 'countries.CK': return '库克群岛'; + case 'countries.CR': return '哥斯达黎加'; + case 'countries.CI': return '科特迪瓦'; + case 'countries.HR': return '克罗地亚'; + case 'countries.CU': return '古巴'; + case 'countries.CW': return '库拉索'; + case 'countries.CY': return '塞浦路斯'; + case 'countries.CZ': return '捷克'; + case 'countries.DK': return '丹麦'; + case 'countries.DJ': return '吉布提'; + case 'countries.DM': return '多米尼加'; + case 'countries.DO': return '多米尼加共和国'; + case 'countries.EC': return '厄瓜多尔'; + case 'countries.EG': return '埃及'; + case 'countries.SV': return '萨尔瓦多'; + case 'countries.GB-ENG': return '英格兰'; + case 'countries.GQ': return '赤道几内亚'; + case 'countries.ER': return '厄立特里亚'; + case 'countries.EE': return '爱沙尼亚'; + case 'countries.ET': return '埃塞俄比亚'; + case 'countries.EU': return '欧洲'; + case 'countries.FK': return '福克兰群岛/马尔维纳斯群岛'; + case 'countries.FO': return '法罗群岛'; + case 'countries.FJ': return '斐济'; + case 'countries.FI': return '芬兰'; + case 'countries.FR': return '法国'; + case 'countries.GF': return '法属圭亚那'; + case 'countries.PF': return '法属波利尼西亚'; + case 'countries.TF': return '法属南部领地'; + case 'countries.GA': return '加蓬'; + case 'countries.GM': return '冈比亚'; + case 'countries.GE': return '格鲁吉亚'; + case 'countries.DE': return '德国'; + case 'countries.GH': return '加纳'; + case 'countries.GI': return '直布罗陀'; + case 'countries.GR': return '希腊'; + case 'countries.GL': return '格陵兰岛'; + case 'countries.GD': return '格林纳达'; + case 'countries.GP': return '瓜德罗普岛'; + case 'countries.GU': return '关岛'; + case 'countries.GT': return '危地马拉'; + case 'countries.GG': return '根西岛'; + case 'countries.GN': return '几内亚'; + case 'countries.GW': return '几内亚比绍'; + case 'countries.GY': return '圭亚那'; + case 'countries.HT': return '海地'; + case 'countries.HM': return '赫德岛和麦克唐纳群岛'; + case 'countries.VA': return '梵蒂冈'; + case 'countries.HN': return '洪都拉斯'; + case 'countries.HK': return '中国香港'; + case 'countries.HU': return '匈牙利'; + case 'countries.IS': return '冰岛'; + case 'countries.IN': return '印度'; + case 'countries.ID': return '印度尼西亚'; + case 'countries.IR': return '伊朗'; + case 'countries.IQ': return '伊拉克'; + case 'countries.IE': return '爱尔兰'; + case 'countries.IM': return '马恩岛'; + case 'countries.IL': return '以色列'; + case 'countries.IT': return '意大利'; + case 'countries.JM': return '牙买加'; + case 'countries.JP': return '日本'; + case 'countries.JE': return 'Jersey'; + case 'countries.JO': return '约旦'; + case 'countries.KZ': return '哈萨克斯坦'; + case 'countries.KE': return '肯尼亚'; + case 'countries.KI': return '基里巴斯'; + case 'countries.KP': return '朝鲜'; + case 'countries.KR': return '韩国'; + case 'countries.XK': return '科索沃'; + case 'countries.KW': return '科威特'; + case 'countries.KG': return '吉尔吉斯斯坦'; + case 'countries.LA': return '老挝'; + case 'countries.LV': return '拉脱维亚'; + case 'countries.LB': return '黎巴嫩'; + case 'countries.LS': return '莱索托'; + case 'countries.LR': return '利比里亚'; + case 'countries.LY': return '利比亚'; + case 'countries.LI': return '列支敦士登'; + case 'countries.LT': return '立陶宛'; + case 'countries.LU': return '卢森堡'; + case 'countries.MO': return '中国澳门'; + case 'countries.MK': return '马其顿'; + case 'countries.MG': return '马达加斯加'; + case 'countries.MW': return '马拉维'; + case 'countries.MY': return '马来西亚'; + case 'countries.MV': return '马尔代夫'; + case 'countries.ML': return '马里'; + case 'countries.MT': return '马耳他'; + case 'countries.MH': return '马绍尔群岛'; + case 'countries.MQ': return '马提尼克岛'; + case 'countries.MR': return '毛里塔尼亚'; + case 'countries.MU': return '毛里求斯'; + case 'countries.YT': return '马约特岛'; + case 'countries.MX': return '墨西哥'; + case 'countries.FM': return '密克罗尼西亚联邦'; + case 'countries.MD': return '摩尔多瓦共和国'; + case 'countries.MC': return '摩纳哥'; + case 'countries.ME': return '黑山'; + case 'countries.MA': return '摩洛哥'; + case 'countries.MN': return '蒙古'; + case 'countries.MS': return '蒙特塞拉特'; + case 'countries.MZ': return '莫桑比克'; + case 'countries.MM': return '缅甸'; + case 'countries.NA': return '纳米比亚'; + case 'countries.NR': return '瑙鲁'; + case 'countries.NP': return '尼泊尔'; + case 'countries.NL': return '尼德兰'; + case 'countries.AN': return '荷属安的列斯'; + case 'countries.NC': return '新喀里多尼亚'; + case 'countries.NZ': return '新西兰'; + case 'countries.NI': return '尼加拉瓜'; + case 'countries.NE': return '尼日尔'; + case 'countries.NG': return '尼日利亚'; + case 'countries.NU': return '纽埃'; + case 'countries.NF': return '诺福克岛'; + case 'countries.GB-NIR': return '北爱尔兰'; + case 'countries.MP': return '北马里亚纳群岛'; + case 'countries.NO': return '挪威'; + case 'countries.OM': return '阿曼'; + case 'countries.PK': return '巴基斯坦'; + case 'countries.PW': return '帕劳'; + case 'countries.PS': return '巴勒斯坦'; + case 'countries.PA': return '巴拿马'; + case 'countries.PG': return '巴布亚新几内亚'; + case 'countries.PY': return '巴拉圭'; + case 'countries.PE': return '秘鲁'; + case 'countries.PH': return '菲律宾'; + case 'countries.PN': return '皮特凯恩'; + case 'countries.PL': return '波兰'; + case 'countries.PT': return '葡萄牙'; + case 'countries.PR': return '波多黎各'; + case 'countries.QA': return '卡塔尔'; + case 'countries.RE': return '留尼汪'; + case 'countries.RO': return '罗马尼亚'; + case 'countries.RU': return '俄罗斯联邦'; + case 'countries.RW': return '卢旺达'; + case 'countries.BL': return '圣巴泰勒米'; + case 'countries.SH': return '圣赫勒拿,阿森松和特里斯坦-达库尼亚'; + case 'countries.KN': return '圣基茨和尼维斯'; + case 'countries.LC': return '圣卢西亚'; + case 'countries.MF': return '圣马丁'; + case 'countries.PM': return '圣皮埃尔和密克隆群岛'; + case 'countries.VC': return '圣文森特和格林纳丁斯'; + case 'countries.WS': return '萨摩亚'; + case 'countries.SM': return '圣马力诺'; + case 'countries.ST': return '圣多美和普林西比'; + case 'countries.SA': return '沙特阿拉伯'; + case 'countries.GB-SCT': return '苏格兰'; + case 'countries.SN': return '塞内加尔'; + case 'countries.RS': return '塞尔维亚'; + case 'countries.SC': return '塞舌尔'; + case 'countries.SL': return '塞拉利昂'; + case 'countries.SG': return '新加坡'; + case 'countries.SX': return '荷属圣马丁'; + case 'countries.SK': return '斯洛伐克'; + case 'countries.SI': return '斯洛文尼亚'; + case 'countries.SB': return '所罗门群岛'; + case 'countries.SO': return '索马里'; + case 'countries.ZA': return '南非'; + case 'countries.GS': return '南乔治亚和南桑威奇群岛'; + case 'countries.SS': return '南苏丹'; + case 'countries.ES': return '西班牙'; + case 'countries.LK': return '斯里兰卡'; + case 'countries.SD': return '苏丹'; + case 'countries.SR': return '苏里南'; + case 'countries.SJ': return '斯瓦尔巴和扬马延群岛'; + case 'countries.SZ': return '斯威士兰'; + case 'countries.SE': return '瑞典'; + case 'countries.CH': return '瑞士'; + case 'countries.SY': return '叙利亚'; + case 'countries.TW': return '中国台湾'; + case 'countries.TJ': return '塔吉克斯坦'; + case 'countries.TZ': return '坦桑尼亚'; + case 'countries.TH': return '泰国'; + case 'countries.TL': return '东帝汶'; + case 'countries.TG': return '多哥'; + case 'countries.TK': return '托克劳'; + case 'countries.TO': return '汤加'; + case 'countries.TT': return '特立尼达和多巴哥'; + case 'countries.TN': return '突尼斯'; + case 'countries.TR': return '土耳其'; + case 'countries.TM': return '土库曼斯坦'; + case 'countries.TC': return '特克斯和凯科斯群岛'; + case 'countries.TV': return '图瓦卢'; + case 'countries.UG': return '乌干达'; + case 'countries.UA': return '乌克兰'; + case 'countries.AE': return '阿拉伯联合酋长国'; + case 'countries.GB': return '英国'; + case 'countries.US': return '美国'; + case 'countries.UY': return '乌拉圭'; + case 'countries.UM': return '美国小岛屿'; + case 'countries.UZ': return '乌兹别克斯坦'; + case 'countries.VU': return '瓦努阿图'; + case 'countries.VE': return '委内瑞拉玻利瓦尔共和国'; + case 'countries.VN': return '越南'; + case 'countries.VG': return '英属维尔京群岛'; + case 'countries.VI': return '美属维尔京群岛'; + case 'countries.GB-WLS': return '威尔士'; + case 'countries.WF': return '瓦利斯和富图纳群岛'; + case 'countries.EH': return '西撒哈拉'; + case 'countries.YE': return '也门'; + case 'countries.ZM': return '赞比亚'; + case 'countries.ZW': return '津巴布韦'; + case 'countries.XX': return '未知'; + case 'countries.XM': return '月球'; + default: return null; + } + } +} diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index cbb466b..cabae6f 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "English", "ru": "Russian (Русский)", - "zh-cn": "Simplified Chinese (简体中文)" + "zh-CN": "Simplified Chinese (简体中文)" }, "tetraLeague": "Tetra League", "tlRecords": "TL Records", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 01f144c..d29f98e 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "Английский (English)", "ru": "Русский", - "zh-cn": "Упрощенный Китайский (简体中文)" + "zh-CN": "Упрощенный Китайский (简体中文)" }, "tetraLeague": "Тетра Лига", "tlRecords": "Матчи ТЛ", diff --git a/res/i18n/strings_zh-cn.i18n.json b/res/i18n/strings_zh-CN.i18n.json similarity index 99% rename from res/i18n/strings_zh-cn.i18n.json rename to res/i18n/strings_zh-CN.i18n.json index 17fcb88..bc485ec 100644 --- a/res/i18n/strings_zh-cn.i18n.json +++ b/res/i18n/strings_zh-CN.i18n.json @@ -2,7 +2,7 @@ "locales(map)": { "en": "英语 (English)", "ru": "俄语 (Русский)", - "zh-cn": "简体中文" + "zh-CN": "简体中文" }, "tetraLeague": "Tetra联赛", "tlRecords": "Tetra联赛记录", From a20a25b4ce244651f5a1e44e5a031848b76d4fb6 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 12 Sep 2024 01:41:02 +0300 Subject: [PATCH 22/86] ...i don't understand --- lib/views/main_view_tiles.dart | 58 +++++++++++++++------------------- pubspec.lock | 8 +++++ pubspec.yaml | 1 + 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 56690f9..572845b 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -40,6 +40,7 @@ import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/widgets/tl_progress_bar.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; +import 'package:transparent_image/transparent_image.dart'; var fDiff = NumberFormat("+#,###.####;-#,###.####"); late Future _data; @@ -181,11 +182,11 @@ class _MainState extends State with TickerProviderStateMixin { ), duration: Durations.long4, tween: Tween(begin: 0, end: 1), - curve: Easing.emphasizedDecelerate, + curve: Easing.standard, builder: (context, value, child) { return Container( transform: Matrix4.translationValues(-80+value*80, 0, 0), - child: child, + child: Opacity(opacity: value, child: child), ); }, ), @@ -746,6 +747,7 @@ class _DestinationHomeState extends State with SingleTickerProv late MapEntry? closestAverageSprint; late bool sprintBetterThanClosestAverage; late AnimationController _transition; + late final Animation _offsetAnimation; bool? sprintBetterThanRankAverage; bool? blitzBetterThanRankAverage; @@ -1555,13 +1557,21 @@ class _DestinationHomeState extends State with SingleTickerProv ] }; - _transition = AnimationController(vsync: this, value: 0, duration: Durations.long4); + _transition = AnimationController(vsync: this, duration: Durations.long4); - _transition.addListener((){ - setState(() { + // _transition.addListener((){ + // setState(() { - }); - }); + // }); + // }); + + _offsetAnimation = Tween( + begin: Offset.zero, + end: const Offset(1.5, 0.0), + ).animate(CurvedAnimation( + parent: _transition, + curve: Curves.elasticIn, + )); super.initState(); } @@ -1605,11 +1615,11 @@ class _DestinationHomeState extends State with SingleTickerProv return TweenAnimationBuilder( duration: Durations.long4, tween: Tween(begin: 0, end: 1), - curve: Easing.emphasizedDecelerate, + curve: Easing.standard, builder: (context, value, child) { return Container( transform: Matrix4.translationValues(0, 600-value*600, 0), - child: child, + child: Opacity(opacity: value, child: child), ); }, child: Row( @@ -1676,21 +1686,8 @@ class _DestinationHomeState extends State with SingleTickerProv SizedBox( height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, child: SingleChildScrollView( - child: DualTransitionBuilder( - animation: _transition, - forwardBuilder: (context, animation, child){ - print(animation); - return Container( - transform: Matrix4.translationValues(600-animation.value*600, 0, 0), - child: child! - ); - }, - reverseBuilder: (context, animation, child){ - return Container( - transform: Matrix4.translationValues(-600+animation.value*600, 0, 0), - child: child! - ); - }, + child: SlideTransition( + position: _offsetAnimation, child: switch (rightCard){ Cards.overview => getOverviewCard(snapshot.data!.summaries!), Cards.tetraLeague => switch (cardMod){ @@ -2257,12 +2254,11 @@ class NewUserThingy extends StatelessWidget { //clipBehavior: Clip.none, children: [ // TODO: osk banner can cause memory leak - if (player.bannerRevision != null) Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", + if (player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", + placeholder: kTransparentImage, fit: BoxFit.cover, height: 120, - errorBuilder: (context, error, stackTrace) { - return Container(); - }, + fadeInCurve: Easing.standard, fadeInDuration: Durations.long4 ), Positioned( top: player.bannerRevision != null ? 90.0 : 10.0, @@ -2272,10 +2268,8 @@ class NewUserThingy extends StatelessWidget { child: player.role == "banned" ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) : player.avatarRevision != null - ? Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", - fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { - return Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight); - }) + ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + fit: BoxFit.fitHeight, height: 128, placeholder: kTransparentImage, fadeInCurve: Easing.emphasizedDecelerate, fadeInDuration: Durations.long4) : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight), ) ), diff --git a/pubspec.lock b/pubspec.lock index e36950e..559060d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -922,6 +922,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + transparent_image: + dependency: "direct main" + description: + name: transparent_image + sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f + url: "https://pub.dev" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8b5da74..92fc8fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + transparent_image: ^2.0.1 cupertino_icons: ^1.0.2 vector_math: any sqflite: ^2.2.8+2 From 6738bcfab73d909c28b74c7bff7d2968573db69b Mon Sep 17 00:00:00 2001 From: ArsenalBastion4093 <113167850+ArsenalBastion4093@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:29:14 +0800 Subject: [PATCH 23/86] Fixing a mistake --- res/i18n/strings_zh-CN.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/i18n/strings_zh-CN.i18n.json b/res/i18n/strings_zh-CN.i18n.json index bc485ec..c0474a9 100644 --- a/res/i18n/strings_zh-CN.i18n.json +++ b/res/i18n/strings_zh-CN.i18n.json @@ -60,7 +60,7 @@ "many": "只有 $n 个记录", "other": "只有 $n 个记录" }, - "noRecord": "只有 $n 个记录", + "noRecord": "没有记录", "botRecord": "机器人不予参加排位赛", "anonRecord": "匿名用户不予参加排位赛", "notEnoughData": "没有足够的数据", From 28f0d0ad7a40d4454fa3acdb54764cf18c63c3ca Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 12 Sep 2024 23:28:55 +0300 Subject: [PATCH 24/86] Releasing the CN locale + redesign graphs --- lib/data_objects/tetra_league.dart | 63 ++++ lib/gen/strings.g.dart | 6 +- lib/main.dart | 2 +- lib/services/tetrio_crud.dart | 55 ++++ lib/views/main_view_tiles.dart | 495 +++++++++++++++++------------ lib/views/rank_averages_view.dart | 2 +- pubspec.yaml | 2 +- 7 files changed, 417 insertions(+), 208 deletions(-) diff --git a/lib/data_objects/tetra_league.dart b/lib/data_objects/tetra_league.dart index c509d07..773c9ab 100644 --- a/lib/data_objects/tetra_league.dart +++ b/lib/data_objects/tetra_league.dart @@ -151,6 +151,69 @@ class TetraLeague { pps ?? 0, vs ?? 0, decaying); + + num? getStatByEnum(Stats stat){ + switch (stat) { + case Stats.tr: + return tr; + case Stats.glicko: + return glicko; + case Stats.gxe: + return gxe; + case Stats.s1tr: + return s1tr; + case Stats.rd: + return rd; + case Stats.gp: + return gamesPlayed; + case Stats.gw: + return gamesWon; + case Stats.wr: + return winrate*100; + case Stats.apm: + return apm; + case Stats.pps: + return pps; + case Stats.vs: + return vs; + case Stats.app: + return nerdStats?.app; + case Stats.dss: + return nerdStats?.dss; + case Stats.dsp: + return nerdStats?.dsp; + case Stats.appdsp: + return nerdStats?.appdsp; + case Stats.vsapm: + return nerdStats?.vsapm; + case Stats.cheese: + return nerdStats?.cheese; + case Stats.gbe: + return nerdStats?.gbe; + case Stats.nyaapp: + return nerdStats?.nyaapp; + case Stats.area: + return nerdStats?.area; + case Stats.eTR: + return estTr?.esttr; + case Stats.acceTR: + return esttracc; + case Stats.acceTRabs: + return esttracc?.abs(); + case Stats.opener: + return playstyle?.opener; + case Stats.plonk: + return playstyle?.plonk; + case Stats.infDS: + return playstyle?.infds; + case Stats.stride: + return playstyle?.stride; + case Stats.stridemMinusPlonk: + return (playstyle?.stride??0.00) - (playstyle?.plonk??0.00); + case Stats.openerMinusInfDS: + return (playstyle?.opener??0.00) - (playstyle?.infds??0.00); + } + } Map toJson() { final Map data = {}; diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 83f9021..12ea442 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 3 /// Strings: 1818 (606 per locale) /// -/// Built on 2024-09-11 at 14:14 UTC +/// Built on 2024-09-12 at 20:23 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -1620,7 +1620,7 @@ class _StringsZhCn implements Translations { many: '只有 ${n} 个记录', other: '只有 ${n} 个记录', ); - @override String get noRecord => '只有 个记录'; + @override String get noRecord => '没有记录'; @override String get botRecord => '机器人不予参加排位赛'; @override String get anonRecord => '匿名用户不予参加排位赛'; @override String get notEnoughData => '没有足够的数据'; @@ -3580,7 +3580,7 @@ extension on _StringsZhCn { many: '只有 ${n} 个记录', other: '只有 ${n} 个记录', ); - case 'noRecord': return ({required Object n}) => '只有 ${n} 个记录'; + case 'noRecord': return '没有记录'; case 'botRecord': return '机器人不予参加排位赛'; case 'anonRecord': return '匿名用户不予参加排位赛'; case 'notEnoughData': return '没有足够的数据'; diff --git a/lib/main.dart b/lib/main.dart index a847a1e..bbba219 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 7a468c6..6520bf7 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -546,6 +546,61 @@ class TetrioService extends DB { } } + Future> fetchCutoffsHistory() async { + Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/history.csv'); + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); + List history = []; + for (List entry in csv){ + Map tr = {}; + Map glicko = {}; + Map gxe = {}; + for(int i = 0; i < ranks.length; i++){ + tr[ranks[ranks.length + i - ranks.length]] = entry[1 + i*3]; + glicko[ranks[ranks.length + i - ranks.length]] = entry[2 + i*3]; + glicko[ranks[ranks.length + i - ranks.length]] = entry[3 + i*3]; + } + history.add( + Cutoffs( + DateTime.fromMillisecondsSinceEpoch(entry[0]), + tr, + glicko, + gxe + ) + ); + } + return history; + case 404: + developer.log("fetchCutoffsHistory: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); + return []; + // if not 200 or 404 - throw a unique for each code exception + case 403: + throw P1nkl0bst3rForbidden(); + case 429: + throw P1nkl0bst3rTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + developer.log("fetchCutoffsHistory: Cutoffs are unavalable (${response.statusCode})", name: "services/tetrio_crud", error: response.statusCode); + return []; + default: + developer.log("fetchCutoffsHistory: Failed to fetch top Cutoffs", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { // If local http client fails + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet + } + } + Future fetchTopOneFromTheLeaderboard() async { TetrioPlayerFromLeaderboard? cached = _cache.get("topone", TetrioPlayerFromLeaderboard); if (cached != null) return cached; diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 572845b..729becc 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -23,6 +23,8 @@ import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; @@ -289,19 +291,22 @@ class _DestinationGraphsState extends State { bool fetchData = false; bool _gamesPlayedInsteadOfDateAndTime = false; late ZoomPanBehavior _zoomPanBehavior; + late TooltipBehavior _historyTooltipBehavior; late TooltipBehavior _tooltipBehavior; String yAxisTitle = ""; bool _smooth = false; - final 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"]; + //final 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"]; + final List> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; Graph _graph = Graph.history; - int _chartsIndex = 0; + Stats _Ychart = Stats.tr; + Stats _Xchart = Stats.tr; int _season = currentSeason-1; - late List>>> historyData; + //late List>>> historyData; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override void initState(){ - _tooltipBehavior = TooltipBehavior( + _historyTooltipBehavior = TooltipBehavior( color: Colors.black, borderColor: Colors.white, enable: true, @@ -326,6 +331,31 @@ class _DestinationGraphsState extends State { ); } ); + _tooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${data.nickname} (${data.rank.toUpperCase()})", + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20), + ), + ), + Text('${f4.format(data.x)} ${chartsShortTitles[_Xchart]}\n${f4.format(data.y)} ${chartsShortTitles[_Ychart]}') + ], + ), + ); + } + ); _zoomPanBehavior = ZoomPanBehavior( enablePinching: true, enableSelectionZooming: true, @@ -335,7 +365,7 @@ class _DestinationGraphsState extends State { super.initState(); } - Future>>>> getHistoryData(bool fetchHistory) async { + Future>>> getHistoryData(bool fetchHistory) async { if(fetchHistory){ try{ var history = await teto.fetchAndsaveTLHistory(widget.searchFor); @@ -354,217 +384,261 @@ class _DestinationGraphsState extends State { List> states = await Future.wait>([ teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), ]); - - if (states.length >= 2){ - historyData = [for (List s in states) >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in s) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), - ]]; - }else{ - historyData = []; + List>> historyData = []; // [season][metric][spot] + for (int season = 0; season < currentSeason; season++){ + if (states[season].length >= 2){ + Map> statsMap = {}; + for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())]; + historyData.add(statsMap); + }else{ + historyData.add({}); + break; + } } - fetchData = false; return historyData; } + Future> getTetraLeagueData(Stats x, Stats y) async { + TetrioPlayersLeaderboard leaderboard = await teto.fetchTLLeaderboard(); + List<_MyScatterSpot> _spots = [ + for (TetrioPlayerFromLeaderboard entry in leaderboard.leaderboard) + _MyScatterSpot( + entry.getStatByEnum(x).toDouble(), + entry.getStatByEnum(y).toDouble(), + entry.userId, + entry.username, + entry.rank, + rankColors[entry.rank]??Colors.white + ) + ]; + return _spots; + } + + Widget getHistoryGraph(){ + return FutureBuilder>>>( + future: getHistoryData(fetchData), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData && snapshot.data!.isNotEmpty){ + List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; + yAxisTitle = chartsShortTitles[_Ychart]!; + return SfCartesianChart( + tooltipBehavior: _historyTooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( + rangePadding: ChartRangePadding.additional, + ), + margin: const EdgeInsets.all(0), + series: [ + if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( + enableTooltip: true, + dataSource: selectedGraph, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (selectedGraph.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ) + else StepLineSeries<_HistoryChartSpot, DateTime>( + enableTooltip: true, + dataSource: selectedGraph, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (selectedGraph.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ), + ], + ); + }else{ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + } + ); + } + + Widget getLeagueState (){ + return FutureBuilder>( + future: getTetraLeagueData(_Xchart, _Ychart), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + //primaryXAxis: CategoryAxis(), + series: [ + ScatterSeries( + enableTooltip: true, + dataSource: snapshot.data, + animationDuration: 0, + pointColorMapper: (data, _) => data.color, + xValueMapper: (data, _) => data.x, + yValueMapper: (data, _) => data.y, + onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: snapshot.data![point.pointIndex!].nickname), maintainState: false)), + ) + ], + ); + }else{ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + } + ); + } + + Widget getCutoffsHistory(){ + return Container(); // TODO + } + @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ - FutureBuilder>>>>( - future: getHistoryData(fetchData), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData && snapshot.data!.isNotEmpty){ - List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_chartsIndex].value!; - yAxisTitle = _historyShortTitles[_chartsIndex]; - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], - value: _season, - onChanged: (value) { - setState(() { - _season = value!; - }); - } - ), - ], - ), - 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) { - setState(() { - _gamesPlayedInsteadOfDateAndTime = value!; - }); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: historyData[_season], - value: historyData[_season][_chartsIndex].value, - onChanged: (value) { - setState(() { - _chartsIndex = historyData[_season].indexWhere((element) => element.value == value); - }); - } - ), - ], - ), - if (selectedGraph.length > 300) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox(value: _smooth, - checkColor: Colors.black, - onChanged: ((value) { - setState(() { - _smooth = value!; - }); - })), - Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - ), - if(historyData[_season][_chartsIndex].value!.length > 1) Card( - child: SizedBox( - width: MediaQuery.of(context).size.width - 88, - height: MediaQuery.of(context).size.height - 96, - child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), - child: SfCartesianChart( - tooltipBehavior: _tooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, - ), - margin: const EdgeInsets.all(0), - series: [ - if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( - enableTooltip: true, - dataSource: historyData[_season][_chartsIndex].value!, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (historyData[_season][_chartsIndex].value!.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ) - else StepLineSeries<_HistoryChartSpot, DateTime>( - enableTooltip: true, - dataSource: historyData[_season][_chartsIndex].value!, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (historyData[_season][_chartsIndex].value!.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ), - ], - ), - ) - ), - ) - else if (historyData[_season][_chartsIndex].value!.length <= 1) Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - Text(t.errors.actionSuggestion), - TextButton(onPressed: (){setState(() { - fetchData = true; - });}, child: Text(t.fetchAndsaveTLHistory)) - ], - )) - ], - ), - ); - } - if (snapshot.hasError || snapshot.data!.isEmpty){ - return Center(child: - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, + SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Wrap( + spacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, children: [ - Text(snapshot.error != null ? snapshot.error.toString() : t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), + if (_graph == Graph.history) Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], + value: _season, + onChanged: (value) { + setState(() { + _season = value!; + }); + } + ), + ], ), + if (_graph != Graph.leagueCutoffs) Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: switch (_graph){ + Graph.history => [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], + Graph.leagueState => _yAxis, + Graph.leagueCutoffs => [], + }, + value: _graph == Graph.history ? _gamesPlayedInsteadOfDateAndTime : _Xchart, + onChanged: (value) { + setState(() { + if (_graph == Graph.history) + _gamesPlayedInsteadOfDateAndTime = value! as bool; + else _Xchart = value! as Stats; + }); + } + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: _yAxis, + value: _Ychart, + onChanged: (value) { + setState(() { + _Ychart = value!; + }); + } + ), + ], + ), + if (_graph != Graph.leagueState) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox(value: _smooth, + checkColor: Colors.black, + onChanged: ((value) { + setState(() { + _smooth = value!; + }); + })), + Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) + ], + ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) ], - ) - ); - } - } - return const Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("lol", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - ], - )); - }, + ), + ), + Card( + child: SizedBox( + width: MediaQuery.of(context).size.width - 88, + height: MediaQuery.of(context).size.height - 96, + child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), + child: switch (_graph){ + Graph.history => getHistoryGraph(), + Graph.leagueState => getLeagueState(), + Graph.leagueCutoffs => getCutoffsHistory() + }, + ) + ), + ) + ], + ), ), SegmentedButton( showSelectedIcon: false, @@ -584,6 +658,13 @@ class _DestinationGraphsState extends State { onSelectionChanged: (Set newSelection) { setState(() { _graph = newSelection.first; + switch (newSelection.first){ + case Graph.leagueCutoffs: + case Graph.history: + _Ychart = Stats.tr; + case Graph.leagueState: + _Ychart = Stats.apm; + } });}) ], ); @@ -598,6 +679,16 @@ class _HistoryChartSpot{ const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat); } +class _MyScatterSpot{ + num x; + num y; + String id; + String nickname; + String rank; + Color color; + _MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); +} + class DestinationHome extends StatefulWidget{ final String searchFor; //final Function setState; diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index d9d4a42..a0422f4 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -11,7 +11,7 @@ import 'package:tetra_stats/views/main_view.dart' show MainView; import 'package:window_manager/window_manager.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; -var _chartsShortTitlesDropdowns = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)]; +var _chartsShortTitlesDropdowns = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; Stats _chartsX = Stats.tr; Stats _chartsY = Stats.apm; late TooltipBehavior _tooltipBehavior; diff --git a/pubspec.yaml b/pubspec.yaml index 92fc8fa..bfafc4d 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.6.10+36 +version: 1.6.11+37 environment: sdk: '>=3.0.0' From a2a85ce1511f61298119b88730c12726d259dd36 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 14 Sep 2024 01:00:11 +0300 Subject: [PATCH 25/86] Yaaaaay the graaaaaaph!!! --- lib/main.dart | 2 +- lib/services/tetrio_crud.dart | 10 ++-- lib/views/main_view_tiles.dart | 84 +++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index bbba219..a847a1e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 6520bf7..4ac790a 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -554,20 +554,20 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: - List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); + List> csv = const CsvToListConverter().convert(response.body, eol: "\n")..removeAt(0); List history = []; for (List entry in csv){ Map tr = {}; Map glicko = {}; Map gxe = {}; for(int i = 0; i < ranks.length; i++){ - tr[ranks[ranks.length + i - ranks.length]] = entry[1 + i*3]; - glicko[ranks[ranks.length + i - ranks.length]] = entry[2 + i*3]; - glicko[ranks[ranks.length + i - ranks.length]] = entry[3 + i*3]; + tr[ranks[ranks.length - 1 - i]] = entry[1 + i*3]; + glicko[ranks[ranks.length - 1 - i]] = entry[2 + i*3]; + gxe[ranks[ranks.length - 1 - i]] = entry[3 + i*3]; } history.add( Cutoffs( - DateTime.fromMillisecondsSinceEpoch(entry[0]), + DateTime.fromMillisecondsSinceEpoch(entry[0]*1000), tr, glicko, gxe diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 729becc..9b80ab3 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -172,6 +172,7 @@ class _MainState extends State with TickerProviderStateMixin { getDestinationButton(Icons.leaderboard, "Leaderboards"), getDestinationButton(Icons.compress, "Cutoffs"), getDestinationButton(Icons.calculate, "Calc"), + getDestinationButton(Icons.info_outline, "Information"), getDestinationButton(Icons.storage, "Saved Data"), getDestinationButton(Icons.settings, "Settings"), ], @@ -293,6 +294,7 @@ class _DestinationGraphsState extends State { late ZoomPanBehavior _zoomPanBehavior; late TooltipBehavior _historyTooltipBehavior; late TooltipBehavior _tooltipBehavior; + late TooltipBehavior _leagueTooltipBehavior; String yAxisTitle = ""; bool _smooth = false; //final 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"]; @@ -356,6 +358,32 @@ class _DestinationGraphsState extends State { ); } ); + _leagueTooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + print(point); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${f4.format(point.y)} $yAxisTitle", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + ), + ), + Text(timestamp(data.ts)) + ], + ), + ); + } + ); _zoomPanBehavior = ZoomPanBehavior( enablePinching: true, enableSelectionZooming: true, @@ -538,7 +566,61 @@ class _DestinationGraphsState extends State { } Widget getCutoffsHistory(){ - return Container(); // TODO + return FutureBuilder>( + future: teto.fetchCutoffsHistory(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + yAxisTitle = chartsShortTitles[_Ychart]!; + return SfCartesianChart( + tooltipBehavior: _leagueTooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( + rangePadding: ChartRangePadding.additional, + ), + margin: const EdgeInsets.all(0), + series: [ + for (String rank in ranks) StepLineSeries( + enableTooltip: true, + dataSource: snapshot.data, + animationDuration: 0, + //opacity: _smooth ? 0 : 1, + xValueMapper: (Cutoffs data, _) => data.ts, + yValueMapper: (Cutoffs data, _) => data.tr[rank], + color: rankColors[rank], + // trendlines:[ + // Trendline( + // isVisible: _smooth, + // period: (selectedGraph.length/175).floor(), + // type: TrendlineType.movingAverage, + // color: Theme.of(context).colorScheme.primary) + // ], + ) + ], + ); + }else{ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + } + ); } @override From c3214f5ba990e22ca9757e8ac2e8bb734f81a86c Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 15 Sep 2024 01:05:50 +0300 Subject: [PATCH 26/86] Lazy loading for a leaderboards in redesign was implemented --- lib/data_objects/tetra_league.dart | 6 +- .../tetrio_player_from_leaderboard.dart | 20 +- lib/services/tetrio_crud.dart | 78 +++-- lib/views/main_view_tiles.dart | 311 ++++++++++-------- 4 files changed, 255 insertions(+), 160 deletions(-) diff --git a/lib/data_objects/tetra_league.dart b/lib/data_objects/tetra_league.dart index 773c9ab..86e5e29 100644 --- a/lib/data_objects/tetra_league.dart +++ b/lib/data_objects/tetra_league.dart @@ -150,7 +150,11 @@ class TetraLeague { apm ?? 0, pps ?? 0, vs ?? 0, - decaying); + decaying, + -1, + -1, + Duration(seconds: -1), + -1); num? getStatByEnum(Stats stat){ switch (stat) { diff --git a/lib/data_objects/tetrio_player_from_leaderboard.dart b/lib/data_objects/tetrio_player_from_leaderboard.dart index 28a7fab..ad820dc 100644 --- a/lib/data_objects/tetrio_player_from_leaderboard.dart +++ b/lib/data_objects/tetrio_player_from_leaderboard.dart @@ -1,5 +1,7 @@ // ignore_for_file: hash_and_equals +import 'dart:math'; + import 'package:tetra_stats/data_objects/est_tr.dart'; import 'package:tetra_stats/data_objects/nerd_stats.dart'; import 'package:tetra_stats/data_objects/playstyle.dart'; @@ -27,6 +29,10 @@ class TetrioPlayerFromLeaderboard { late NerdStats nerdStats; late EstTr estTr; late Playstyle playstyle; + late int gamesPlayedTotal; + late int gamesWonTotal; + late Duration playtime; + late int ar; TetrioPlayerFromLeaderboard( this.userId, @@ -46,13 +52,19 @@ class TetrioPlayerFromLeaderboard { this.apm, this.pps, this.vs, - this.decaying){ + this.decaying, + this.gamesPlayedTotal, + this.gamesWonTotal, + this.playtime, + this.ar){ 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 winrateTotal => gamesWonTotal / gamesWonTotal; + double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1; double get esttracc => estTr.esttr - tr; double get s1tr => gxe * 250; @@ -66,7 +78,7 @@ class TetrioPlayerFromLeaderboard { gamesPlayed = json['league']['gamesplayed'] as int; gamesWon = json['league']['gameswon'] as int; tr = json['league']['tr'] != null ? json['league']['tr'].toDouble() : 0; - gxe = json['league']['gxe']??-1; + gxe = json['league']['gxe']?.toDouble(); glicko = json['league']['glicko']?.toDouble(); rd = json['league']['rd']?.toDouble(); rank = json['league']['rank']; @@ -75,6 +87,10 @@ class TetrioPlayerFromLeaderboard { pps = json['league']['pps'] != null ? json['league']['pps'].toDouble() : 0.00; vs = json['league']['vs'] != null ? json['league']['vs'].toDouble(): 0.00; decaying = json['league']['decaying']; + gamesPlayedTotal = json['gamesplayed'] as int; + gamesWonTotal = json['gameswon'] as int; + playtime = Duration(microseconds: (json['gametime'].toDouble() * 1000000).floor()); + ar = json['ar']; 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); diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 4ac790a..5e042ea 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -798,34 +798,60 @@ class TetrioService extends DB { } } - // Stream fetchFullLeaderboard() async* { - // late double after; - // int lbLength = 100; - // TetrioPlayersLeaderboard leaderboard = await fetchTLLeaderboard(); - // after = leaderboard.leaderboard.last.tr; - // while (lbLength == 100){ - // TetrioPlayersLeaderboard pseudoLb = await fetchTLLeaderboard(after: after); - // leaderboard.addPlayers(pseudoLb.leaderboard); - // lbLength = pseudoLb.leaderboard.length; - // after = pseudoLb.leaderboard.last.tr; - // yield leaderboard; - // } - // } - - // i want to know progress, so i trying to figure out this thing: - // Stream fetchTLLeaderboardAsStream() async { - // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); - // if (cached != null) return cached; + Future> fetchTetrioLeaderboard({double? after, String? lb}) async { + const int lbLength = 100; + // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); + // if (cached != null) return cached; - // Uri url; - // if (kIsWeb) { - // url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); - // } else { - // url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); - // } + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); + } else { + url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', { + "limit": "100", + if (after != null && after != -1) "after": "$after:0:0" + }); + } + try{ + final response = await client.get(url); - // Stream stream = http.StreamedRequest("GET", url); - // } + switch (response.statusCode) { + case 200: + _lbPositions.clear(); + var rawJson = jsonDecode(response.body); + if (rawJson['success']) { // if api confirmed that everything ok + List leaderboard = []; + for (Map entry in rawJson['data']['entries']) { + leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at']))); + } + developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + //_cache.store(leaderboard, rawJson['cache']['cached_until']); + return leaderboard; + } else { // idk how to hit that one + developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); + throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side? + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); + } + } TetrioPlayersLeaderboard? getCachedLeaderboard(){ return _cache.get("league", TetrioPlayersLeaderboard); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 9b80ab3..c30ccf9 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; @@ -207,6 +209,23 @@ class _MainState extends State with TickerProviderStateMixin { } } +class DestinationCutoffs extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationCutoffs({super.key, required this.constraints}); + + @override + State createState() => _DestinationCutoffsState(); +} + +class _DestinationCutoffsState extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } +} + class DestinationLeaderboards extends StatefulWidget{ final BoxConstraints constraints; @@ -216,10 +235,72 @@ class DestinationLeaderboards extends StatefulWidget{ State createState() => _DestinationLeaderboardsState(); } +enum Leaderboards{ + tl, + xp, + ar +} + class _DestinationLeaderboardsState extends State { - Cards rightCard = Cards.tetraLeague; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - final List leaderboards = ["Tetra League", "Quick Play", "Quick Play Expert"]; + final Map leaderboards = {Leaderboards.tl: "Tetra League", Leaderboards.xp: "XP", Leaderboards.ar: "Acievement Points"}; + Leaderboards _currentLb = Leaderboards.tl; + final StreamController> _dataStreamController = StreamController>(); + late final ScrollController _scrollController; + Stream> get dataStream => _dataStreamController.stream; + List list = []; + bool _isFetchingData = false; + double after = 25000.00; + + Future _fetchData() async { + if (_isFetchingData) { + // Avoid fetching new data while already fetching + return; + } + try { + _isFetchingData = true; + setState(() {}); + + final items = switch(_currentLb){ + Leaderboards.tl => await teto.fetchTetrioLeaderboard(after: after), + Leaderboards.xp => await teto.fetchTetrioLeaderboard(after: after, lb: "xp"), + Leaderboards.ar => await teto.fetchTetrioLeaderboard(after: after, lb: "ar"), + }; + + list.addAll(items); + + _dataStreamController.add(list); + after = switch (_currentLb){ + Leaderboards.tl => list.last.tr, + Leaderboards.xp => list.last.xp, + Leaderboards.ar => list.last.ar.toDouble(), + }; + } catch (e) { + _dataStreamController.addError(e); + } finally { + // Set to false when data fetching is complete + _isFetchingData = false; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _fetchData(); + _scrollController.addListener(() { + _scrollController.addListener(() { + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.position.pixels; + + if (currentScroll == maxScroll) { + // When the last item is fully visible, load the next page. + _fetchData(); + } + }); + }); + } @override Widget build(BuildContext context) { @@ -247,7 +328,17 @@ class _DestinationLeaderboardsState extends State { return Card( surfaceTintColor: theme.colorScheme.primary, child: ListTile( - title: Text(leaderboards[index]), + title: Text(leaderboards.values.elementAt(index)), + onTap: () { + _currentLb = leaderboards.keys.elementAt(index); + list.clear(); + after = switch (_currentLb){ + Leaderboards.tl => 25000.00, + Leaderboards.xp => -1.00, + Leaderboards.ar => -1.00, + }; + _fetchData(); + }, ), ); } @@ -258,11 +349,45 @@ class _DestinationLeaderboardsState extends State { ), SizedBox( width: widget.constraints.maxWidth - 350 - 88, - child: const Card( - child: Column( - children: [ - - ], + child: Card( + child: StreamBuilder>( + stream: dataStream, + builder:(context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + Text(leaderboards[_currentLb]!, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length, + itemBuilder: (BuildContext context, int index){ + return ListTile( + leading: Text(intf.format(index+1)), + title: Text(snapshot.data![index].username), + trailing: Text(switch (_currentLb){ + Leaderboards.tl => f2.format(snapshot.data![index].tr), + Leaderboards.xp => f2.format(snapshot.data![index].level), + Leaderboards.ar => intf.format(snapshot.data![index].ar), + }), + ); + } + ), + ), + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + }, ), ), ), @@ -297,13 +422,11 @@ class _DestinationGraphsState extends State { late TooltipBehavior _leagueTooltipBehavior; String yAxisTitle = ""; bool _smooth = false; - //final 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"]; final List> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; Graph _graph = Graph.history; Stats _Ychart = Stats.tr; Stats _Xchart = Stats.tr; int _season = currentSeason-1; - //late List>>> historyData; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override @@ -365,7 +488,6 @@ class _DestinationGraphsState extends State { animationDuration: 0, builder: (dynamic data, dynamic point, dynamic series, int pointIndex, int seriesIndex) { - print(point); return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -454,7 +576,7 @@ class _DestinationGraphsState extends State { case ConnectionState.active: return const Center(child: CircularProgressIndicator()); case ConnectionState.done: - if (snapshot.hasData && snapshot.data!.isNotEmpty){ + if (snapshot.hasData){ List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; yAxisTitle = chartsShortTitles[_Ychart]!; return SfCartesianChart( @@ -500,20 +622,7 @@ class _DestinationGraphsState extends State { ), ], ); - }else{ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + }else{ return FutureError(snapshot); } } } ); @@ -546,20 +655,7 @@ class _DestinationGraphsState extends State { ) ], ); - }else{ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + }else{ return FutureError(snapshot); } } } ); @@ -580,9 +676,14 @@ class _DestinationGraphsState extends State { return SfCartesianChart( tooltipBehavior: _leagueTooltipBehavior, zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, + primaryXAxis: const DateTimeAxis(), + primaryYAxis: NumericAxis( + // isInversed: true, + maximum: switch (_Ychart){ + Stats.tr => 25000.0, + Stats.gxe => 100.00, + _ => null + }, ), margin: const EdgeInsets.all(0), series: [ @@ -590,34 +691,18 @@ class _DestinationGraphsState extends State { enableTooltip: true, dataSource: snapshot.data, animationDuration: 0, - //opacity: _smooth ? 0 : 1, + //opacity: 0.5, xValueMapper: (Cutoffs data, _) => data.ts, - yValueMapper: (Cutoffs data, _) => data.tr[rank], - color: rankColors[rank], - // trendlines:[ - // Trendline( - // isVisible: _smooth, - // period: (selectedGraph.length/175).floor(), - // type: TrendlineType.movingAverage, - // color: Theme.of(context).colorScheme.primary) - // ], + yValueMapper: (Cutoffs data, _) => switch (_Ychart){ + Stats.glicko => data.glicko[rank], + Stats.gxe => data.gxe[rank], + _ => data.tr[rank] + }, + color: rankColors[rank]! ) ], ); - }else{ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + }else{ return FutureError(snapshot); } } } ); @@ -679,7 +764,7 @@ class _DestinationGraphsState extends State { children: [ const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), DropdownButton( - items: _yAxis, + items: _graph == Graph.leagueCutoffs ? [DropdownMenuItem(value: Stats.tr, child: Text(chartsShortTitles[Stats.tr]!)), DropdownMenuItem(value: Stats.glicko, child: Text(chartsShortTitles[Stats.glicko]!)), DropdownMenuItem(value: Stats.gxe, child: Text(chartsShortTitles[Stats.gxe]!))] : _yAxis, value: _Ychart, onChanged: (value) { setState(() { @@ -1254,20 +1339,7 @@ class _DestinationHomeState extends State with SingleTickerProv ], ); } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); }, @@ -1306,20 +1378,7 @@ class _DestinationHomeState extends State with SingleTickerProv ], ); } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); }, @@ -1366,20 +1425,7 @@ class _DestinationHomeState extends State with SingleTickerProv if (snapshot.hasData){ return SizedBox(height: constraints.maxHeight - 145, child: _TLRecords(userID: widget.searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); }, @@ -1760,20 +1806,7 @@ class _DestinationHomeState extends State with SingleTickerProv case ConnectionState.active: return const Center(child: CircularProgressIndicator()); case ConnectionState.done: - if (snapshot.hasError){ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } if (snapshot.hasData){ blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; @@ -1837,13 +1870,7 @@ class _DestinationHomeState extends State with SingleTickerProv case ConnectionState.done: if (snapshot.hasData){ return NewsThingy(snapshot.data!); - }else if (snapshot.hasError){ - return Card(child: Column(children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Text(snapshot.stackTrace.toString()) - ] - )); - } + }else if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); } @@ -3318,4 +3345,26 @@ class TLRatingThingy extends StatelessWidget{ ], ); } +} + +class FutureError extends StatelessWidget{ + final AsyncSnapshot snapshot; + + FutureError(this.snapshot); + + @override + Widget build(BuildContext context) { + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), + ), + ], + ) + ); + } } \ No newline at end of file From dbdfe29dc096d2ebb6d785aca5191c6867b1943b Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 15 Sep 2024 19:38:07 +0300 Subject: [PATCH 27/86] leaderboards done (kinda) --- lib/data_objects/record_single.dart | 11 ++- .../tetrio_player_from_leaderboard.dart | 8 ++ lib/data_objects/tetrio_prisecter.dart | 18 ++++ lib/services/tetrio_crud.dart | 56 ++++++++++++- lib/views/main_view_tiles.dart | 83 ++++++++++++------- 5 files changed, 142 insertions(+), 34 deletions(-) create mode 100644 lib/data_objects/tetrio_prisecter.dart diff --git a/lib/data_objects/record_single.dart b/lib/data_objects/record_single.dart index 92eb304..6106a85 100644 --- a/lib/data_objects/record_single.dart +++ b/lib/data_objects/record_single.dart @@ -3,9 +3,11 @@ import 'package:tetra_stats/data_objects/aggregate_stats.dart'; import 'package:tetra_stats/data_objects/record_extras.dart'; import 'package:tetra_stats/data_objects/results_stats.dart'; +import 'package:tetra_stats/data_objects/tetrio_prisecter.dart'; class RecordSingle { late String? userId; + late String username; late String replayId; late String ownId; late String gamemode; @@ -15,8 +17,9 @@ class RecordSingle { late int countryRank; late AggregateStats aggregateStats; late RecordExtras extras; + late Prisecter prisecter; - RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); + RecordSingle({required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); RecordSingle.fromJson(Map json, int ran, int cran) { ownId = json['_id']; @@ -24,10 +27,14 @@ class RecordSingle { stats = ResultsStats.fromJson(json['results']['stats']); replayId = json['replayid']; timestamp = DateTime.parse(json['ts']); - if (json['user'] != null) userId = json['user']['id']; + if (json['user'] != null) { + userId = json['user']['id']; + username = json['user']['username']; + } rank = ran; countryRank = cran; aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']); + prisecter = Prisecter.fromJson(json['p']); var ex = json['extras'] as Map; switch (ex.keys.firstOrNull){ case "zenith": diff --git a/lib/data_objects/tetrio_player_from_leaderboard.dart b/lib/data_objects/tetrio_player_from_leaderboard.dart index ad820dc..80ba9b8 100644 --- a/lib/data_objects/tetrio_player_from_leaderboard.dart +++ b/lib/data_objects/tetrio_player_from_leaderboard.dart @@ -6,6 +6,7 @@ import 'package:tetra_stats/data_objects/est_tr.dart'; import 'package:tetra_stats/data_objects/nerd_stats.dart'; import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_prisecter.dart'; class TetrioPlayerFromLeaderboard { late String userId; @@ -33,6 +34,8 @@ class TetrioPlayerFromLeaderboard { late int gamesWonTotal; late Duration playtime; late int ar; + late Map ar_counts; + late Prisecter prisecter; TetrioPlayerFromLeaderboard( this.userId, @@ -91,6 +94,11 @@ class TetrioPlayerFromLeaderboard { gamesWonTotal = json['gameswon'] as int; playtime = Duration(microseconds: (json['gametime'].toDouble() * 1000000).floor()); ar = json['ar']; + ar_counts = {}; + for (var entry in json['ar_counts'].keys){ + ar_counts[entry.toString()] = json['ar_counts'][entry]; + } + prisecter = Prisecter.fromJson(json['p']); 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); diff --git a/lib/data_objects/tetrio_prisecter.dart b/lib/data_objects/tetrio_prisecter.dart new file mode 100644 index 0000000..6595cb1 --- /dev/null +++ b/lib/data_objects/tetrio_prisecter.dart @@ -0,0 +1,18 @@ +class Prisecter { + late final num pri; + late final num sec; + late final num ter; + + Prisecter(this.pri, this.sec, this.ter); + + @override + String toString() { + return "${pri}:${sec}:${ter}"; + } + + Prisecter.fromJson(Map json){ + pri = json['pri']; + sec = json['sec']; + ter = json['ter']; + } +} \ No newline at end of file diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 5e042ea..be4466f 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -798,8 +798,7 @@ class TetrioService extends DB { } } - Future> fetchTetrioLeaderboard({double? after, String? lb}) async { - const int lbLength = 100; + Future> fetchTetrioLeaderboard({String? prisecter, String? lb}) async { // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); // if (cached != null) return cached; @@ -809,7 +808,7 @@ class TetrioService extends DB { } else { url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', { "limit": "100", - if (after != null && after != -1) "after": "$after:0:0" + if (prisecter != null) "after": prisecter }); } try{ @@ -853,6 +852,57 @@ class TetrioService extends DB { } } + Future> fetchTetrioRecordsLeaderboard({String? prisecter, String? lb}) async{ + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); + } else { + url = Uri.https('ch.tetr.io', 'api/records/${lb??"40l_global"}', { + "limit": "100", + if (prisecter != null) "after": prisecter + }); + } + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + _lbPositions.clear(); + var rawJson = jsonDecode(response.body); + if (rawJson['success']) { // if api confirmed that everything ok + List leaderboard = []; + for (Map entry in rawJson['data']['entries']) { + leaderboard.add(RecordSingle.fromJson(entry, -1, -1)); + } + developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + //_cache.store(leaderboard, rawJson['cache']['cached_until']); + return leaderboard; + } else { // idk how to hit that one + developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); + throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side? + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); + } + } + TetrioPlayersLeaderboard? getCachedLeaderboard(){ return _cache.get("league", TetrioPlayersLeaderboard); } diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index c30ccf9..578473c 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -200,6 +200,7 @@ class _MainState extends State with TickerProviderStateMixin { 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), 2 => DestinationLeaderboards(constraints: constraints), + 3 => DestinationCutoffs(constraints: constraints), _ => Text("Unknown destination $destination") }, ) @@ -215,14 +216,17 @@ class DestinationCutoffs extends StatefulWidget{ const DestinationCutoffs({super.key, required this.constraints}); @override - State createState() => _DestinationCutoffsState(); + State createState() => _DestinationCutoffsState(); } -class _DestinationCutoffsState extends State { +class _DestinationCutoffsState extends State { @override Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); + return Column( + children: [ + Card(), + ] + ); } } @@ -238,19 +242,31 @@ class DestinationLeaderboards extends StatefulWidget{ enum Leaderboards{ tl, xp, - ar + ar, + sprint, + blitz, + zenith, + zenithex, } class _DestinationLeaderboardsState extends State { //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - final Map leaderboards = {Leaderboards.tl: "Tetra League", Leaderboards.xp: "XP", Leaderboards.ar: "Acievement Points"}; + final Map leaderboards = { + Leaderboards.tl: "Tetra League", + Leaderboards.xp: "XP", + Leaderboards.ar: "Acievement Points", + Leaderboards.sprint: "40 Lines", + Leaderboards.blitz: "Blitz", + Leaderboards.zenith: "Quick Play", + Leaderboards.zenithex: "Quick Play Expert", + }; Leaderboards _currentLb = Leaderboards.tl; - final StreamController> _dataStreamController = StreamController>(); + final StreamController> _dataStreamController = StreamController>(); late final ScrollController _scrollController; - Stream> get dataStream => _dataStreamController.stream; - List list = []; + Stream> get dataStream => _dataStreamController.stream; + List list = []; bool _isFetchingData = false; - double after = 25000.00; + String? prisecter; Future _fetchData() async { if (_isFetchingData) { @@ -262,19 +278,19 @@ class _DestinationLeaderboardsState extends State { setState(() {}); final items = switch(_currentLb){ - Leaderboards.tl => await teto.fetchTetrioLeaderboard(after: after), - Leaderboards.xp => await teto.fetchTetrioLeaderboard(after: after, lb: "xp"), - Leaderboards.ar => await teto.fetchTetrioLeaderboard(after: after, lb: "ar"), + Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter), + Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp"), + Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar"), + Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter), + Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global"), + Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global"), + Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global"), }; list.addAll(items); _dataStreamController.add(list); - after = switch (_currentLb){ - Leaderboards.tl => list.last.tr, - Leaderboards.xp => list.last.xp, - Leaderboards.ar => list.last.ar.toDouble(), - }; + prisecter = list.last.prisecter.toString(); } catch (e) { _dataStreamController.addError(e); } finally { @@ -332,11 +348,7 @@ class _DestinationLeaderboardsState extends State { onTap: () { _currentLb = leaderboards.keys.elementAt(index); list.clear(); - after = switch (_currentLb){ - Leaderboards.tl => 25000.00, - Leaderboards.xp => -1.00, - Leaderboards.ar => -1.00, - }; + prisecter = null; _fetchData(); }, ), @@ -350,7 +362,7 @@ class _DestinationLeaderboardsState extends State { SizedBox( width: widget.constraints.maxWidth - 350 - 88, child: Card( - child: StreamBuilder>( + child: StreamBuilder>( stream: dataStream, builder:(context, snapshot) { switch (snapshot.connectionState){ @@ -371,12 +383,25 @@ class _DestinationLeaderboardsState extends State { itemBuilder: (BuildContext context, int index){ return ListTile( leading: Text(intf.format(index+1)), - title: Text(snapshot.data![index].username), + title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), trailing: Text(switch (_currentLb){ - Leaderboards.tl => f2.format(snapshot.data![index].tr), - Leaderboards.xp => f2.format(snapshot.data![index].level), - Leaderboards.ar => intf.format(snapshot.data![index].ar), - }), + Leaderboards.tl => "${f2.format(snapshot.data![index].tr)} TR", + Leaderboards.xp => "LVL ${f2.format(snapshot.data![index].level)}", + Leaderboards.ar => "${intf.format(snapshot.data![index].ar)} AR", + Leaderboards.sprint => get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), + Leaderboards.blitz => intf.format(snapshot.data![index].stats.score), + Leaderboards.zenith => "${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", + Leaderboards.zenithex => "${f2.format(snapshot.data![index].stats.zenith!.altitude)} m" + }, style: TextStyle(fontSize: 28)), + subtitle: Text(switch (_currentLb){ + Leaderboards.tl => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", + Leaderboards.xp => "${f2.format(snapshot.data![index].xp)} XP${snapshot.data![index].playtime.isNegative ? "" : ", ${playtime(snapshot.data![index].playtime)} of gametime"}", + Leaderboards.ar => "${snapshot.data![index].ar_counts}", + Leaderboards.sprint => "${intf.format(snapshot.data![index].stats.finesse.faults)} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${intf.format(snapshot.data![index].stats.piecesPlaced)} P", + Leaderboards.blitz => "lvl ${snapshot.data![index].stats.level}, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${f2.format(snapshot.data![index].stats.spp)} SPP", + Leaderboards.zenith => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B", + Leaderboards.zenithex => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B" + }, style: TextStyle(color: Colors.grey, fontSize: 12)), ); } ), From 8d7bccfac0ae6ad236b26d982305b95b3c7ac143 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 17 Sep 2024 01:37:25 +0300 Subject: [PATCH 28/86] Trying to do cutoffs --- lib/data_objects/cutoff_tetrio.dart | 24 +++++-- lib/views/main_view_tiles.dart | 97 +++++++++++++++++++++++++++-- lib/views/ranks_averages_view.dart | 8 +-- 3 files changed, 115 insertions(+), 14 deletions(-) diff --git a/lib/data_objects/cutoff_tetrio.dart b/lib/data_objects/cutoff_tetrio.dart index 586192a..da51e00 100644 --- a/lib/data_objects/cutoff_tetrio.dart +++ b/lib/data_objects/cutoff_tetrio.dart @@ -5,20 +5,32 @@ class CutoffTetrio { late double percentile; late double tr; late double targetTr; - late double apm; - late double pps; - late double vs; + late double? apm; + late double? pps; + late double? vs; late int count; late double countPercentile; + CutoffTetrio({ + required this.pos, + required this.percentile, + required this.tr, + required this.targetTr, + required this.apm, + required this.pps, + required this.vs, + required this.count, + required this.countPercentile + }); + CutoffTetrio.fromJson(Map json, int total){ pos = json['pos']; percentile = json['percentile'].toDouble(); tr = json['tr'].toDouble(); targetTr = json['targettr'].toDouble(); - apm = json['apm'].toDouble(); - pps = json['pps'].toDouble(); - vs = json['vs'].toDouble(); + apm = json['apm']?.toDouble(); + pps = json['pps']?.toDouble(); + vs = json['vs']?.toDouble(); count = json['count']; countPercentile = count / total; } diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 578473c..704bc05 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -10,6 +10,7 @@ import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/badge.dart'; import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; import 'package:tetra_stats/data_objects/distinguishment.dart'; import 'package:tetra_stats/data_objects/est_tr.dart'; import 'package:tetra_stats/data_objects/nerd_stats.dart'; @@ -210,6 +211,14 @@ class _MainState extends State with TickerProviderStateMixin { } } +class FetchCutoffsResults{ + late bool success; + CutoffsTetrio? cutoffs; + Exception? exception; + + FetchCutoffsResults(this.success, this.cutoffs, this.exception); +} + class DestinationCutoffs extends StatefulWidget{ final BoxConstraints constraints; @@ -220,12 +229,92 @@ class DestinationCutoffs extends StatefulWidget{ } class _DestinationCutoffsState extends State { + + Future fetch() async { + TetrioPlayerFromLeaderboard top1; + CutoffsTetrio cutoffs; + List requests = await Future.wait([ + teto.fetchCutoffsTetrio(), + teto.fetchTopOneFromTheLeaderboard(), + ]); + cutoffs = requests[0]; + top1 = requests[1]; + cutoffs.data["top1"] = CutoffTetrio( + pos: 1, + percentile: 0.00, + tr: top1.tr, + targetTr: 25000, + apm: top1.apm, + pps: top1.pps, + vs: top1.vs, + count: 1, + countPercentile: 0.0 + ); + return cutoffs; + } + @override Widget build(BuildContext context) { - return Column( - children: [ - Card(), - ] + return FutureBuilder( + future: fetch(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + Card( + child: Center(child: Text("Tetra League State")), + ), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 8.0), + child: SfLinearGauge( + minimum: 0.00000000, + maximum: 25000.0000, + showTicks: false, + showLabels: false, + ranges: [ + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.outside, + startValue: snapshot.data!.data[cutoff]!.tr, + startWidth: 20.0, + endWidth: 20.0, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.tr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr + }, + color: cutoff != "top1" ? rankColors[cutoff] : null, + //shaderCallback: (bounds) { + // make shader blyat + // }, + ), + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.inside, + startValue: snapshot.data!.data[cutoff]!.targetTr, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.targetTr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr + }, + color: cutoff != "top1" ? rankColors[cutoff] : null, + ) + ], + ), + ), + ), + ] + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + } ); } } diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index cd10535..a368d35 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -114,19 +114,19 @@ class RanksAverages extends State { ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.apm), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.pps), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.vs), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text("${f3.format(snapshot.data!.data[rank]!.apm / (snapshot.data!.data[rank]!.pps * 60))} APP\n${f3.format(snapshot.data!.data[rank]!.vs / snapshot.data!.data[rank]!.apm)} VS/APM", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} APP\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} VS/APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), From 374293b275f4df35d92b8b2b98e0a89234c92cc0 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 18 Sep 2024 01:17:34 +0300 Subject: [PATCH 29/86] How to shaders? --- lib/data_objects/tetrio_constants.dart | 3 +- lib/views/main_view_tiles.dart | 247 +++++++++++++++++++------ 2 files changed, 192 insertions(+), 58 deletions(-) diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index 07bbcc3..dcbdfa7 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -161,7 +161,8 @@ const Map rankColors = { 'c-': Color(0xFF79558C), 'd+': Color(0xFF8E6091), 'd': Color(0xFF907591), - 'z': Color(0xFF375433) + 'z': Color(0xFF375433), + 'top1': Colors.yellowAccent }; const Map sprintAverages = { diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 704bc05..6bc00c8 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,5 +1,5 @@ import 'dart:async'; - +import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; @@ -258,63 +258,196 @@ class _DestinationCutoffsState extends State { return FutureBuilder( future: fetch(), builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.hasData){ - return Column( - children: [ - Card( - child: Center(child: Text("Tetra League State")), - ), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 8.0), - child: SfLinearGauge( - minimum: 0.00000000, - maximum: 25000.0000, - showTicks: false, - showLabels: false, - ranges: [ - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.outside, - startValue: snapshot.data!.data[cutoff]!.tr, - startWidth: 20.0, - endWidth: 20.0, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.tr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr - }, - color: cutoff != "top1" ? rankColors[cutoff] : null, - //shaderCallback: (bounds) { - // make shader blyat - // }, - ), - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.inside, - startValue: snapshot.data!.data[cutoff]!.targetTr, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.targetTr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr - }, - color: cutoff != "top1" ? rankColors[cutoff] : null, - ) - ], - ), + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return SingleChildScrollView( + child: Column( + children: [ + Card( + child: Center(child: Text("Tetra League State")), + ), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 8.0), + child: SfLinearGauge( + minimum: 0.00000000, + maximum: 25000.0000, + showTicks: false, + showLabels: false, + ranges: [ + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.outside, + startValue: snapshot.data!.data[cutoff]!.tr, + startWidth: 20.0, + endWidth: 20.0, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.tr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr + }, + color: cutoff != "top1" ? rankColors[cutoff] : null, + // shaderCallback: (bounds) { + // return ImageShader(Image.file(""), TileMode.repeated, TileMode.repeated, Matrix4.identity().storage); + // }, ), - ), - ] - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return Text("huh?"); - } + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.inside, + startValue: snapshot.data!.data[cutoff]!.targetTr, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.targetTr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr + }, + color: cutoff != "top1" ? rankColors[cutoff] : null, + ) + ], + ), + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all(color: Colors.grey.shade900), + columnWidths: const { + 0: FixedColumnWidth(48), + 1: FixedColumnWidth(155), + 2: FixedColumnWidth(140), + 3: FixedColumnWidth(160), + 4: FixedColumnWidth(150), + 5: FixedColumnWidth(90), + 6: FixedColumnWidth(130), + 7: FixedColumnWidth(120), + 8: FixedColumnWidth(125), + 9: FixedColumnWidth(70), + }, + children: [ + TableRow( + children: [ + Text("Rank", textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Cutoff TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Target TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("State", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("More info", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + ] + ), + for (String rank in snapshot.data!.data.keys) if (rank != "top1") TableRow( + decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])), + children: [ + Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), + children: [ + if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), + TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), + if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) + ] + )), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} APP\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} VS/APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), + children: [ + TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)), + TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)), + TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null)) + ] + )) + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { + + },), + ), + ] + ) + ], + ), + Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))) + ], + ), + ) + ] + ), + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + } ); } } From b56d534cb33df09afbee3b2af570cf8e94de2877 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 20 Sep 2024 01:38:31 +0300 Subject: [PATCH 30/86] Screw shaders. Calculator looks like ass for now --- lib/views/main_view_tiles.dart | 474 ++++++++++++++++++++++----------- lib/views/tl_match_view.dart | 4 +- 2 files changed, 316 insertions(+), 162 deletions(-) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 6bc00c8..ad54973 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -46,6 +46,7 @@ import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/widgets/tl_progress_bar.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:transparent_image/transparent_image.dart'; +import 'package:vector_math/vector_math_64.dart' hide Colors; var fDiff = NumberFormat("+#,###.####;-#,###.####"); late Future _data; @@ -202,6 +203,7 @@ class _MainState extends State with TickerProviderStateMixin { 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), 2 => DestinationLeaderboards(constraints: constraints), 3 => DestinationCutoffs(constraints: constraints), + 4 => DestinationCalculator(constraints: constraints), _ => Text("Unknown destination $destination") }, ) @@ -211,6 +213,120 @@ class _MainState extends State with TickerProviderStateMixin { } } +class DestinationCalculator extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationCalculator({super.key, required this.constraints}); + + @override + State createState() => _DestinationCalculatorState(); +} + +class _DestinationCalculatorState extends State { + double? apm; + double? pps; + double? vs; + NerdStats? nerdStats; + EstTr? estTr; + Playstyle? playstyle; + TextEditingController ppsController = TextEditingController(); + TextEditingController apmController = TextEditingController(); + TextEditingController vsController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + void calc() { + apm = double.tryParse(apmController.text); + pps = double.tryParse(ppsController.text); + vs = double.tryParse(vsController.text); + if (apm != null && pps != null && vs != null) { + 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); + setState(() {}); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Please, enter valid values"))); + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Stats Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + 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(suffix: Text("APM"), alignLabelWithHint: true, hintText: "Enter your APM"), + ), + )), + Expanded( + child: TextField( + onSubmitted: (value) => calc(), + controller: ppsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("PPS"), alignLabelWithHint: true, hintText: "Enter your PPS"), + )), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: TextField( + onSubmitted: (value) => calc(), + controller: vsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("VS"), alignLabelWithHint: true, hintText: "Enter your VS"), + ), + )), + TextButton( + onPressed: () => calc(), + child: Text(t.calc), + ), + ], + ), + ), + ), + if (nerdStats != null && playstyle != null) Card( + child: Row( + children: [ + Expanded(child: NerdStatsThingy(nerdStats: nerdStats!)), + Expanded(child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!)) + ], + ), + ) + ], + ), + ); + } + +} + class FetchCutoffsResults{ late bool success; CutoffsTetrio? cutoffs; @@ -269,176 +385,214 @@ class _DestinationCutoffsState extends State { child: Column( children: [ Card( - child: Center(child: Text("Tetra League State")), + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Tetra League State", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("as of ${timestamp(snapshot.data!.timestamp)}"), + ], + ), + )), ), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 8.0), - child: SfLinearGauge( - minimum: 0.00000000, - maximum: 25000.0000, - showTicks: false, - showLabels: false, - ranges: [ - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.outside, - startValue: snapshot.data!.data[cutoff]!.tr, - startWidth: 20.0, - endWidth: 20.0, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.tr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr - }, - color: cutoff != "top1" ? rankColors[cutoff] : null, - // shaderCallback: (bounds) { - // return ImageShader(Image.file(""), TileMode.repeated, TileMode.repeated, Matrix4.identity().storage); - // }, + Padding( + padding: const EdgeInsets.only(bottom:4.0), + child: Card( + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text("Actual"), + ), + Text("Target") + ] + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), + child: SfLinearGauge( + minimum: 0.00000000, + maximum: 25000.0000, + showTicks: false, + showLabels: false, + ranges: [ + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.outside, + startValue: snapshot.data!.data[cutoff]!.tr, + startWidth: 20.0, + endWidth: 20.0, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.tr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr + }, + color: cutoff != "top1" ? rankColors[cutoff] : Colors.grey.shade800, + ), + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.inside, + startValue: snapshot.data!.data[cutoff]!.targetTr, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.targetTr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr + }, + color: cutoff != "top1" ? rankColors[cutoff] : null, + ), + for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[cutoff]!.tr < snapshot.data!.data[cutoff]!.targetTr) LinearGaugeRange( + position: LinearElementPosition.cross, + startValue: snapshot.data!.data[cutoff]!.tr, + endValue: snapshot.data!.data[cutoff]!.targetTr, + color: Colors.green, + ), + for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr)LinearGaugeRange( + position: LinearElementPosition.cross, + startValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr, + endValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr, + color: Colors.red, + ), + ], + markerPointers: [ + for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.tr), style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(0, 35, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0), value: snapshot.data!.data[cutoff]!.tr, position: LinearElementPosition.outside, offset: 20), + for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.targetTr), textAlign: ui.TextAlign.right, style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(-15, 0, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0, transformAlignment: Alignment.topRight), value: snapshot.data!.data[cutoff]!.targetTr, position: LinearElementPosition.inside, offset: 6) + ], + ), + ), + ), + ], ), - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.inside, - startValue: snapshot.data!.data[cutoff]!.targetTr, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.targetTr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr - }, - color: cutoff != "top1" ? rankColors[cutoff] : null, - ) ], ), ), ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - border: TableBorder.all(color: Colors.grey.shade900), - columnWidths: const { - 0: FixedColumnWidth(48), - 1: FixedColumnWidth(155), - 2: FixedColumnWidth(140), - 3: FixedColumnWidth(160), - 4: FixedColumnWidth(150), - 5: FixedColumnWidth(90), - 6: FixedColumnWidth(130), - 7: FixedColumnWidth(120), - 8: FixedColumnWidth(125), - 9: FixedColumnWidth(70), - }, - children: [ - TableRow( + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all(color: Colors.grey.shade900), + columnWidths: const { + 0: FixedColumnWidth(48), + 1: FixedColumnWidth(155), + 2: FixedColumnWidth(140), + 3: FixedColumnWidth(160), + 4: FixedColumnWidth(150), + 5: FixedColumnWidth(90), + 6: FixedColumnWidth(130), + 7: FixedColumnWidth(120), + 8: FixedColumnWidth(125), + 9: FixedColumnWidth(70), + }, + children: [ + TableRow( + children: [ + Text("Rank", textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Cutoff TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Target TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("State", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("More info", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + ] + ), + for (String rank in snapshot.data!.data.keys) if (rank != "top1") TableRow( + decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])), + children: [ + Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), children: [ - Text("Rank", textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("Cutoff TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("Target TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("State", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("More info", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), + if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), + TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), + if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) ] - ), - for (String rank in snapshot.data!.data.keys) if (rank != "top1") TableRow( - decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])), + )), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} APP\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} VS/APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), children: [ - Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: RichText( - textAlign: TextAlign.right, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), - children: [ - if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) - else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), - TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), - if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) - else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) - ] - )), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} APP\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} VS/APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: RichText( - textAlign: TextAlign.right, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), - children: [ - TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)), - TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)), - TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null)) - ] - )) - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { - - },), - ), + TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)), + TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)), + TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null)) ] - ) - ], - ), - Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))) - ], - ), + )) + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { + + },), + ), + ] + ) + ], ) ] ), diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 6b43db3..5f7844f 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -358,9 +358,9 @@ class TlMatchResultState extends State { CompareThingy( label: "Plonk", greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.plonk : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk, + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.plonk : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk, redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.plonk : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk, + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.plonk : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk, fractionDigits: 3, higherIsBetter: true, ), From cbfb00490eaac9a3538d56ddee3afbf744fd7cc3 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 22 Sep 2024 02:11:31 +0300 Subject: [PATCH 31/86] Idea: damage calculator --- lib/data_objects/tetrio_constants.dart | 60 ++++++ lib/views/main_view_tiles.dart | 275 ++++++++++++++++++++++--- 2 files changed, 302 insertions(+), 33 deletions(-) diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index dcbdfa7..1fa15b6 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -217,3 +217,63 @@ List seasonStarts = [ List seasonEnds = [ DateTime.utc(2024, DateTime.july, 26, 15) // Source - TETR.IO discord guild ]; + +/// Stolen directly from TETR.IO, redone for the sake of me + +enum Lineclears{ + ZERO, + SINGLE, + DOUBLE, + TRIPLE, + QUAD, + PENTA, + TSPIN_MINI, + TSPIN, + TSPIN_MINI_SINGLE, + TSPIN_SINGLE, + TSPIN_MINI_DOUBLE, + TSPIN_DOUBLE, + TSPIN_MINI_TRIPLE, + TSPIN_TRIPLE, + TSPIN_MINI_QUAD, + TSPIN_QUAD, + TSPIN_PENTA, +} + +enum ComboTables{ + none, + classic, + modern +} + +const int BACKTOBACK_BONUS = 1; +const double BACKTOBACK_BONUS_LOG = .8; +const int COMBO_MINIFIER = 1; +const double COMBO_MINIFIER_LOG = 1.25; +const double COMBO_BONUS = .25; +// const int ALL_CLEAR = 10; lol + +const Map garbage = { + Lineclears.SINGLE: 0, + Lineclears.DOUBLE: 1, + Lineclears.TRIPLE: 2, + Lineclears.QUAD: 4, + Lineclears.PENTA: 5, + Lineclears.TSPIN_MINI: 0, + Lineclears.TSPIN: 0, + Lineclears.TSPIN_MINI_SINGLE: 0, + Lineclears.TSPIN_SINGLE: 2, + Lineclears.TSPIN_MINI_DOUBLE: 1, + Lineclears.TSPIN_DOUBLE: 4, + Lineclears.TSPIN_MINI_TRIPLE: 2, + Lineclears.TSPIN_TRIPLE: 6, + Lineclears.TSPIN_MINI_QUAD: 4, + Lineclears.TSPIN_QUAD: 10, + Lineclears.TSPIN_PENTA: 12 +}; + +const Map> combotable = { + ComboTables.none: [0], + ComboTables.classic: [0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5], + ComboTables.modern: [0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4] +}; \ No newline at end of file diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index ad54973..2e42157 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:ui' as ui; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; @@ -222,6 +223,76 @@ class DestinationCalculator extends StatefulWidget{ State createState() => _DestinationCalculatorState(); } +enum CalcCards{ + calc, + damage +} + +class ClearData{ + final String title; + final Lineclears lineclear; + final int lines; + final bool miniSpin; + final bool spin; + bool perfectClear = false; + + ClearData(this.title, this.lineclear, this.lines, this.miniSpin, this.spin); + + bool get difficultClear { + if (lines == 0) return false; + if (lines >= 4 || miniSpin || spin) return true; + else return false; + } + + void togglePC(){ + perfectClear = !perfectClear; + } + + int dealsDamage(int combo, int b2b, ComboTables table,){ + if (lines == 0) return 0; + double damage = 0; + if (spin){ + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); + } else if (miniSpin){ + damage += garbage[lineclear]!; + } else { + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.PENTA]! + (lines - 5); + } + if (difficultClear && b2b > 0){ + damage += BACKTOBACK_BONUS * ((1 + log((b2b - 1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b - 1 == 1 ? 0 : (1 + log((b2b - 1) * BACKTOBACK_BONUS_LOG) % 1) / 3));; + } + if (combo > 1){ + damage += combotable[table]![max(0, min(combo - 2, combotable[table]!.length - 1))]; + } + return damage.floor(); + } +} + +Map> clearsExisting = { + "No Spin Clears": [ + ClearData("No lineclear (Break Combo)", Lineclears.ZERO, 0, false, false), + ClearData("Single", Lineclears.SINGLE, 1, false, false), + ClearData("Double", Lineclears.DOUBLE, 2, false, false), + ClearData("Triple", Lineclears.TRIPLE, 3, false, false), + ClearData("Quad", Lineclears.QUAD, 4, false, false) + ], + "Spins": [ + ClearData("Spin Zero", Lineclears.TSPIN, 0, false, true), + ClearData("Spin Single", Lineclears.TSPIN_SINGLE, 1, false, true), + ClearData("Spin Double", Lineclears.TSPIN_DOUBLE, 2, false, true), + ClearData("Spin Spin Triple", Lineclears.TSPIN_TRIPLE, 3, false, true), + ClearData("Spin Spin Quad", Lineclears.TSPIN_QUAD, 4, false, true), + ], + "Mini spins": [ + ClearData("Mini Spin Zero", Lineclears.TSPIN_MINI, 0, true, false), + ClearData("Mini Spin Single", Lineclears.TSPIN_MINI_SINGLE, 1, true, false), + ClearData("Mini Spin Double", Lineclears.TSPIN_MINI_DOUBLE, 2, true, false), + ClearData("Mini Spin Spin Triple", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false), + ] +}; + class _DestinationCalculatorState extends State { double? apm; double? pps; @@ -233,6 +304,10 @@ class _DestinationCalculatorState extends State { TextEditingController apmController = TextEditingController(); TextEditingController vsController = TextEditingController(); + List clears = []; + int combo = -1; + int b2b = -1; + @override void initState() { super.initState(); @@ -257,8 +332,13 @@ class _DestinationCalculatorState extends State { } } - @override - Widget build(BuildContext context) { + void calcDamage(){ + for (ClearData lineclear in clears){ + + } + } + + Widget getCalculator(){ return SingleChildScrollView( child: Column( children: [ @@ -274,36 +354,42 @@ class _DestinationCalculatorState extends State { ), Card( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), 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(suffix: Text("APM"), alignLabelWithHint: true, hintText: "Enter your APM"), - ), - )), - Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), child: TextField( - onSubmitted: (value) => calc(), - controller: ppsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("PPS"), alignLabelWithHint: true, hintText: "Enter your PPS"), - )), + onSubmitted: (value) => calc(), + controller: apmController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("APM"), alignLabelWithHint: true, hintText: "Enter your APM"), + ), + ) + ), Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 12), - child: TextField( - onSubmitted: (value) => calc(), - controller: vsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("VS"), alignLabelWithHint: true, hintText: "Enter your VS"), + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: ppsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("PPS"), alignLabelWithHint: true, hintText: "Enter your PPS"), + ), + ) ), - )), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: vsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("VS"), alignLabelWithHint: true, hintText: "Enter your VS"), + ), + ) + ), TextButton( onPressed: () => calc(), child: Text(t.calc), @@ -312,19 +398,142 @@ class _DestinationCalculatorState extends State { ), ), ), - if (nerdStats != null && playstyle != null) Card( - child: Row( - children: [ - Expanded(child: NerdStatsThingy(nerdStats: nerdStats!)), - Expanded(child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!)) - ], - ), + if (nerdStats != null) Card( + child: NerdStatsThingy(nerdStats: nerdStats!) + ), + if (playstyle != null) Card( + child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!) ) ], ), ); } + Widget getDamageCalculator(){ + List rSideWidgets = []; + for (var key in clearsExisting.keys){ + rSideWidgets.add(Text(key)); + for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( + child: ListTile( + title: Text(data.title), + subtitle: Text("${data.dealsDamage(0, 0, ComboTables.modern)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + trailing: Icon(Icons.arrow_forward_ios), + onTap: (){ + setState((){ + clears.add(data); + }); + }, + ), + )); + rSideWidgets.add(Text("Custom")); + rSideWidgets.add(const Divider(color: Color.fromARGB(50, 158, 158, 158))); + } + return Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Damage Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Row( + children: [ + SizedBox( + width: 350.0, + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: TabBar(tabs: [ + Tab(text: "Actions"), + Tab(text: "Rules"), + ]), + ), + SizedBox( + height: widget.constraints.maxHeight - 164, + child: TabBarView(children: [ + SingleChildScrollView( + child: Column( + children: rSideWidgets, + ), + ), + SingleChildScrollView( + child: Card( + child: Column( + children: [ + ListTile( + title: Text("Doubles"), + ) + ], + ), + ), + ) + ]), + ) + ], + ) + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 350 - 80, + child: Card( + child: Column( + children: [ + Column( + children: [for (ClearData data in clears) ListTile( + title: Text(data.title), + subtitle: Text("${data.dealsDamage(0, 0, ComboTables.modern)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + trailing: Text(data.dealsDamage(0, 0, ComboTables.modern).toString()), + onTap: (){ + clears.add(data); + }, + )], + ) + ], + ), + ), + ) + ], + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: widget.constraints.maxHeight -32, + child: getDamageCalculator(), + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: CalcCards.calc, + label: Text('Stats Calculator'), + ), + ButtonSegment( + value: CalcCards.damage, + label: Text('Damage Calculator'), + ), + ], + selected: {CalcCards.damage}, + onSelectionChanged: (Set newSelection) { + setState(() { + // cardMod = CardMod.info; + // rightCard = newSelection.first; + });}) + ], + ); + } + } class FetchCutoffsResults{ From 868da0c304ec949c6bfffd40d0caae9cbcc6cd1f Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 23 Sep 2024 01:17:41 +0300 Subject: [PATCH 32/86] man.......... --- lib/views/main_view_tiles.dart | 143 +++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 33 deletions(-) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 2e42157..6eb8d16 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -235,9 +235,15 @@ class ClearData{ final bool miniSpin; final bool spin; bool perfectClear = false; + int id = -1; ClearData(this.title, this.lineclear, this.lines, this.miniSpin, this.spin); + ClearData cloneWith(int i){ + ClearData newOne = ClearData(title, lineclear, lines, miniSpin, spin)..id = i; + return newOne; + } + bool get difficultClear { if (lines == 0) return false; if (lines >= 4 || miniSpin || spin) return true; @@ -251,6 +257,7 @@ class ClearData{ int dealsDamage(int combo, int b2b, ComboTables table,){ if (lines == 0) return 0; double damage = 0; + if (spin){ if (lines <= 5) damage += garbage[lineclear]!; else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); @@ -260,12 +267,37 @@ class ClearData{ if (lines <= 5) damage += garbage[lineclear]!; else damage += garbage[Lineclears.PENTA]! + (lines - 5); } - if (difficultClear && b2b > 0){ - damage += BACKTOBACK_BONUS * ((1 + log((b2b - 1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b - 1 == 1 ? 0 : (1 + log((b2b - 1) * BACKTOBACK_BONUS_LOG) % 1) / 3));; + + // Ok i can't figure out how b2b and combo works + + // From tetrio.js: + // const n = e.cm.constants.garbage.BACKTOBACK_BONUS * (Math.floor(1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG)) + (t.stats.btb - 1 == 1 ? 0 : (1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG) % 1) / 3)); + // if (h && (d += n), + + if (difficultClear && b2b >= 1){ + if (true) damage += BACKTOBACK_BONUS * ((1 + log((b2b+1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b+1 == 1 ? 0 : (1 + log((b2b+1) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? + else damage += 1; // if b2b chaining off } - if (combo > 1){ - damage += combotable[table]![max(0, min(combo - 2, combotable[table]!.length - 1))]; + + // From tetrio.js: + // if (t.stats.combo > 1) + // if (p += e.cm.constants.scoring.COMBO * (t.stats.combo - 1), + // "multiplier" === t.setoptions.combotable) + // d *= 1 + e.cm.constants.garbage.COMBO_BONUS * (t.stats.combo - 1), + // t.stats.combo > 2 && (d = Math.max(Math.log1p(e.cm.constants.garbage.COMBO_MINIFIER * (t.stats.combo - 1) * e.cm.constants.garbage.COMBO_MINIFIER_LOG), d)); + // else { + // const n = e.cm.constants.garbage.combotable[t.setoptions.combotable] || [0]; + // d += n[Math.max(0, Math.min(t.stats.combo - 2, n.length - 1))] + // } + + if (combo >= 1){ + if (lines == 1) damage += combotable[table]![max(0, min(combo - 1, combotable[table]!.length - 1))]; + else damage *= 1 + COMBO_BONUS * (combo - 1); } + if (combo > 2) { + damage = max(log(COMBO_MINIFIER * (combo - 1) * COMBO_MINIFIER_LOG), damage); + } + return damage.floor(); } } @@ -282,14 +314,14 @@ Map> clearsExisting = { ClearData("Spin Zero", Lineclears.TSPIN, 0, false, true), ClearData("Spin Single", Lineclears.TSPIN_SINGLE, 1, false, true), ClearData("Spin Double", Lineclears.TSPIN_DOUBLE, 2, false, true), - ClearData("Spin Spin Triple", Lineclears.TSPIN_TRIPLE, 3, false, true), - ClearData("Spin Spin Quad", Lineclears.TSPIN_QUAD, 4, false, true), + ClearData("Spin Triple", Lineclears.TSPIN_TRIPLE, 3, false, true), + ClearData("Spin Quad", Lineclears.TSPIN_QUAD, 4, false, true), ], "Mini spins": [ ClearData("Mini Spin Zero", Lineclears.TSPIN_MINI, 0, true, false), ClearData("Mini Spin Single", Lineclears.TSPIN_MINI_SINGLE, 1, true, false), ClearData("Mini Spin Double", Lineclears.TSPIN_MINI_DOUBLE, 2, true, false), - ClearData("Mini Spin Spin Triple", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false), + ClearData("Mini Spin Triple", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false), ] }; @@ -305,8 +337,7 @@ class _DestinationCalculatorState extends State { TextEditingController vsController = TextEditingController(); List clears = []; - int combo = -1; - int b2b = -1; + int idCounter = 0; @override void initState() { @@ -411,23 +442,71 @@ class _DestinationCalculatorState extends State { Widget getDamageCalculator(){ List rSideWidgets = []; + List lSideWidgets = []; + for (var key in clearsExisting.keys){ rSideWidgets.add(Text(key)); for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( child: ListTile( - title: Text(data.title), - subtitle: Text("${data.dealsDamage(0, 0, ComboTables.modern)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), - trailing: Icon(Icons.arrow_forward_ios), - onTap: (){ - setState((){ - clears.add(data); - }); - }, - ), + title: Text(data.title), + subtitle: Text("${data.dealsDamage(0, 0, ComboTables.modern)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + trailing: Icon(Icons.arrow_forward_ios), + onTap: (){ + setState((){ + clears.add(data.cloneWith(idCounter)); + }); + idCounter++; + }, + ), )); - rSideWidgets.add(Text("Custom")); + if (key != "Mini spins") rSideWidgets.add(Text("Custom")); rSideWidgets.add(const Divider(color: Color.fromARGB(50, 158, 158, 158))); } + + int combo = -1; + int b2b = -1; + double totalDamage = 0; + + for (ClearData lineclear in clears){ + if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; + if (lineclear.lines > 0) combo++; else combo = -1; + int dmg = lineclear.dealsDamage(combo, b2b, ComboTables.modern); + lSideWidgets.add( + ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)), + IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.pregnant_woman)), + ], + ), + title: Text("${lineclear.title}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), + subtitle: Text("${dmg} damage${lineclear.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + trailing: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), + ) + ); + totalDamage += dmg; + } + lSideWidgets.add(Divider()); + lSideWidgets.add( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 24.0, 0.0), + child: Row( + children: [ + Text("Total damage:"), + Spacer(), + Text(totalDamage.floor().toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) + ], + ), + ), + ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) + ], + ) + ); + return Column( children: [ Card( @@ -441,6 +520,7 @@ class _DestinationCalculatorState extends State { )), ), Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 350.0, @@ -481,20 +561,17 @@ class _DestinationCalculatorState extends State { ), SizedBox( width: widget.constraints.maxWidth - 350 - 80, - child: Card( - child: Column( - children: [ - Column( - children: [for (ClearData data in clears) ListTile( - title: Text(data.title), - subtitle: Text("${data.dealsDamage(0, 0, ComboTables.modern)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), - trailing: Text(data.dealsDamage(0, 0, ComboTables.modern).toString()), - onTap: (){ - clears.add(data); - }, - )], - ) - ], + height: widget.constraints.maxHeight - 148, + child: clears.isEmpty ? Center(child: Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center)) : + SingleChildScrollView( + child: Card( + child: Column( + children: [ + Column( + children: lSideWidgets, + ) + ], + ), ), ), ) From 0b01fe80b4cf5912de5427e1f7d15d2ed89f5c93 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 25 Sep 2024 01:57:37 +0300 Subject: [PATCH 33/86] Damage calculator items are now draggable --- lib/views/main_view_tiles.dart | 73 ++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 6eb8d16..408ae91 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -298,6 +298,8 @@ class ClearData{ damage = max(log(COMBO_MINIFIER * (combo - 1) * COMBO_MINIFIER_LOG), damage); } + if (perfectClear) damage += 10; + return damage.floor(); } } @@ -473,39 +475,24 @@ class _DestinationCalculatorState extends State { int dmg = lineclear.dealsDamage(combo, b2b, ComboTables.modern); lSideWidgets.add( ListTile( + key: ValueKey(lineclear.id), leading: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)), - IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.pregnant_woman)), + IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.pregnant_woman)), ], ), - title: Text("${lineclear.title}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), + title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), subtitle: Text("${dmg} damage${lineclear.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), - trailing: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), + trailing: Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), + ), ) ); totalDamage += dmg; } - lSideWidgets.add(Divider()); - lSideWidgets.add( - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 24.0, 0.0), - child: Row( - children: [ - Text("Total damage:"), - Spacer(), - Text(totalDamage.floor().toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) - ], - ), - ), - ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) - ], - ) - ); return Column( children: [ @@ -563,15 +550,41 @@ class _DestinationCalculatorState extends State { width: widget.constraints.maxWidth - 350 - 80, height: widget.constraints.maxHeight - 148, child: clears.isEmpty ? Center(child: Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center)) : - SingleChildScrollView( - child: Card( - child: Column( - children: [ - Column( + Card( + child: Column( + children: [ + Expanded( + child: ReorderableListView( + onReorder: (oldIndex, newIndex) { + setState((){ + if (oldIndex < newIndex) { + newIndex -= 1; + } + final ClearData item = clears.removeAt(oldIndex); + clears.insert(newIndex, item); + }); + }, children: lSideWidgets, - ) - ], - ), + ), + ), + Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 34.0, 0.0), + child: Row( + children: [ + Text("Total damage:"), + Spacer(), + Text(totalDamage.floor().toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) + ], + ), + ), + ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) + ], + ) + ], ), ), ) From 3d0b79bbde29d1470d110f0b2a6ac868043d250a Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 26 Sep 2024 01:55:49 +0300 Subject: [PATCH 34/86] Not fully functioning rules for damage calculator --- lib/data_objects/tetrio_constants.dart | 5 +- lib/views/main_view_tiles.dart | 184 +++++++++++++++++++++---- 2 files changed, 160 insertions(+), 29 deletions(-) diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index 1fa15b6..934463d 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -220,6 +220,8 @@ List seasonEnds = [ /// Stolen directly from TETR.IO, redone for the sake of me +const List clearNames = ["Zero", "Single", "Double", "Triple", "Quad", "Penta", "Hexa", "Hepta", "Octa", "Ennea", "Deca", "Hendeca", "Dodeca", "Triadeca", "Tessaradeca", "Pentedeca", "Hexadeca", "Heptadeca", "Octadeca", "Enneadeca", "Eicosa", "Kagaris"]; + enum Lineclears{ ZERO, SINGLE, @@ -243,7 +245,8 @@ enum Lineclears{ enum ComboTables{ none, classic, - modern + modern, + multiplier } const int BACKTOBACK_BONUS = 1; diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 408ae91..c82599b 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; +import 'package:flutter/services.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; @@ -228,6 +229,27 @@ enum CalcCards{ damage } +// class Damage{ +// final int clear; +// final int combo; +// final int b2b; +// final int surge; +// final int pc; + + + +// const Damage(this.clear, this.combo, this.b2b, this.surge, this.pc); + +// Damage operator+(Damage other){ +// return Damage( +// clear+other.clear, +// combo+other.combo, +// b2b+other.b2b, +// surge+other.b2b, +// pc+other.pc); +// } +// } + class ClearData{ final String title; final Lineclears lineclear; @@ -254,7 +276,7 @@ class ClearData{ perfectClear = !perfectClear; } - int dealsDamage(int combo, int b2b, ComboTables table,){ + int dealsDamage(int combo, int b2b, Rules rules){ if (lines == 0) return 0; double damage = 0; @@ -274,8 +296,8 @@ class ClearData{ // const n = e.cm.constants.garbage.BACKTOBACK_BONUS * (Math.floor(1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG)) + (t.stats.btb - 1 == 1 ? 0 : (1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG) % 1) / 3)); // if (h && (d += n), - if (difficultClear && b2b >= 1){ - if (true) damage += BACKTOBACK_BONUS * ((1 + log((b2b+1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b+1 == 1 ? 0 : (1 + log((b2b+1) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? + if (difficultClear && b2b >= 1 && rules.b2b){ + if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log((b2b+1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b+1 == 1 ? 0 : (1 + log((b2b+1) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? else damage += 1; // if b2b chaining off } @@ -290,17 +312,19 @@ class ClearData{ // d += n[Math.max(0, Math.min(t.stats.combo - 2, n.length - 1))] // } - if (combo >= 1){ - if (lines == 1) damage += combotable[table]![max(0, min(combo - 1, combotable[table]!.length - 1))]; - else damage *= 1 + COMBO_BONUS * (combo - 1); - } - if (combo > 2) { - damage = max(log(COMBO_MINIFIER * (combo - 1) * COMBO_MINIFIER_LOG), damage); + if (rules.combo) { + if (combo >= 1){ + if (lines == 1) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; + else damage *= 1 + COMBO_BONUS * (combo - 1); + } + if (combo > 2) { + damage = max(log(COMBO_MINIFIER * (combo - 1) * COMBO_MINIFIER_LOG), damage); + } } - if (perfectClear) damage += 10; + if (perfectClear) damage += rules.pcDamage; - return damage.floor(); + return (damage * rules.multiplier).floor(); } } @@ -327,6 +351,21 @@ Map> clearsExisting = { ] }; +class Rules{ + bool combo = true; + bool b2b = true; + bool b2bChaining = false; + bool surge = true; + int surgeInitAmount = 4; + int surgeInitAtB2b = 4; + ComboTables comboTable = ComboTables.multiplier; + int pcDamage = 5; + int pcB2B = 1; + double multiplier = 1.0; +} + +const TextStyle mainToggleInRules = TextStyle(fontSize: 18, fontWeight: ui.FontWeight.w800); + class _DestinationCalculatorState extends State { double? apm; double? pps; @@ -339,7 +378,12 @@ class _DestinationCalculatorState extends State { TextEditingController vsController = TextEditingController(); List clears = []; + Map customClearsChoice = { + "No Spin Clears": 5, + "Spins": 5 + }; int idCounter = 0; + Rules rules = Rules(); @override void initState() { @@ -451,7 +495,7 @@ class _DestinationCalculatorState extends State { for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( child: ListTile( title: Text(data.title), - subtitle: Text("${data.dealsDamage(0, 0, ComboTables.modern)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + subtitle: Text("${data.dealsDamage(0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), trailing: Icon(Icons.arrow_forward_ios), onTap: (){ setState((){ @@ -461,18 +505,41 @@ class _DestinationCalculatorState extends State { }, ), )); - if (key != "Mini spins") rSideWidgets.add(Text("Custom")); + if (key != "Mini spins") rSideWidgets.add(Card( + child: ListTile( + title: Text("Custom"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 30.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: "5"), + onChanged: (value) => customClearsChoice[key] = int.parse(value), + )), + Text(" Lines", style: TextStyle(fontSize: 18)), + Icon(Icons.arrow_forward_ios) + ], + ), + onTap: (){ + setState((){ + clears.add(ClearData("${key == "Spins" ? "Spin " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} Lines)", key == "Spins" ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, customClearsChoice[key]!, false, key == "Spins").cloneWith(idCounter)); + }); + idCounter++; + }, + ), + )); rSideWidgets.add(const Divider(color: Color.fromARGB(50, 158, 158, 158))); } int combo = -1; int b2b = -1; - double totalDamage = 0; + int totalDamage = 0; for (ClearData lineclear in clears){ if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; if (lineclear.lines > 0) combo++; else combo = -1; - int dmg = lineclear.dealsDamage(combo, b2b, ComboTables.modern); + int dmg = lineclear.dealsDamage(combo, b2b, rules); lSideWidgets.add( ListTile( key: ValueKey(lineclear.id), @@ -480,15 +547,15 @@ class _DestinationCalculatorState extends State { mainAxisSize: MainAxisSize.min, children: [ IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)), - IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.pregnant_woman)), + if (lineclear.lines > 0) IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.local_parking_outlined, color: lineclear.perfectClear ? Colors.white : Colors.grey.shade800)), ], ), title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), - subtitle: Text("${dmg} damage${lineclear.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), - trailing: Padding( + subtitle: lineclear.lines > 0 ? Text("${dmg} damage${lineclear.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)) : null, + trailing: lineclear.lines > 0 ? Padding( padding: const EdgeInsets.only(right: 10.0), child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), - ), + ) : null, ) ); totalDamage += dmg; @@ -530,14 +597,75 @@ class _DestinationCalculatorState extends State { ), ), SingleChildScrollView( - child: Card( - child: Column( - children: [ - ListTile( - title: Text("Doubles"), - ) - ], - ), + child: Column( + children: [ + Card( + child: ListTile( + title: Text("Multiplier", style: mainToggleInRules), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))], + decoration: InputDecoration(hintText: rules.multiplier.toString()), + onChanged: (value) => setState((){rules.multiplier = double.parse(value);}), + )), + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Combo", style: mainToggleInRules), + trailing: Switch(value: rules.combo, onChanged: (v) => setState((){rules.combo = v;})), + ), + if (rules.combo) ListTile( + title: Text("Combo Table"), + ) + ], + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Back-To-Back (B2B)", style: mainToggleInRules), + trailing: Switch(value: rules.b2b, onChanged: (v) => setState((){rules.b2b = v;})), + ), + if (rules.b2b) ListTile( + title: Text("Back-To-Back Chaining"), + trailing: Switch(value: rules.b2bChaining, onChanged: (v) => setState((){rules.b2bChaining = v;})), + ), + ], + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Surge", style: mainToggleInRules), + trailing: Switch(value: rules.surge, onChanged: (v) => setState((){rules.surge = v;})), + ), + if (rules.surge) ListTile( + title: Text("Starts at B2B"), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: rules.surgeInitAtB2b.toString()), + onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}), + )), + ), + if (rules.surge) ListTile( + title: Text("Start amount"), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: rules.surgeInitAmount.toString()), + onChanged: (value) => setState((){rules.surgeInitAmount = int.parse(value);}), + )), + ), + ], + ), + ) + ], ), ) ]), @@ -575,7 +703,7 @@ class _DestinationCalculatorState extends State { padding: const EdgeInsets.fromLTRB(16.0, 0.0, 34.0, 0.0), child: Row( children: [ - Text("Total damage:"), + Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), Spacer(), Text(totalDamage.floor().toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) ], From 24ad72c029b4b0b5b7cff81792e2ad51a748c9d8 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 27 Sep 2024 01:14:45 +0300 Subject: [PATCH 35/86] I guess i made it worse --- lib/views/main_view_tiles.dart | 94 +++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index c82599b..a3658b3 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -229,26 +229,33 @@ enum CalcCards{ damage } -// class Damage{ -// final int clear; -// final int combo; -// final int b2b; -// final int surge; -// final int pc; +class Damage{ + final int clear; + final double combo; + final double b2b; + final int surge; + final int pc; + final double multiplier; + int get total => ((clear + combo + b2b + surge + pc) * multiplier).floor(); + const Damage(this.clear, this.combo, this.b2b, this.surge, this.pc, this.multiplier); -// const Damage(this.clear, this.combo, this.b2b, this.surge, this.pc); + @override + String toString(){ + return total.toString(); + } -// Damage operator+(Damage other){ -// return Damage( -// clear+other.clear, -// combo+other.combo, -// b2b+other.b2b, -// surge+other.b2b, -// pc+other.pc); -// } -// } + Damage operator+(Damage other){ + return Damage( + clear+other.clear, + combo+other.combo, + b2b+other.b2b, + surge+other.surge, + pc+other.pc, + multiplier); + } +} class ClearData{ final String title; @@ -276,18 +283,18 @@ class ClearData{ perfectClear = !perfectClear; } - int dealsDamage(int combo, int b2b, Rules rules){ - if (lines == 0) return 0; - double damage = 0; + Damage dealsDamage(int combo, int b2b, Rules rules){ + if (lines == 0) return Damage(0,0,0,0,0,rules.multiplier); + int clearDamage = 0; if (spin){ - if (lines <= 5) damage += garbage[lineclear]!; - else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); + if (lines <= 5) clearDamage += garbage[lineclear]!; + else clearDamage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); } else if (miniSpin){ - damage += garbage[lineclear]!; + clearDamage += garbage[lineclear]!; } else { - if (lines <= 5) damage += garbage[lineclear]!; - else damage += garbage[Lineclears.PENTA]! + (lines - 5); + if (lines <= 5) clearDamage += garbage[lineclear]!; + else clearDamage += garbage[Lineclears.PENTA]! + (lines - 5); } // Ok i can't figure out how b2b and combo works @@ -296,9 +303,17 @@ class ClearData{ // const n = e.cm.constants.garbage.BACKTOBACK_BONUS * (Math.floor(1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG)) + (t.stats.btb - 1 == 1 ? 0 : (1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG) % 1) / 3)); // if (h && (d += n), + double b2bDamage = 0; + if (difficultClear && b2b >= 1 && rules.b2b){ - if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log((b2b+1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b+1 == 1 ? 0 : (1 + log((b2b+1) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? - else damage += 1; // if b2b chaining off + if (rules.b2bChaining) b2bDamage += BACKTOBACK_BONUS * ((1 + log((b2b+1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b+1 == 1 ? 0 : (1 + log((b2b+1) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? + else b2bDamage += 1; // if b2b chaining off + } + + int surgeDamage = 0; + + if (!difficultClear && rules.surge){ + } // From tetrio.js: @@ -312,19 +327,23 @@ class ClearData{ // d += n[Math.max(0, Math.min(t.stats.combo - 2, n.length - 1))] // } + double comboDamage = 0; + if (rules.combo) { - if (combo >= 1){ - if (lines == 1) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; - else damage *= 1 + COMBO_BONUS * (combo - 1); + if (combo > 1){ + if (lines == 1 && rules.comboTable != ComboTables.multiplier) comboDamage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; + else comboDamage = (clearDamage + b2bDamage) * (1 + COMBO_BONUS * (combo-1)); } if (combo > 2) { - damage = max(log(COMBO_MINIFIER * (combo - 1) * COMBO_MINIFIER_LOG), damage); + comboDamage = max(log(COMBO_MINIFIER * (combo-1) * COMBO_MINIFIER_LOG), comboDamage); } } - if (perfectClear) damage += rules.pcDamage; + int pcDamage = 0; - return (damage * rules.multiplier).floor(); + if (perfectClear) pcDamage += rules.pcDamage; + + return Damage(clearDamage, comboDamage, b2bDamage, surgeDamage, pcDamage, rules.multiplier); } } @@ -534,12 +553,13 @@ class _DestinationCalculatorState extends State { int combo = -1; int b2b = -1; - int totalDamage = 0; + Damage totalDamage = Damage(0,0,0,0,0,rules.multiplier); + int totalDamageNumber = 0; for (ClearData lineclear in clears){ if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; if (lineclear.lines > 0) combo++; else combo = -1; - int dmg = lineclear.dealsDamage(combo, b2b, rules); + Damage dmg = lineclear.dealsDamage(combo, b2b, rules); lSideWidgets.add( ListTile( key: ValueKey(lineclear.id), @@ -551,7 +571,7 @@ class _DestinationCalculatorState extends State { ], ), title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), - subtitle: lineclear.lines > 0 ? Text("${dmg} damage${lineclear.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)) : null, + subtitle: lineclear.lines > 0 ? Text("${dmg.clear} from clear, ${dmg.combo} from combo, ${dmg.b2b} from B2B, ${dmg.surge} from Surge and ${dmg.pc} from PC", style: TextStyle(color: Colors.grey)) : null, trailing: lineclear.lines > 0 ? Padding( padding: const EdgeInsets.only(right: 10.0), child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), @@ -559,6 +579,7 @@ class _DestinationCalculatorState extends State { ) ); totalDamage += dmg; + totalDamageNumber += dmg.total; } return Column( @@ -705,10 +726,11 @@ class _DestinationCalculatorState extends State { children: [ Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), Spacer(), - Text(totalDamage.floor().toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) + Text(totalDamageNumber.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) ], ), ), + Text(totalDamage.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)), ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) ], ) From 1350007e1d7e87efc0dad666c9b8afb3221a9f86 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 27 Sep 2024 01:34:27 +0300 Subject: [PATCH 36/86] Ok i will move on from calculator from now on --- lib/views/main_view_tiles.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index a3658b3..8c3a335 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -328,7 +328,7 @@ class ClearData{ // } double comboDamage = 0; - + if (rules.combo) { if (combo > 1){ if (lines == 1 && rules.comboTable != ComboTables.multiplier) comboDamage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; @@ -404,6 +404,8 @@ class _DestinationCalculatorState extends State { int idCounter = 0; Rules rules = Rules(); + CalcCards card = CalcCards.calc; + @override void initState() { super.initState(); @@ -750,7 +752,10 @@ class _DestinationCalculatorState extends State { children: [ SizedBox( height: widget.constraints.maxHeight -32, - child: getDamageCalculator(), + child: switch (card){ + CalcCards.calc => getCalculator(), + CalcCards.damage => getDamageCalculator() + } ), SegmentedButton( showSelectedIcon: false, @@ -764,11 +769,10 @@ class _DestinationCalculatorState extends State { label: Text('Damage Calculator'), ), ], - selected: {CalcCards.damage}, + selected: {card}, onSelectionChanged: (Set newSelection) { setState(() { - // cardMod = CardMod.info; - // rightCard = newSelection.first; + card = newSelection.first; });}) ], ); From af1dec56bc7ee92e0d73727aef2ffca94c1cc5e3 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 30 Sep 2024 01:02:19 +0300 Subject: [PATCH 37/86] Error message design --- lib/services/tetrio_crud.dart | 7 +- lib/views/main_view_tiles.dart | 154 +++++++++++++++++++++++++++---- lib/widgets/tl_progress_bar.dart | 2 +- 3 files changed, 142 insertions(+), 21 deletions(-) diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index be4466f..f85ed42 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -798,7 +798,7 @@ class TetrioService extends DB { } } - Future> fetchTetrioLeaderboard({String? prisecter, String? lb}) async { + Future> fetchTetrioLeaderboard({String? prisecter, String? lb, String? country}) async { // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); // if (cached != null) return cached; @@ -808,7 +808,8 @@ class TetrioService extends DB { } else { url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', { "limit": "100", - if (prisecter != null) "after": prisecter + if (prisecter != null) "after": prisecter, + if (country != null) "country": country }); } try{ @@ -1337,7 +1338,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: - var json = jsonDecode(response.body); + var json = jsonDecode(utf8.decode(response.bodyBytes)); if (json['success']) { // parse and count stats TetrioPlayer player = TetrioPlayer.fromJson(json['data'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['_id'], json['data']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true)); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 8c3a335..211567e 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart' hide Badge; import 'package:flutter/services.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:http/http.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; @@ -1070,6 +1071,7 @@ class DestinationLeaderboards extends StatefulWidget{ enum Leaderboards{ tl, + fullTL, xp, ar, sprint, @@ -1081,7 +1083,8 @@ enum Leaderboards{ class _DestinationLeaderboardsState extends State { //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); final Map leaderboards = { - Leaderboards.tl: "Tetra League", + Leaderboards.tl: "Tetra League (Current Season)", + Leaderboards.fullTL: "Tetra League (Current Season, full one)", Leaderboards.xp: "XP", Leaderboards.ar: "Acievement Points", Leaderboards.sprint: "40 Lines", @@ -1090,7 +1093,7 @@ class _DestinationLeaderboardsState extends State { Leaderboards.zenithex: "Quick Play Expert", }; Leaderboards _currentLb = Leaderboards.tl; - final StreamController> _dataStreamController = StreamController>(); + final StreamController> _dataStreamController = StreamController>.broadcast(); late final ScrollController _scrollController; Stream> get dataStream => _dataStreamController.stream; List list = []; @@ -1108,6 +1111,7 @@ class _DestinationLeaderboardsState extends State { final items = switch(_currentLb){ Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter), + Leaderboards.fullTL => (await teto.fetchTLLeaderboard()).leaderboard, Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp"), Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar"), Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter), @@ -1139,7 +1143,7 @@ class _DestinationLeaderboardsState extends State { final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.position.pixels; - if (currentScroll == maxScroll) { + if (currentScroll == maxScroll && _currentLb != Leaderboards.fullTL) { // When the last item is fully visible, load the next page. _fetchData(); } @@ -1147,6 +1151,8 @@ class _DestinationLeaderboardsState extends State { }); } + static TextStyle trailingStyle = TextStyle(fontSize: 28); + @override Widget build(BuildContext context) { return Row( @@ -1171,9 +1177,10 @@ class _DestinationLeaderboardsState extends State { itemCount: leaderboards.length, itemBuilder: (BuildContext context, int index) { return Card( - surfaceTintColor: theme.colorScheme.primary, + surfaceTintColor: index == 1 ? Colors.redAccent : theme.colorScheme.primary, child: ListTile( title: Text(leaderboards.values.elementAt(index)), + subtitle: index == 1 ? Text("Heavy, but allows you to sort players by their stats", style: TextStyle(color: Colors.grey, fontSize: 12)) : null, onTap: () { _currentLb = leaderboards.keys.elementAt(index); list.clear(); @@ -1209,24 +1216,44 @@ class _DestinationLeaderboardsState extends State { child: ListView.builder( controller: _scrollController, itemCount: list.length, + prototypeItem: ListTile( + leading: Text("0"), + title: Text("ehhh...", style: TextStyle(fontSize: 22)), + trailing: SizedBox(height: 36, width: 1), + subtitle: const Text("eh...", style: TextStyle(color: Colors.grey, fontSize: 12)), + ), itemBuilder: (BuildContext context, int index){ return ListTile( leading: Text(intf.format(index+1)), title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), - trailing: Text(switch (_currentLb){ - Leaderboards.tl => "${f2.format(snapshot.data![index].tr)} TR", - Leaderboards.xp => "LVL ${f2.format(snapshot.data![index].level)}", - Leaderboards.ar => "${intf.format(snapshot.data![index].ar)} AR", - Leaderboards.sprint => get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), - Leaderboards.blitz => intf.format(snapshot.data![index].stats.score), - Leaderboards.zenith => "${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", - Leaderboards.zenithex => "${f2.format(snapshot.data![index].stats.zenith!.altitude)} m" - }, style: TextStyle(fontSize: 28)), + trailing: switch (_currentLb){ + Leaderboards.tl => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), + Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) + ], + ), + Leaderboards.fullTL => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), + Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) + ], + ), + Leaderboards.xp => Text("LVL ${f2.format(snapshot.data![index].level)}", style: trailingStyle), + Leaderboards.ar => Text("${intf.format(snapshot.data![index].ar)} AR", style: trailingStyle), + Leaderboards.sprint => Text(get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), style: trailingStyle), + Leaderboards.blitz => Text(intf.format(snapshot.data![index].stats.score), style: trailingStyle), + Leaderboards.zenith => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle), + Leaderboards.zenithex => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle) + }, subtitle: Text(switch (_currentLb){ Leaderboards.tl => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", + Leaderboards.fullTL => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", Leaderboards.xp => "${f2.format(snapshot.data![index].xp)} XP${snapshot.data![index].playtime.isNegative ? "" : ", ${playtime(snapshot.data![index].playtime)} of gametime"}", Leaderboards.ar => "${snapshot.data![index].ar_counts}", - Leaderboards.sprint => "${intf.format(snapshot.data![index].stats.finesse.faults)} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${intf.format(snapshot.data![index].stats.piecesPlaced)} P", + Leaderboards.sprint => "${intf.format(snapshot.data![index].stats.finesse.faults)} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.kps)} KPS, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${intf.format(snapshot.data![index].stats.piecesPlaced)} P", Leaderboards.blitz => "lvl ${snapshot.data![index].stats.level}, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${f2.format(snapshot.data![index].stats.spp)} SPP", Leaderboards.zenith => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B", Leaderboards.zenithex => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B" @@ -2662,6 +2689,7 @@ class _DestinationHomeState extends State with SingleTickerProv case ConnectionState.done: if (snapshot.hasError){ return FutureError(snapshot); } if (snapshot.hasData){ + if (!snapshot.data!.success) return FetchResultError(snapshot.data!); blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; if (snapshot.data!.summaries!.sprint != null) { @@ -4208,17 +4236,109 @@ class FutureError extends StatelessWidget{ @override Widget build(BuildContext context) { - return Center(child: - Column( + return TweenAnimationBuilder( + duration: Durations.medium3, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(0, 50-value*50, 0), + child: Opacity(opacity: value, child: child), + ); + }, + child: Column( mainAxisSize: MainAxisSize.min, children: [ + Spacer(), + Icon(Icons.error_outline, size: 128.0, color: Colors.red, shadows: [ + Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red), + Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red), + ]), Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 8.0), child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), ), + Spacer() ], - ) + ), + ); + } +} + +class FetchResultError extends StatelessWidget{ + final FetchResults data; + + FetchResultError(this.data); + + @override + Widget build(BuildContext context) { + IconData icon = Icons.error_outline; + String errText = ""; + String? subText; + switch (data.exception.runtimeType){ + case TetrioPlayerNotExist: + icon = Icons.search_off; + errText = t.errors.noSuchUser; + subText = t.errors.noSuchUserSub; + break; + case TetrioDiscordNotExist: + icon = Icons.search_off; + errText = t.errors.discordNotAssigned; + subText = t.errors.discordNotAssignedSub; + case ConnectionIssue: + var err = data.exception as ConnectionIssue; + errText = t.errors.connection(code: err.code, message: err.message); + break; + case TetrioForbidden: + icon = Icons.remove_circle; + 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; + break; + default: + errText = data.exception.toString(); + } + return TweenAnimationBuilder( + duration: Durations.medium3, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(0, 50-value*50, 0), + child: Opacity(opacity: value, child: child), + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Icon(icon, size: 128.0, color: Colors.red, shadows: [ + Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red), + Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red), + ]), + 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, textAlign: TextAlign.center), + ), + Spacer() + ], + ), ); } } \ No newline at end of file diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index 9e5c492..bd46a4d 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -79,7 +79,7 @@ class TLProgress extends StatelessWidget{ ], markerPointers: [ LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), - //if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) + if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) ], isMirrored: true, showTicks: true, From ba78d50f216f6b69059c7f357e920bfad40a998c Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 2 Oct 2024 00:46:43 +0300 Subject: [PATCH 38/86] Sorting functions for LB's (works kinda meh...) --- .../tetrio_players_leaderboard.dart | 19 ++++++ lib/gen/strings.g.dart | 10 ++-- lib/services/tetrio_crud.dart | 5 +- lib/views/main_view_tiles.dart | 60 ++++++++++++++++--- res/i18n/strings.i18n.json | 2 +- res/i18n/strings_ru.i18n.json | 2 +- 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/lib/data_objects/tetrio_players_leaderboard.dart b/lib/data_objects/tetrio_players_leaderboard.dart index 1d2305d..91e3336 100644 --- a/lib/data_objects/tetrio_players_leaderboard.dart +++ b/lib/data_objects/tetrio_players_leaderboard.dart @@ -38,6 +38,25 @@ class TetrioPlayersLeaderboard { return lb; } + List getStatRankingFromLB(Stats stat, {bool reversed = false, String country = ""}){ + List lb = List.from(leaderboard); + if (country.isNotEmpty){ + lb.removeWhere((element) => element.country != country); + } + lb.sort(((a, b) { + if (a.getStatByEnum(stat).isNaN) return 1; + if (b.getStatByEnum(stat).isNaN) return -1; + if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){ + return reversed ? 1 : -1; + }else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){ + return 0; + }else{ + return reversed ? -1 : 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); diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 12ea442..c34c1f6 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 3 /// Strings: 1818 (606 per locale) /// -/// Built on 2024-09-12 at 20:23 UTC +/// Built on 2024-09-30 at 21:23 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -396,7 +396,7 @@ class Translations implements BaseTranslations { late final _StringsPopupActionsEn popupActions = _StringsPopupActionsEn._(_root); late final _StringsErrorsEn errors = _StringsErrorsEn._(_root); Map get countries => { - '': 'Not selected', + '': 'Worldwide', 'AF': 'Afghanistan', 'AX': 'Åland Islands', 'AL': 'Albania', @@ -1108,7 +1108,7 @@ class _StringsRu implements Translations { @override late final _StringsPopupActionsRu popupActions = _StringsPopupActionsRu._(_root); @override late final _StringsErrorsRu errors = _StringsErrorsRu._(_root); @override Map get countries => { - '': 'Не выбрана', + '': 'Во всём мире', 'AF': 'Афганистан', 'AX': 'Аландские острова', 'AL': 'Албания', @@ -2628,7 +2628,7 @@ extension on Translations { 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'; - case 'countries.': return 'Not selected'; + case 'countries.': return 'Worldwide'; case 'countries.AF': return 'Afghanistan'; case 'countries.AX': return 'Åland Islands'; case 'countries.AL': return 'Albania'; @@ -3256,7 +3256,7 @@ extension on _StringsRu { case 'errors.replayAlreadySaved': return 'Повтор уже сохранён'; case 'errors.replayExpired': return 'Повтор истёк и больше недоступен'; case 'errors.replayRejected': return 'Стороннее API заблокировало ваш IP адрес'; - case 'countries.': return 'Не выбрана'; + case 'countries.': return 'Во всём мире'; case 'countries.AF': return 'Афганистан'; case 'countries.AX': return 'Аландские острова'; case 'countries.AL': return 'Албания'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index f85ed42..c74c16b 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -853,14 +853,15 @@ class TetrioService extends DB { } } - Future> fetchTetrioRecordsLeaderboard({String? prisecter, String? lb}) async{ + Future> fetchTetrioRecordsLeaderboard({String? prisecter, String? lb, String? country}) async{ Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); } else { url = Uri.https('ch.tetr.io', 'api/records/${lb??"40l_global"}', { "limit": "100", - if (prisecter != null) "after": prisecter + if (prisecter != null) "after": prisecter, + if (country != null) "country": country }); } try{ diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 211567e..4344d4a 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1099,6 +1099,10 @@ class _DestinationLeaderboardsState extends State { List list = []; bool _isFetchingData = false; String? prisecter; + List _countries = [for (MapEntry e in t.countries.entries) DropdownMenuEntry(value: e.key, label: e.value)]; + List _stats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuEntry(value: e.key, label: e.value)]; + String? _country; + Stats stat = Stats.tr; Future _fetchData() async { if (_isFetchingData) { @@ -1110,14 +1114,14 @@ class _DestinationLeaderboardsState extends State { setState(() {}); final items = switch(_currentLb){ - Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter), - Leaderboards.fullTL => (await teto.fetchTLLeaderboard()).leaderboard, - Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp"), - Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar"), - Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter), - Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global"), - Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global"), - Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global"), + Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter, country: _country), + Leaderboards.fullTL => (await teto.fetchTLLeaderboard()).getStatRankingFromLB(stat, country: _country??""), + Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp", country: _country), + Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar", country: _country), + Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, country: _country), + Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global", country: _country), + Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global", country: _country), + Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global", country: _country), }; list.addAll(items); @@ -1211,6 +1215,44 @@ class _DestinationLeaderboardsState extends State { return Column( children: [ Text(leaderboards[_currentLb]!, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownMenu( + leadingIcon: Icon(Icons.public), + inputDecorationTheme: InputDecorationTheme( + isDense: true, + ), + textStyle: TextStyle(fontSize: 14, height: 0.9), + dropdownMenuEntries: _countries, + initialSelection: "", + onSelected: ((value) { + _country = value as String?; + list.clear(); + prisecter = null; + _isFetchingData = false; + setState((){_fetchData();}); + }) + ), + if (_currentLb == Leaderboards.fullTL) SizedBox(width: 5.0), + if (_currentLb == Leaderboards.fullTL) DropdownMenu( + leadingIcon: Icon(Icons.sort), + inputDecorationTheme: InputDecorationTheme( + isDense: true, + ), + textStyle: TextStyle(fontSize: 14, height: 0.9), + dropdownMenuEntries: _stats, + initialSelection: stat, + onSelected: ((value) { + stat = value; + list.clear(); + prisecter = null; + _isFetchingData = false; + setState((){_fetchData();}); + }) + ) + ], + ), const Divider(color: Color.fromARGB(50, 158, 158, 158)), Expanded( child: ListView.builder( @@ -4257,7 +4299,7 @@ class FutureError extends StatelessWidget{ Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.left, style: TextStyle(fontFamily: "Monospace")), ), Spacer() ], diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index cabae6f..6c75715 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -377,7 +377,7 @@ "replayRejected": "Third party API blocked your IP address" }, "countries(map)": { - "": "Not selected", + "": "Worldwide", "AF": "Afghanistan", "AX": "\u00c5land Islands", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index d29f98e..9b538d0 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -377,7 +377,7 @@ "replayRejected": "Стороннее API заблокировало ваш IP адрес" }, "countries(map)": { - "": "Не выбрана", + "": "Во всём мире", "AF": "Афганистан", "AX": "Аландские острова", From 4df644f0f61e7916630f8f1bf4eeaa908a58746c Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 4 Oct 2024 00:00:41 +0300 Subject: [PATCH 39/86] Idea for compare view Time mostly spent on `AddNewColumnCard` animation That view should allow to compare multiple players --- lib/views/compare_view_tiles.dart | 307 ++++++++++++++++++++++++++++++ lib/views/main_view_tiles.dart | 15 +- 2 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 lib/views/compare_view_tiles.dart diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart new file mode 100644 index 0000000..f0eb736 --- /dev/null +++ b/lib/views/compare_view_tiles.dart @@ -0,0 +1,307 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart' show teto; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:tetra_stats/widgets/vs_graphs.dart'; +import 'package:transparent_image/transparent_image.dart'; +import 'package:vector_math/vector_math_64.dart' hide Colors; +import 'package:window_manager/window_manager.dart'; + +enum Mode{ + player, + stats, + averages +} +final DateFormat dateFormat = DateFormat.yMd(LocaleSettings.currentLocale.languageCode).add_Hm(); +var numbersReg = RegExp(r'\d+(\.\d*)*'); +late String oldWindowTitle; + +class CompareView extends StatefulWidget { + final TetrioPlayer initPlayer; + const CompareView(this.initPlayer); + + @override + State createState() => CompareState(); +} + +class CompareState extends State { + late ScrollController _scrollController; + List players = []; + List summaries = []; + + @override + void initState() { + _scrollController = ScrollController(); + players = [widget.initPlayer]; + getSummariesForInit(); + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ + windowManager.getTitle().then((value) => oldWindowTitle = value); + } + super.initState(); + } + + @override + void dispose(){ + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); + super.dispose(); + } + + void getSummariesForInit() async { + summaries[0] = await teto.fetchSummaries(widget.initPlayer.userId); + setState(() { + + }); + } + + void addPlayer(String nickname) async { + players.add(await teto.fetchPlayer(nickname)); + summaries.add(await teto.fetchSummaries(players.last.userId)); + setState(() { + + }); + } + + double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { + return ((1 / + (1 + pow(10, + (notyourGlicko - yourGlicko) / + (400 * sqrt(1 + (3 * pow(0.0057564273, 2) * + (pow(yourRD, 2) + pow(notyourRD, 2)) / pow(pi, 2) + ))) + ) + ) + )); + } + + void _justUpdate() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + bool bigScreen = MediaQuery.of(context).size.width > 768; + return Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + floatingActionButton: Padding( + padding: const EdgeInsets.all(8.0), + child: FloatingActionButton( + onPressed: () => Navigator.pop(context), + tooltip: 'Fuck go back', + child: const Icon(Icons.arrow_back), + ), + ), + body: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + child: Table( + defaultColumnWidth: FixedColumnWidth(350), + columnWidths: { + 0: FixedColumnWidth(200.000) + }, + children: [ + TableRow( + children: [ + Center(child: Text("player")), + for (var p in players) HeaderCard(p), + AddNewColumnCard(addPlayer) + ] + ), + TableRow( + children: [ + Text("Account Created"), + for (var p in players) Text(timestamp(p.registrationTime!)), + Container() + ] + ), + TableRow( + children: [ + Text("XP"), + for (var p in players) RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), + Container() + ] + ), + TableRow( + children: [ + Text("Time Played"), + for (var p in players) Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), + Container() + ] + ), + TableRow( + children: [ + Text("Online Games Played"), + for (var p in players) Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), + Container() + ] + ), + TableRow( + children: [ + Text("Online Games Won"), + for (var p in players) Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), + Container() + ] + ), + TableRow( + children: [ + Text("Followers"), + for (var p in players) Text(intf.format(p.friendCount)), + Container() + ] + ), + ], + ), + ), + ); + } +} + +class HeaderCard extends StatelessWidget{ + final TetrioPlayer player; + + const HeaderCard(this.player); + + String fontStyle(int length){ + if (length < 10) return "Eurostile Round Extended"; + else if (length < 13) return "Eurostile Round"; + else return "Eurostile Round Condensed"; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 175, + child: Card( + child: Column( + children: [ + Stack( + alignment: Alignment.topCenter, + clipBehavior: Clip.none, + children: [ + if (player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", + placeholder: kTransparentImage, + fit: BoxFit.cover, + height: 120, + fadeInCurve: Easing.standard, fadeInDuration: Durations.long4 + ), + Positioned( + top: 20.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: player.role == "banned" + ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: 128) + : player.avatarRevision != null + ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + fit: BoxFit.fitHeight, height: 128, placeholder: kTransparentImage, fadeInCurve: Easing.emphasizedDecelerate, fadeInDuration: Durations.long4) + : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: 128), + ) + ), + ], + ), + RichText( + text: TextSpan(text: player.username, style: TextStyle( + fontFamily: fontStyle(player.username.length), + fontSize: 28, + shadows: textShadow + ), + ) + ), + ], + ), + ), + ); + }} + +class AddNewColumnCard extends StatefulWidget{ + final Function addPlayer; + + const AddNewColumnCard(this.addPlayer); + + @override + State createState() => _AddNewColumnCardState(); +} + +class _AddNewColumnCardState extends State with SingleTickerProviderStateMixin { + late AnimationController _animController; + late Animation _anim; + + @override + void initState(){ + _animController = AnimationController( + duration: Durations.medium3, + vsync: this, + ); + _anim = new Tween( + begin: 0.0, + end: 1.0, + ).animate(new CurvedAnimation( + parent: _animController, + curve: Easing.standard + )); + super.initState(); + } + + @override + void dispose() { + _animController.dispose(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) { + // TODO: implement build + return SizedBox( + height: 175.0, + child: Card( + child: AnimatedBuilder( + animation: _anim, + builder: (context, child) { + return _anim.value > 0.5 ? Opacity( + opacity: _anim.value*2-1, + child: Container( + transform: Matrix4.translationValues(0, 100-(_anim.value as double)*100, 0), + child: Column( + children: [ + Text("Enter username:"), + TextField( + autofocus: true, + onSubmitted: (value){ + widget.addPlayer(value); + }, + onTapOutside: (event) { + setState((){_animController.animateBack(0);}); + }, + ) + ], + ), + ), + ) : Center( + child: IconButton( + visualDensity: VisualDensity.comfortable, + onPressed: (){setState((){_animController.forward();});}, + icon: Opacity(opacity: 1-(_anim.value as double)*2, child: Transform.translate(offset: Offset.fromDirection(pi*1.5, (_anim.value as double)*50), child: Transform.rotate(angle: (_anim.value as double)*2, child: Icon(Icons.add)))), + constraints: BoxConstraints.expand(), + ), + ); + } + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 4344d4a..a3b753b 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -39,6 +39,7 @@ import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/singleplayer_record_view.dart'; import 'package:tetra_stats/views/tl_match_view.dart'; +import 'package:tetra_stats/views/compare_view_tiles.dart'; import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/lineclears_thingy.dart'; @@ -3548,7 +3549,19 @@ class NewUserThingy extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.person_add), label: Text(t.track), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0))))))), - Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.balance), label: Text(t.compare), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))))) + Expanded( + child: ElevatedButton.icon( + onPressed: (){ + Navigator.push(context, MaterialPageRoute( + builder: (context) => CompareView(player), + ), + ); + }, + icon: const Icon(Icons.balance), + label: Text(t.compare), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))) + ) + ) ], ) ], From 52c0b6720740c5ac0fdb12aeca7f4a680ed761f8 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 6 Oct 2024 01:32:09 +0300 Subject: [PATCH 40/86] ok it's a table...??? --- lib/main.dart | 2 + lib/views/compare_view_tiles.dart | 362 ++++++++++++++++++++++++------ lib/views/main_view_tiles.dart | 24 +- 3 files changed, 313 insertions(+), 75 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a847a1e..f6b5e16 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,6 +50,8 @@ ThemeData theme = ThemeData( shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200), ) ), + dividerColor: Color.fromARGB(50, 158, 158, 158), + dividerTheme: DividerThemeData(color: Color.fromARGB(50, 158, 158, 158)), scaffoldBackgroundColor: Colors.black ); diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index f0eb736..bea3f09 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -61,7 +61,7 @@ class CompareState extends State { } void getSummariesForInit() async { - summaries[0] = await teto.fetchSummaries(widget.initPlayer.userId); + summaries.add(await teto.fetchSummaries(widget.initPlayer.userId)); setState(() { }); @@ -98,7 +98,7 @@ class CompareState extends State { return Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.startTop, floatingActionButton: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(16.0), child: FloatingActionButton( onPressed: () => Navigator.pop(context), tooltip: 'Fuck go back', @@ -106,71 +106,306 @@ class CompareState extends State { ), ), body: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - child: Table( - defaultColumnWidth: FixedColumnWidth(350), - columnWidths: { - 0: FixedColumnWidth(200.000) - }, - children: [ - TableRow( - children: [ - Center(child: Text("player")), - for (var p in players) HeaderCard(p), - AddNewColumnCard(addPlayer) - ] - ), - TableRow( - children: [ - Text("Account Created"), - for (var p in players) Text(timestamp(p.registrationTime!)), - Container() - ] - ), - TableRow( - children: [ - Text("XP"), - for (var p in players) RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), - Container() - ] - ), - TableRow( - children: [ - Text("Time Played"), - for (var p in players) Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), - Container() - ] - ), - TableRow( - children: [ - Text("Online Games Played"), - for (var p in players) Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), - Container() - ] - ), - TableRow( - children: [ - Text("Online Games Won"), - for (var p in players) Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), - Container() - ] - ), - TableRow( - children: [ - Text("Followers"), - for (var p in players) Text(intf.format(p.friendCount)), - Container() - ] - ), - ], + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: 200.0, height: 175.0, child: Expanded(child: Card(child: Padding( + padding: const EdgeInsets.fromLTRB(80.0, 10.0, 5.0, 0), + child: Text("Comparison", style: TextStyle(fontSize: 20)), + ), + )) + ), + for (var p in players) SizedBox(width: 350.0, child: HeaderCard(p)), + SizedBox(width: 350.0, child: AddNewColumnCard(addPlayer)) + ] + ), + Table( + border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), + defaultColumnWidth: FixedColumnWidth(350), + columnWidths: { + 0: FixedColumnWidth(200.000) + }, + children: [ + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Account Created")), + for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(timestamp(p.registrationTime!))) + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("XP")), + for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))]))) + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Time Played")), + for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white))) + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Played")), + for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Won")), + for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Followers")), + for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(p.friendCount))), + ] + ), + ], + ), + SizedBox( + width: 200+(summaries.length*350), + child: ExpansionTile( + title: Text("Tetra League"), + children: [ + Table( + border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), + defaultColumnWidth: FixedColumnWidth(350), + columnWidths: { + 0: FixedColumnWidth(200.000) + }, + children: [ + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Tetra Rating")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Glicko")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("RD")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("GLIXARE")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("S1-like TR")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Played")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesPlayed))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Won")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesWon))), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Winrate")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Minute")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.apm) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Pieces Per Second")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.pps) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Versus Score")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.vs) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Nerd Stats")), + for (var _ in summaries) Container(), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Piece")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("VS / APM")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Second")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Piece")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("APP + DSP")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Cheese Index")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Garbage Efficiency")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Weighted APP")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---")), + ] + ), + TableRow( + children: [ + Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Area")), + for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---")), + ] + ), + ], + ), + ], + ) + ) + ], + ), ), ), ); } } + +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Row( +// children: [ +// SizedBox( +// height: 175.0, +// width: 300.0, +// child: Expanded( +// child: Card( +// child: Padding( +// padding: const EdgeInsets.fromLTRB(80.0, 10.0, 5.0, 0), +// child: Text("Comparison", style: TextStyle(fontSize: 28)), +// ), +// ), +// ), +// ), +// for (var p in players) SizedBox( +// width: 300.0, +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// HeaderCard(p), + +// ], +// ), +// ), +// SizedBox(width: 300, child: AddNewColumnCard(addPlayer)) +// ] +// ), +// Row( +// children: [ +// SizedBox( +// width: 300.0, +// child: Card( +// child: Column(children: [ +// Text("Registration Date"), +// const Divider(), +// Text("XP"), +// const Divider(), +// Text("Time Played"), +// const Divider(), +// Text("Online Games Played"), +// const Divider(), +// Text("Online Games Won"), +// const Divider(), +// Text("Followers"), +// ]), +// ), +// ), +// for (var p in players) SizedBox( +// width: 300.0, +// child: Card( +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// Text(timestamp(p.registrationTime!)), +// const Divider(), +// RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), +// const Divider(), +// Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), +// const Divider(), +// Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), +// const Divider(), +// Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), +// const Divider(), +// Text(intf.format(p.friendCount)) +// ], +// ), +// ), +// ), +// ] +// ), +// SizedBox( +// width: 500, +// child: ExpansionTile( +// title: Text("Test"), +// children: [Text("test1"), Text("test2")], +// ), +// ) +// ]) + + class HeaderCard extends StatelessWidget{ final TetrioPlayer player; @@ -196,9 +431,10 @@ class HeaderCard extends StatelessWidget{ if (player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", placeholder: kTransparentImage, fit: BoxFit.cover, - height: 120, + height: 120.0, fadeInCurve: Easing.standard, fadeInDuration: Durations.long4 - ), + ) + else SizedBox(height: 120.0), Positioned( top: 20.0, child: ClipRRect( diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index a3b753b..5b55081 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -552,7 +552,7 @@ class _DestinationCalculatorState extends State { }, ), )); - rSideWidgets.add(const Divider(color: Color.fromARGB(50, 158, 158, 158))); + rSideWidgets.add(const Divider()); } int combo = -1; @@ -1254,7 +1254,7 @@ class _DestinationLeaderboardsState extends State { ) ], ), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), Expanded( child: ListView.builder( controller: _scrollController, @@ -1906,9 +1906,9 @@ class LeagueCard extends StatelessWidget{ ], ) else Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), TLRatingThingy(userID: "", tlData: league, showPositions: true), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), Text("${league.apm != null ? f2.format(league.apm) : "-.--"} APM • ${league.pps != null ? f2.format(league.pps) : "-.--"} PPS • ${league.vs != null ? f2.format(league.vs) : "-.--"} VS • ${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP • ${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) ], ), @@ -1962,9 +1962,9 @@ class _DestinationHomeState extends State with SingleTickerProv crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) ], ), @@ -1979,9 +1979,9 @@ class _DestinationHomeState extends State with SingleTickerProv crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) ], ), @@ -2001,9 +2001,9 @@ class _DestinationHomeState extends State with SingleTickerProv crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), RecordSummary(record: summaries.zenith, hideRank: true), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) ], ), @@ -2018,9 +2018,9 @@ class _DestinationHomeState extends State with SingleTickerProv crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), RecordSummary(record: summaries.zenithEx, hideRank: true,), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), + const Divider(), Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) ], ), From ff2ebd5505619750c952922dc6d1d45a02c8ce9a Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 7 Oct 2024 01:25:55 +0300 Subject: [PATCH 41/86] ok it's NOT a table... --- lib/main.dart | 4 + lib/views/compare_view_tiles.dart | 759 +++++++++++++++++++----------- 2 files changed, 488 insertions(+), 275 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f6b5e16..c0b11f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,6 +52,10 @@ ThemeData theme = ThemeData( ), dividerColor: Color.fromARGB(50, 158, 158, 158), dividerTheme: DividerThemeData(color: Color.fromARGB(50, 158, 158, 158)), + expansionTileTheme: ExpansionTileThemeData( + expansionAnimationStyle: AnimationStyle(curve: Easing.standard, reverseCurve: Easing.standard), + expandedAlignment: Alignment.bottomCenter, + ), scaffoldBackgroundColor: Colors.black ); diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index bea3f09..47a17de 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -42,6 +42,7 @@ class CompareState extends State { late ScrollController _scrollController; List players = []; List summaries = []; + TextStyle _expansionTileTitleTextStyle = TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24.0); @override void initState() { @@ -113,207 +114,314 @@ class CompareState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - SizedBox( - width: 200.0, height: 175.0, child: Expanded(child: Card(child: Padding( - padding: const EdgeInsets.fromLTRB(80.0, 10.0, 5.0, 0), - child: Text("Comparison", style: TextStyle(fontSize: 20)), - ), - )) - ), - for (var p in players) SizedBox(width: 350.0, child: HeaderCard(p)), - SizedBox(width: 350.0, child: AddNewColumnCard(addPlayer)) - ] - ), - Table( - border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), - defaultColumnWidth: FixedColumnWidth(350), - columnWidths: { - 0: FixedColumnWidth(200.000) - }, - children: [ - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Account Created")), - for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(timestamp(p.registrationTime!))) - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("XP")), - for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))]))) - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Time Played")), - for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white))) - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Played")), - for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Won")), - for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Followers")), - for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(p.friendCount))), - ] - ), - ], - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ SizedBox( - width: 200+(summaries.length*350), - child: ExpansionTile( - title: Text("Tetra League"), - children: [ - Table( - border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), - defaultColumnWidth: FixedColumnWidth(350), - columnWidths: { - 0: FixedColumnWidth(200.000) - }, + height: 175.0, + width: 300.0, + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(90.0, 18.0, 5.0, 0), + child: Text("Comparison", style: TextStyle(fontSize: 28)), + ), + ), + ), + for (var p in players) SizedBox( + width: 300.0, + child: HeaderCard(p), + ), + SizedBox(width: 300, child: AddNewColumnCard(addPlayer)) + ] + ), + Row( + children: [ + SizedBox( + width: 300.0, + child: Card( + child: Column(children: [ + Text("Registration Date"), + Text("XP"), + Text("Time Played"), + Text("Online Games Played"), + Text("Online Games Won"), + Text("Followers"), + ]), + ), + ), + for (var p in players) SizedBox( + width: 300.0, + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Tetra Rating")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Glicko")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("RD")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("GLIXARE")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("S1-like TR")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Played")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesPlayed))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Won")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesWon))), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Winrate")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Minute")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.apm) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Pieces Per Second")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.pps) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Versus Score")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.vs) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Nerd Stats")), - for (var _ in summaries) Container(), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Piece")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("VS / APM")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Second")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Piece")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("APP + DSP")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Cheese Index")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Garbage Efficiency")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Weighted APP")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---")), - ] - ), - TableRow( - children: [ - Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Area")), - for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---")), - ] - ), + Text(timestamp(p.registrationTime!)), + RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), + Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), + Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), + Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), + Text(intf.format(p.friendCount)) ], ), - ], + ), + ), + ] + ), + SizedBox( + width: 300+300*summaries.length.toDouble(), + child: ExpansionTile( + title: Text("Tetra League", style: _expansionTileTitleTextStyle), + children: [ + Row( + children: [ + SizedBox( + width: 300.0, + child: Card( + child: Column(children: [ + Text("Tetra Rating"), + Text("Glicko"), + Text("RD"), + Text("GLIXARE"), + Text("S1-like TR"), + Text("Position"), + Text("Position (Country)"), + Text("Games Played"), + Text("Games Won"), + Text("Winrate"), + Text("Attack Per Minute"), + Text("Pieces Per Second"), + Text("Versus Score"), + Text("Nerd Stats"), + Text("Attack Per Piece"), + Text("VS / APM"), + Text("Downstack Per Second"), + Text("Downstack Per Piece"), + Text("APP + DSP"), + Text("Cheese Index"), + Text("Garbage Efficiency"), + Text("Weighted APP"), + Text("Area"), + Text("Playstyle"), + Text("Opener"), + Text("Plonk"), + Text("Stride"), + Text("Infinite Downstack"), + ]), + ), + ), + for (var s in summaries) SizedBox( + width: 300.0, + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr)), + Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko)), + Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd), style: TextStyle(color: s.league.rd!.isNegative ? Colors.grey : Colors.white)), + Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe)), + Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr)), + Text(s.league.standing.isNegative ? "---" : "№ "+intf.format(s.league.standing)), + Text(s.league.standingLocal.isNegative ? "---" : "№ "+intf.format(s.league.standingLocal)), + // RichText(text: s.league.standingLocal.isNegative ? TextSpan(text: "---", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(s.league.standingLocal), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (in ${s.league.})", style: TextStyle(color: Colors.grey))])) + Text(intf.format(s.league.gamesPlayed)), + Text(intf.format(s.league.gamesWon)), + Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%"), + Text(s.league.apm != null ? f2.format(s.league.apm) : "---"), + Text(s.league.pps != null ? f2.format(s.league.pps) : "---"), + Text(s.league.vs != null ? f2.format(s.league.vs) : "---"), + Text(""), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---"), + Text(""), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.opener) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.plonk) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.stride) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.infds) : "---"), + ], + ), + ), + ), + ] ) - ) + ], + ), + ), + SizedBox( + width: 300+300*summaries.length.toDouble(), + child: ExpansionTile( + title: Text("Quick Play", style: _expansionTileTitleTextStyle), + children: [ + Row( + children: [ + SizedBox( + width: 300.0, + child: Card( + child: Column(children: [ + Text("Altitude"), + Text("Position"), + Text("Position (Country)"), + Text("Attack Per Minute"), + Text("Pieces Per Second"), + Text("Versus Score"), + Text("KO's"), + Text("Top B2B"), + Text("Climb Speed"), + Text("Peak Climb Speed"), + Text("Time Spend"), + Text("Finesse"), + Text("Nerd Stats"), + Text("Attack Per Piece"), + Text("VS / APM"), + Text("Downstack Per Second"), + Text("Downstack Per Piece"), + Text("APP + DSP"), + Text("Cheese Index"), + Text("Garbage Efficiency"), + Text("Weighted APP"), + Text("Area"), + Text("Playstyle"), + Text("Opener"), + Text("Plonk"), + Text("Stride"), + Text("Infinite Downstack"), + ]), + ), + ), + for (var s in summaries) SizedBox( + width: 300.0, + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.altitude) : "---"), + Text(s.zenith != null ? "№ "+intf.format(s.zenith!.rank) : "---"), + Text((s.zenith != null && !s.zenith!.countryRank.isNegative) ? "№ "+intf.format(s.zenith!.countryRank) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.apm) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.pps) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.vs) : "---"), + Text(s.zenith != null ? intf.format(s.zenith!.stats.kills) : "---"), + Text(s.zenith != null ? intf.format(s.zenith!.stats.topBtB) : "---"), + Text(s.zenith != null ? f4.format(s.zenith!.stats.cps) : "---"), + Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.peakrank) : "---"), + Text(s.zenith != null ? getMoreNormalTime(s.zenith!.stats.finalTime) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.stats.finessePercentage*100)+"%" : "---"), + Text(""), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.app) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.vsapm) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dss) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dsp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.appdsp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.cheese) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.gbe) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.nyaapp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.area) : "---"), + Text(""), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.opener) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.plonk) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.stride) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.infds) : "---"), + ], + ), + ), + ), + ] + ) + ], + ), + ), + SizedBox( + width: 300+300*summaries.length.toDouble(), + child: ExpansionTile( + title: Text("Quick Play Expert", style: _expansionTileTitleTextStyle), + children: [ + Row( + children: [ + SizedBox( + width: 300.0, + child: Card( + child: Column(children: [ + Text("Altitude"), + Text("Position"), + Text("Position (Country)"), + Text("Attack Per Minute"), + Text("Pieces Per Second"), + Text("Versus Score"), + Text("KO's"), + Text("Top B2B"), + Text("Climb Speed"), + Text("Peak Climb Speed"), + Text("Time Spend"), + Text("Finesse"), + Text("Nerd Stats"), + Text("Attack Per Piece"), + Text("VS / APM"), + Text("Downstack Per Second"), + Text("Downstack Per Piece"), + Text("APP + DSP"), + Text("Cheese Index"), + Text("Garbage Efficiency"), + Text("Weighted APP"), + Text("Area"), + Text("Playstyle"), + Text("Opener"), + Text("Plonk"), + Text("Stride"), + Text("Infinite Downstack"), + ]), + ), + ), + for (var s in summaries) SizedBox( + width: 300.0, + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(s.zenithEx != null ? f4.format(s.zenithEx!.stats.zenith!.altitude) : "---"), + Text(s.zenithEx != null ? "№ "+intf.format(s.zenithEx!.rank) : "---"), + Text((s.zenithEx != null && !s.zenithEx!.countryRank.isNegative) ? "№ "+intf.format(s.zenithEx!.countryRank) : "---"), + Text(s.zenithEx != null ? f2.format(s.zenithEx!.aggregateStats.apm) : "---"), + Text(s.zenithEx != null ? f2.format(s.zenithEx!.aggregateStats.pps) : "---"), + Text(s.zenithEx != null ? f2.format(s.zenithEx!.aggregateStats.vs) : "---"), + Text(s.zenithEx != null ? intf.format(s.zenithEx!.stats.kills) : "---"), + Text(s.zenithEx != null ? intf.format(s.zenithEx!.stats.topBtB) : "---"), + Text(s.zenithEx != null ? f4.format(s.zenithEx!.stats.cps) : "---"), + Text(s.zenithEx != null ? f4.format(s.zenithEx!.stats.zenith!.peakrank) : "---"), + Text(s.zenithEx != null ? getMoreNormalTime(s.zenithEx!.stats.finalTime) : "---"), + Text(s.zenithEx != null ? f2.format(s.zenithEx!.stats.finessePercentage*100)+"%" : "---"), + Text(""), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.app) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.vsapm) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.dss) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.dsp) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.appdsp) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.cheese) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.gbe) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.nyaapp) : "---"), + Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.area) : "---"), + Text(""), + Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.opener) : "---"), + Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.plonk) : "---"), + Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.stride) : "---"), + Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.infds) : "---"), + ], + ), + ), + ), + ] + ) + ], + ), + ) + ]), ], ), ), @@ -323,87 +431,188 @@ class CompareState extends State { } -// Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Row( -// children: [ -// SizedBox( -// height: 175.0, -// width: 300.0, -// child: Expanded( -// child: Card( -// child: Padding( -// padding: const EdgeInsets.fromLTRB(80.0, 10.0, 5.0, 0), -// child: Text("Comparison", style: TextStyle(fontSize: 28)), -// ), -// ), -// ), -// ), -// for (var p in players) SizedBox( -// width: 300.0, -// child: Column( -// mainAxisSize: MainAxisSize.min, -// children: [ -// HeaderCard(p), - -// ], -// ), -// ), -// SizedBox(width: 300, child: AddNewColumnCard(addPlayer)) -// ] -// ), -// Row( -// children: [ -// SizedBox( -// width: 300.0, -// child: Card( -// child: Column(children: [ -// Text("Registration Date"), -// const Divider(), -// Text("XP"), -// const Divider(), -// Text("Time Played"), -// const Divider(), -// Text("Online Games Played"), -// const Divider(), -// Text("Online Games Won"), -// const Divider(), -// Text("Followers"), -// ]), -// ), -// ), -// for (var p in players) SizedBox( -// width: 300.0, -// child: Card( -// child: Column( -// mainAxisSize: MainAxisSize.min, +// Table( +// border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), +// defaultColumnWidth: FixedColumnWidth(350), +// columnWidths: { +// 0: FixedColumnWidth(200.000) +// }, +// children: [ +// TableRow( +// decoration: BoxDecoration(color: Color.fromARGB(255, 10, 10, 10)), // children: [ -// Text(timestamp(p.registrationTime!)), -// const Divider(), -// RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), -// const Divider(), -// Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), -// const Divider(), -// Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), -// const Divider(), -// Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), -// const Divider(), -// Text(intf.format(p.friendCount)) -// ], +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Account Created")), +// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(timestamp(p.registrationTime!))) +// ] // ), -// ), -// ), -// ] -// ), -// SizedBox( -// width: 500, -// child: ExpansionTile( -// title: Text("Test"), -// children: [Text("test1"), Text("test2")], -// ), -// ) -// ]) +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("XP")), +// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))]))) +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Time Played")), +// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white))) +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Played")), +// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Won")), +// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Followers")), +// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(p.friendCount))), +// ] +// ), +// ], +// ) +// Table( +// border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), +// defaultColumnWidth: FixedColumnWidth(350), +// columnWidths: { +// 0: FixedColumnWidth(200.000) +// }, +// children: [ +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Tetra Rating")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Glicko")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("RD")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("GLIXARE")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("S1-like TR")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Played")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesPlayed))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Won")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesWon))), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Winrate")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Minute")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.apm) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Pieces Per Second")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.pps) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Versus Score")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.vs) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Nerd Stats")), +// for (var _ in summaries) Container(), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Piece")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("VS / APM")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Second")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Piece")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("APP + DSP")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Cheese Index")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Garbage Efficiency")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Weighted APP")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---")), +// ] +// ), +// TableRow( +// children: [ +// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Area")), +// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---")), +// ] +// ), +// ], +// ) + class HeaderCard extends StatelessWidget{ From 672c2a6c8c99e6257f3c3d7ea1d308c61241174d Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 8 Oct 2024 01:05:45 +0300 Subject: [PATCH 42/86] ok there will be a graphs --- lib/data_objects/aggregate_stats.dart | 2 + lib/views/compare_view_tiles.dart | 447 +++++++++++++++----------- 2 files changed, 262 insertions(+), 187 deletions(-) diff --git a/lib/data_objects/aggregate_stats.dart b/lib/data_objects/aggregate_stats.dart index 2e16afe..7500585 100644 --- a/lib/data_objects/aggregate_stats.dart +++ b/lib/data_objects/aggregate_stats.dart @@ -18,6 +18,8 @@ class AggregateStats{ playstyle = Playstyle(apm, pps, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); } + AggregateStats.precalculated(this.apm, this.pps, this.vs, this.nerdStats, this.playstyle); + AggregateStats.fromJson(Map json){ apm = json['apm'] != null ? json['apm'].toDouble() : 0.00; pps = json['apm'] != null ? json['pps'].toDouble() : 0.00; diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index 47a17de..401434f 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -2,9 +2,13 @@ import 'dart:io'; import 'dart:math'; +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/aggregate_stats.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/data_objects/summaries.dart'; import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; @@ -15,6 +19,7 @@ import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/vs_graphs.dart'; import 'package:transparent_image/transparent_image.dart'; @@ -42,6 +47,7 @@ class CompareState extends State { late ScrollController _scrollController; List players = []; List summaries = []; + List nicknames = []; TextStyle _expansionTileTitleTextStyle = TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24.0); @override @@ -63,6 +69,7 @@ class CompareState extends State { void getSummariesForInit() async { summaries.add(await teto.fetchSummaries(widget.initPlayer.userId)); + nicknames.add(players[0].username); setState(() { }); @@ -71,6 +78,7 @@ class CompareState extends State { void addPlayer(String nickname) async { players.add(await teto.fetchPlayer(nickname)); summaries.add(await teto.fetchSummaries(players.last.userId)); + nicknames.add(players.last.username); setState(() { }); @@ -251,7 +259,8 @@ class CompareState extends State { ), ), ] - ) + ), + VsGraphs(stats: [for (var s in summaries) AggregateStats.precalculated(s.league.apm??0, s.league.pps??0, s.league.vs??0, s.league.nerdStats??NerdStats(0, 0, 0), s.league.playstyle??Playstyle(0, 0, 0, 0, 0, 0, 0.0001, 0))], nicknames: nicknames) ], ), ), @@ -417,7 +426,7 @@ class CompareState extends State { ), ), ] - ) + ), ], ), ) @@ -430,191 +439,6 @@ class CompareState extends State { } } - -// Table( -// border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), -// defaultColumnWidth: FixedColumnWidth(350), -// columnWidths: { -// 0: FixedColumnWidth(200.000) -// }, -// children: [ -// TableRow( -// decoration: BoxDecoration(color: Color.fromARGB(255, 10, 10, 10)), -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Account Created")), -// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(timestamp(p.registrationTime!))) -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("XP")), -// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))]))) -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Time Played")), -// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white))) -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Played")), -// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Online Games Won")), -// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Followers")), -// for (var p in players) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(p.friendCount))), -// ] -// ), -// ], -// ) -// Table( -// border: TableBorder(verticalInside: BorderSide(color: Colors.grey)), -// defaultColumnWidth: FixedColumnWidth(350), -// columnWidths: { -// 0: FixedColumnWidth(200.000) -// }, -// children: [ -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Tetra Rating")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Glicko")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("RD")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("GLIXARE")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("S1-like TR")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Played")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesPlayed))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Games Won")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(intf.format(s.league.gamesWon))), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Winrate")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Minute")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.apm) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Pieces Per Second")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.pps) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Versus Score")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.apm != null ? f2.format(s.league.vs) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Nerd Stats")), -// for (var _ in summaries) Container(), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Attack Per Piece")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("VS / APM")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Second")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Downstack Per Piece")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("APP + DSP")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Cheese Index")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Garbage Efficiency")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Weighted APP")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---")), -// ] -// ), -// TableRow( -// children: [ -// Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text("Area")), -// for (var s in summaries) Container(padding: EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), child: Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---")), -// ] -// ), -// ], -// ) - - - class HeaderCard extends StatelessWidget{ final TetrioPlayer player; @@ -749,4 +573,253 @@ class _AddNewColumnCardState extends State with SingleTickerPr ) ); } +} + +class VsGraphs extends StatelessWidget{ + final List stats; + final List nicknames; + const VsGraphs({super.key, required this.stats, required this.nicknames}); + + static const List colorsForGraphs = [ + Colors.cyanAccent, + Colors.redAccent, + Colors.purpleAccent, + Colors.amberAccent, + Colors.pinkAccent, + Colors.tealAccent, + Colors.deepOrangeAccent, + ]; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Wrap( + direction: Axis.horizontal, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 20, + children: [ + for (int i = 0; i < stats.length; i++) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 0.0), + child: Container(width: 20.0, height: 10.0, decoration: BoxDecoration(color: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), border: Border.all(color: colorsForGraphs[i%colorsForGraphs.length])),), + ), + Text(nicknames[i], style: TextStyle(fontFamily: "Eurostile Round Extended")) + ], + ) + ], + ), + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle( + text: 'APM', + angle: angle, + positionPercentageOffset: 0.05 + ); + case 1: + return RadarChartTitle( + text: 'PPS', + angle: angle, + positionPercentageOffset: 0.05 + ); + case 2: + return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); + case 4: + return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 5: + return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 6: + return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 7: + return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); + case 8: + return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); + case 9: + return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].apm * apmWeight), + RadarEntry(value: stats[i].pps * ppsWeight), + RadarEntry(value: stats[i].vs * vsWeight), + RadarEntry(value: stats[i].nerdStats.app * appWeight), + RadarEntry(value: stats[i].nerdStats.dss * dssWeight), + RadarEntry(value: stats[i].nerdStats.dsp * dspWeight), + RadarEntry(value: stats[i].nerdStats.appdsp * appdspWeight), + RadarEntry(value: stats[i].nerdStats.vsapm * vsapmWeight), + RadarEntry(value: stats[i].nerdStats.cheese * cheeseWeight), + RadarEntry(value: stats[i].nerdStats.gbe * gbeWeight), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), + swapAnimationCurve: Curves.linear, + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: 'Opener',angle: angle, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: 'Stride', angle: angle, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: 'Inf Ds', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'Plonk', angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].playstyle.opener), + RadarEntry(value: stats[i].playstyle.stride), + RadarEntry(value: stats[i].playstyle.infds), + RadarEntry(value: stats[i].playstyle.plonk), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ), + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional + ), + ), + ), + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: t.graphs.attack, angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: t.graphs.speed, angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: t.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: t.graphs.cheese, angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].apm / 60 * 0.4), + RadarEntry(value: stats[i].pps / 3.75), + RadarEntry(value: stats[i].nerdStats.dss * 1.15), + RadarEntry(value: stats[i].nerdStats.cheese / 110), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1.2), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ) + ) + ) + ) + ], + ), + ], + ); + } } \ No newline at end of file From 3c83a6c244bbcc7d71cfab5d0ddfbc08341937e1 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 11 Oct 2024 01:35:48 +0300 Subject: [PATCH 43/86] My brain hurts I guess i should get drunk --- lib/data_objects/tetrio_player.dart | 4 +- lib/views/compare_view_tiles.dart | 1271 ++++++++++++++++----------- 2 files changed, 749 insertions(+), 526 deletions(-) diff --git a/lib/data_objects/tetrio_player.dart b/lib/data_objects/tetrio_player.dart index 63fd430..02f3ed2 100644 --- a/lib/data_objects/tetrio_player.dart +++ b/lib/data_objects/tetrio_player.dart @@ -14,7 +14,7 @@ class TetrioPlayer { late String role; int? avatarRevision; int? bannerRevision; - DateTime? registrationTime; + late DateTime registrationTime; List badges = []; String? bio; String? country; @@ -39,7 +39,7 @@ class TetrioPlayer { required this.state, this.avatarRevision, this.bannerRevision, - this.registrationTime, + required this.registrationTime, required this.badges, this.bio, this.country, diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index 401434f..b9fa330 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -48,6 +48,126 @@ class CompareState extends State { List players = []; List summaries = []; List nicknames = []; + Map> TitesForStats = { + "General": [ + "Registration Date", + "XP", + "Time Played", + "Online Games Played", + "Online Games Won", + "Followers", + ], + "Tetra League": [ + "Tetra Rating", + "Glicko", + "RD", + "GLIXARE", + "S1-like TR", + "Position", + "Games Played", + "Games Won", + "Winrate", + "Attack Per Minute", + "Pieces Per Second", + "Versus Score", + "Nerd Stats", + "Attack Per Piece", + "VS / APM", + "Downstack Per Second", + "Downstack Per Piece", + "APP + DSP", + "Cheese Index", + "Garbage Efficiency", + "Weighted APP", + "Area", + "Playstyle", + "Opener", + "Plonk", + "Stride", + "Infinite Downstack" + ], + "Quick Play":[ + "Altitude", + "Position", + "Attack Per Minute", + "Pieces Per Second", + "Versus Score", + "KO's", + "Top B2B", + "Climb Speed", + "Peak Climb Speed", + "Time Spend", + "Finesse", + "Nerd Stats", + "Attack Per Piece", + "VS / APM", + "Downstack Per Second", + "Downstack Per Piece", + "APP + DSP", + "Cheese Index", + "Garbage Efficiency", + "Weighted APP", + "Area", + "Playstyle", + "Opener", + "Plonk", + "Stride", + "Infinite Downstack", + ], + "Quick Play Expert": [ + "Altitude", + "Position", + "Attack Per Minute", + "Pieces Per Second", + "Versus Score", + "KO's", + "Top B2B", + "Climb Speed", + "Peak Climb Speed", + "Time Spend", + "Finesse", + "Nerd Stats", + "Attack Per Piece", + "VS / APM", + "Downstack Per Second", + "Downstack Per Piece", + "APP + DSP", + "Cheese Index", + "Garbage Efficiency", + "Weighted APP", + "Area", + "Playstyle", + "Opener", + "Plonk", + "Stride", + "Infinite Downstack", + ], + "40 Lines": [ + "Time", + "Pieces", + "Inputs", + "Key Presses Per Piece", + "Pieces Per Second", + "Key Presses Per Second", + "" + ], + "Blitz": [ + "Score", + "Pieces", + "Inputs", + "Key Presses Per Piece", + "Pieces Per Second", + "Key Presses Per Second", + "" + ], + "Zen": [ + "Score", + "Level" + ] + }; + List>> rawValues = [[],[],[],[],[],[],[]]; + List>> formattedValues = [[],[],[],[],[],[],[]]; //formattedValues[category][player][stat] + List> best = []; TextStyle _expansionTileTitleTextStyle = TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24.0); @override @@ -67,9 +187,362 @@ class CompareState extends State { super.dispose(); } + addvaluesEntrys(TetrioPlayer p, Summaries s){ + rawValues[0].add([ + p.registrationTime, + p.xp, + p.gameTime, + p.gamesPlayed, + p.gamesWon, + p.friendCount + ]); + rawValues[1].add( + [ + s.league.tr, + s.league.glicko, + s.league.rd, + s.league.gxe, + s.league.s1tr, + s.league.standing, + s.league.gamesPlayed, + s.league.gamesWon, + s.league.winrate, + s.league.apm, + s.league.pps, + s.league.vs, + "", + s.league.nerdStats?.app, + s.league.nerdStats?.vsapm, + s.league.nerdStats?.dss, + s.league.nerdStats?.dsp, + s.league.nerdStats?.appdsp, + s.league.nerdStats?.cheese, + s.league.nerdStats?.gbe, + s.league.nerdStats?.nyaapp, + s.league.nerdStats?.area, + "", + s.league.playstyle?.opener, + s.league.playstyle?.plonk, + s.league.playstyle?.stride, + s.league.playstyle?.infds, + ] + ); + rawValues[2].add([ + s.zenith?.stats.zenith?.altitude, + s.zenith?.rank, + s.zenith?.aggregateStats.apm, + s.zenith?.aggregateStats.pps, + s.zenith?.aggregateStats.vs, + s.zenith?.stats.kills, + s.zenith?.stats.topBtB, + s.zenith?.stats.cps, + s.zenith?.stats.zenith?.peakrank, + s.zenith?.stats.finalTime, + s.zenith?.stats.finessePercentage, + "", + s.zenith?.aggregateStats.nerdStats.app, + s.zenith?.aggregateStats.nerdStats.vsapm, + s.zenith?.aggregateStats.nerdStats.dss, + s.zenith?.aggregateStats.nerdStats.dsp, + s.zenith?.aggregateStats.nerdStats.appdsp, + s.zenith?.aggregateStats.nerdStats.cheese, + s.zenith?.aggregateStats.nerdStats.gbe, + s.zenith?.aggregateStats.nerdStats.nyaapp, + s.zenith?.aggregateStats.nerdStats.area, + "", + s.zenith?.aggregateStats.playstyle.opener, + s.zenith?.aggregateStats.playstyle.plonk, + s.zenith?.aggregateStats.playstyle.stride, + s.zenith?.aggregateStats.playstyle.infds, + ]); + rawValues[3].add([ + s.zenithEx?.stats.zenith?.altitude, + s.zenithEx?.rank, + s.zenithEx?.aggregateStats.apm, + s.zenithEx?.aggregateStats.pps, + s.zenithEx?.aggregateStats.vs, + s.zenithEx?.stats.kills, + s.zenithEx?.stats.topBtB, + s.zenithEx?.stats.cps, + s.zenithEx?.stats.zenith?.peakrank, + s.zenithEx?.stats.finalTime, + s.zenithEx?.stats.finessePercentage, + "", + s.zenithEx?.aggregateStats.nerdStats.app, + s.zenithEx?.aggregateStats.nerdStats.vsapm, + s.zenithEx?.aggregateStats.nerdStats.dss, + s.zenithEx?.aggregateStats.nerdStats.dsp, + s.zenithEx?.aggregateStats.nerdStats.appdsp, + s.zenithEx?.aggregateStats.nerdStats.cheese, + s.zenithEx?.aggregateStats.nerdStats.gbe, + s.zenithEx?.aggregateStats.nerdStats.nyaapp, + s.zenithEx?.aggregateStats.nerdStats.area, + "", + s.zenithEx?.aggregateStats.playstyle.opener, + s.zenithEx?.aggregateStats.playstyle.plonk, + s.zenithEx?.aggregateStats.playstyle.stride, + s.zenithEx?.aggregateStats.playstyle.infds, + ]);; + rawValues[4].add([ + s.sprint?.stats.finalTime, + s.sprint?.stats.piecesPlaced, + s.sprint?.stats.inputs, + s.sprint?.stats.kpp, + s.sprint?.stats.pps, + s.sprint?.stats.kps + ]); + rawValues[5].add( + [ + s.blitz?.stats.score, + s.blitz?.stats.piecesPlaced, + s.blitz?.stats.inputs, + s.blitz?.stats.kpp, + s.blitz?.stats.pps, + s.blitz?.stats.kps + ] + ); + rawValues[6].add([ + s.zen.score, + s.zen.level + ]); + formattedValues[0].add([ + Text(timestamp(p.registrationTime!)), + RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), + Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), + Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), + Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), + Text(intf.format(p.friendCount)) + ]); + formattedValues[1].add([ + Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr)), + Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko)), + Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd), style: TextStyle(color: s.league.rd!.isNegative ? Colors.grey : Colors.white)), + Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe)), + Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr)), + Text(s.league.standing.isNegative ? "---" : "№ "+intf.format(s.league.standing)), + Text(intf.format(s.league.gamesPlayed)), + Text(intf.format(s.league.gamesWon)), + Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%"), + Text(s.league.apm != null ? f2.format(s.league.apm) : "---"), + Text(s.league.pps != null ? f2.format(s.league.pps) : "---"), + Text(s.league.vs != null ? f2.format(s.league.vs) : "---"), + Text(""), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---"), + Text(""), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.opener) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.plonk) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.stride) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.infds) : "---"), + ]); + formattedValues[2].add([ + Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.altitude) : "---"), + Text(s.zenith != null ? "№ "+intf.format(s.zenith!.rank) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.apm) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.pps) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.vs) : "---"), + Text(s.zenith != null ? intf.format(s.zenith!.stats.kills) : "---"), + Text(s.zenith != null ? intf.format(s.zenith!.stats.topBtB) : "---"), + Text(s.zenith != null ? f4.format(s.zenith!.stats.cps) : "---"), + Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.peakrank) : "---"), + Text(s.zenith != null ? getMoreNormalTime(s.zenith!.stats.finalTime) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.stats.finessePercentage*100)+"%" : "---"), + Text(""), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.app) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.vsapm) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dss) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dsp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.appdsp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.cheese) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.gbe) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.nyaapp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.area) : "---"), + Text(""), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.opener) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.plonk) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.stride) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.infds) : "---"), + ]); + formattedValues[3].add([ + Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.altitude) : "---"), + Text(s.zenith != null ? "№ "+intf.format(s.zenith!.rank) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.apm) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.pps) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.vs) : "---"), + Text(s.zenith != null ? intf.format(s.zenith!.stats.kills) : "---"), + Text(s.zenith != null ? intf.format(s.zenith!.stats.topBtB) : "---"), + Text(s.zenith != null ? f4.format(s.zenith!.stats.cps) : "---"), + Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.peakrank) : "---"), + Text(s.zenith != null ? getMoreNormalTime(s.zenith!.stats.finalTime) : "---"), + Text(s.zenith != null ? f2.format(s.zenith!.stats.finessePercentage*100)+"%" : "---"), + Text(""), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.app) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.vsapm) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dss) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dsp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.appdsp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.cheese) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.gbe) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.nyaapp) : "---"), + Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.area) : "---"), + Text(""), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.opener) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.plonk) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.stride) : "---"), + Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.infds) : "---"), + ]); + formattedValues[4].add([ + Text(s.sprint != null ? getMoreNormalTime(s.sprint!.stats.finalTime) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.piecesPlaced) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.inputs) : "---"), + Text(s.sprint != null ? f4.format(s.sprint!.stats.kpp) : "---"), + Text(s.sprint != null ? f4.format(s.sprint!.stats.pps) : "---"), + Text(s.sprint != null ? f4.format(s.sprint!.stats.kps) : "---") + ]); + formattedValues[5].add([ + Text(s.blitz != null ? getMoreNormalTime(s.blitz!.stats.finalTime) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.piecesPlaced) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.inputs) : "---"), + Text(s.blitz != null ? f4.format(s.blitz!.stats.kpp) : "---"), + Text(s.blitz != null ? f4.format(s.blitz!.stats.pps) : "---"), + Text(s.blitz != null ? f4.format(s.blitz!.stats.kps) : "---") + ]); + formattedValues[6].add([ + Text(intf.format(s.zen.score)), + Text(intf.format(s.zen.level)) + ]); + } + + List> recalculateBestEntries(){ + return [ + [ + players.reduce((curr, next) => curr.registrationTime.isBefore(next.registrationTime) ? curr : next).registrationTime, + players.reduce((curr, next) => curr.xp > next.xp ? curr : next).xp, + players.reduce((curr, next) => curr.gameTime.compareTo(next.gameTime) > 0 ? curr : next).gameTime, + players.reduce((curr, next) => curr.gamesPlayed > next.gamesPlayed ? curr : next).gamesPlayed, + players.reduce((curr, next) => curr.gamesWon > next.gamesWon ? curr : next).gamesWon, + players.reduce((curr, next) => curr.friendCount > next.friendCount ? curr : next).friendCount, + ], + [ + summaries.reduce((curr, next) => curr.league.tr > next.league.tr ? curr : next).league.tr, + summaries.reduce((curr, next) => (curr.league.glicko??-1) > (next.league.glicko??-1) ? curr : next).league.glicko, + summaries.reduce((curr, next) => (curr.league.rd??-1) > (next.league.rd??-1) ? curr : next).league.rd, + summaries.reduce((curr, next) => curr.league.gxe > next.league.gxe ? curr : next).league.gxe, + summaries.reduce((curr, next) => curr.league.s1tr > next.league.s1tr ? curr : next).league.s1tr, + summaries.reduce((curr, next) => curr.league.standing > next.league.standing ? curr : next).league.standing, + summaries.reduce((curr, next) => curr.league.gamesPlayed > next.league.gamesPlayed ? curr : next).league.gamesPlayed, + summaries.reduce((curr, next) => curr.league.gamesWon > next.league.gamesWon ? curr : next).league.gamesWon, + summaries.reduce((curr, next) => curr.league.winrate > next.league.winrate ? curr : next).league.winrate, + summaries.reduce((curr, next) => (curr.league.apm??0) > (next.league.apm??0) ? curr : next).league.apm, + summaries.reduce((curr, next) => (curr.league.pps??0) > (next.league.pps??0) ? curr : next).league.pps, + summaries.reduce((curr, next) => (curr.league.vs??0) > (next.league.vs??0) ? curr : next).league.vs, + null, + summaries.reduce((curr, next) => (curr.league.nerdStats?.app??0) > (next.league.nerdStats?.app??0) ? curr : next).league.nerdStats?.app, + summaries.reduce((curr, next) => (curr.league.nerdStats?.vsapm??0) > (next.league.nerdStats?.vsapm??0) ? curr : next).league.nerdStats?.vsapm, + summaries.reduce((curr, next) => (curr.league.nerdStats?.dss??0) > (next.league.nerdStats?.dss??0) ? curr : next).league.nerdStats?.dss, + summaries.reduce((curr, next) => (curr.league.nerdStats?.dsp??0) > (next.league.nerdStats?.dsp??0) ? curr : next).league.nerdStats?.dsp, + summaries.reduce((curr, next) => (curr.league.nerdStats?.appdsp??0) > (next.league.nerdStats?.appdsp??0) ? curr : next).league.nerdStats?.appdsp, + summaries.reduce((curr, next) => (curr.league.nerdStats?.cheese??double.negativeInfinity) > (next.league.nerdStats?.cheese??double.negativeInfinity) ? curr : next).league.nerdStats?.cheese, + summaries.reduce((curr, next) => (curr.league.nerdStats?.gbe??0) > (next.league.nerdStats?.gbe??0) ? curr : next).league.nerdStats?.gbe, + summaries.reduce((curr, next) => (curr.league.nerdStats?.nyaapp??0) > (next.league.nerdStats?.nyaapp??0) ? curr : next).league.nerdStats?.nyaapp, + summaries.reduce((curr, next) => (curr.league.nerdStats?.area??0) > (next.league.nerdStats?.area??0) ? curr : next).league.nerdStats?.area, + null, + summaries.reduce((curr, next) => (curr.league.playstyle?.opener??double.negativeInfinity) > (next.league.playstyle?.opener??double.negativeInfinity) ? curr : next).league.playstyle?.opener, + summaries.reduce((curr, next) => (curr.league.playstyle?.plonk??double.negativeInfinity) > (next.league.playstyle?.plonk??double.negativeInfinity) ? curr : next).league.playstyle?.plonk, + summaries.reduce((curr, next) => (curr.league.playstyle?.stride??double.negativeInfinity) > (next.league.playstyle?.stride??double.negativeInfinity) ? curr : next).league.playstyle?.stride, + summaries.reduce((curr, next) => (curr.league.playstyle?.infds??double.negativeInfinity) > (next.league.playstyle?.infds??double.negativeInfinity) ? curr : next).league.playstyle?.infds + ], + [ + summaries.reduce((curr, next) => (curr.zenith?.stats.zenith?.altitude??-1) > (next.zenith?.stats.zenith?.altitude??-1) ? curr : next).zenith?.stats.zenith?.altitude??-1, + summaries.reduce((curr, next) => (curr.zenith?.rank??-1) > (next.zenith?.rank??-1) ? curr : next).zenith?.rank, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.apm??-1) > (next.zenith?.aggregateStats.apm??-1) ? curr : next).zenith?.aggregateStats.apm, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.pps??-1) > (next.zenith?.aggregateStats.pps??-1) ? curr : next).zenith?.aggregateStats.pps, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.vs??-1) > (next.zenith?.aggregateStats.vs??-1) ? curr : next).zenith?.aggregateStats.vs, + summaries.reduce((curr, next) => (curr.zenith?.stats.kills??-1) > (next.zenith?.stats.kills??-1) ? curr : next).zenith?.stats.kills, + summaries.reduce((curr, next) => (curr.zenith?.stats.topBtB??-1) > (next.zenith?.stats.topBtB??-1) ? curr : next).zenith?.stats.topBtB, + summaries.reduce((curr, next) => (curr.zenith?.stats.cps??-1) > (next.zenith?.stats.cps??-1) ? curr : next).zenith?.stats.cps, + summaries.reduce((curr, next) => (curr.zenith?.stats.zenith?.peakrank??-1) > (next.zenith?.stats.zenith?.peakrank??-1) ? curr : next).zenith?.stats.zenith?.peakrank, + summaries.reduce((curr, next) => (curr.zenith?.stats.finalTime != null) ? curr.zenith!.stats.finalTime.compareTo(next.zenith?.stats.finalTime??Duration.zero) > 1 ? curr : next : next).zenith?.stats.finalTime, + summaries.reduce((curr, next) => (curr.zenith?.stats.finessePercentage??-1) > (next.zenith?.stats.finessePercentage??-1) ? curr : next).zenith?.stats.finessePercentage, + null, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.app??0) > (next.zenith?.aggregateStats.nerdStats.app??0) ? curr : next).zenith?.aggregateStats.nerdStats.app, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.vsapm??0) > (next.zenith?.aggregateStats.nerdStats.vsapm??0) ? curr : next).zenith?.aggregateStats.nerdStats.vsapm, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.dss??0) > (next.zenith?.aggregateStats.nerdStats.dss??0) ? curr : next).zenith?.aggregateStats.nerdStats.dss, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.dsp??0) > (next.zenith?.aggregateStats.nerdStats.dsp??0) ? curr : next).zenith?.aggregateStats.nerdStats.dsp, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.appdsp??0) > (next.zenith?.aggregateStats.nerdStats.appdsp??0) ? curr : next).zenith?.aggregateStats.nerdStats.appdsp, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.cheese??double.negativeInfinity) > (next.zenith?.aggregateStats.nerdStats.cheese??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.nerdStats.cheese, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.gbe??0) > (next.zenith?.aggregateStats.nerdStats.gbe??0) ? curr : next).zenith?.aggregateStats.nerdStats.gbe, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.nyaapp??0) > (next.zenith?.aggregateStats.nerdStats.nyaapp??0) ? curr : next).zenith?.aggregateStats.nerdStats.nyaapp, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.area??0) > (next.zenith?.aggregateStats.nerdStats.area??0) ? curr : next).zenith?.aggregateStats.nerdStats.area, + null, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.opener??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.opener??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.opener, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.plonk??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.plonk??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.plonk, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.stride??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.stride??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.stride, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.infds??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.infds??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.infds + ], + [ + summaries.reduce((curr, next) => (curr.zenithEx?.stats.zenith?.altitude??-1) > (next.zenithEx?.stats.zenith?.altitude??-1) ? curr : next).zenithEx?.stats.zenith?.altitude??-1, + summaries.reduce((curr, next) => (curr.zenithEx?.rank??-1) > (next.zenithEx?.rank??-1) ? curr : next).zenithEx?.rank, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.apm??-1) > (next.zenithEx?.aggregateStats.apm??-1) ? curr : next).zenithEx?.aggregateStats.apm, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.pps??-1) > (next.zenithEx?.aggregateStats.pps??-1) ? curr : next).zenithEx?.aggregateStats.pps, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.vs??-1) > (next.zenithEx?.aggregateStats.vs??-1) ? curr : next).zenithEx?.aggregateStats.vs, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.kills??-1) > (next.zenithEx?.stats.kills??-1) ? curr : next).zenithEx?.stats.kills, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.topBtB??-1) > (next.zenithEx?.stats.topBtB??-1) ? curr : next).zenithEx?.stats.topBtB, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.cps??-1) > (next.zenithEx?.stats.cps??-1) ? curr : next).zenithEx?.stats.cps, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.zenith?.peakrank??-1) > (next.zenithEx?.stats.zenith?.peakrank??-1) ? curr : next).zenithEx?.stats.zenith?.peakrank, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.finalTime != null) ? curr.zenithEx!.stats.finalTime.compareTo(next.zenithEx?.stats.finalTime??Duration.zero) > 1 ? curr : next : next).zenithEx?.stats.finalTime, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.finessePercentage??-1) > (next.zenithEx?.stats.finessePercentage??-1) ? curr : next).zenithEx?.stats.finessePercentage, + null, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.app??0) > (next.zenithEx?.aggregateStats.nerdStats.app??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.app, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.vsapm??0) > (next.zenithEx?.aggregateStats.nerdStats.vsapm??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.vsapm, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.dss??0) > (next.zenithEx?.aggregateStats.nerdStats.dss??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.dss, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.dsp??0) > (next.zenithEx?.aggregateStats.nerdStats.dsp??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.dsp, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.appdsp??0) > (next.zenithEx?.aggregateStats.nerdStats.appdsp??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.appdsp, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.cheese??double.negativeInfinity) > (next.zenithEx?.aggregateStats.nerdStats.cheese??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.nerdStats.cheese, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.gbe??0) > (next.zenithEx?.aggregateStats.nerdStats.gbe??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.gbe, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.nyaapp??0) > (next.zenithEx?.aggregateStats.nerdStats.nyaapp??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.nyaapp, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.area??0) > (next.zenithEx?.aggregateStats.nerdStats.area??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.area, + null, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.opener??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.opener??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.opener, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.plonk??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.plonk??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.plonk, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.stride??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.stride??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.stride, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.infds??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.infds??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.infds + ], + [ + summaries.reduce((curr, next) => (curr.sprint?.stats.finalTime != null) ? curr.sprint!.stats.finalTime.compareTo(next.sprint?.stats.finalTime??Duration.zero) < 1 ? curr : next : next).sprint?.stats.finalTime, + summaries.reduce((curr, next) => (curr.sprint?.stats.piecesPlaced??-1) < (next.sprint?.stats.piecesPlaced??-1) ? curr : next).sprint?.stats.piecesPlaced, + summaries.reduce((curr, next) => (curr.sprint?.stats.inputs??-1) < (next.sprint?.stats.inputs??-1) ? curr : next).sprint?.stats.inputs, + summaries.reduce((curr, next) => (curr.sprint?.stats.kpp??-1) < (next.sprint?.stats.kpp??-1) ? curr : next).sprint?.stats.kpp, + summaries.reduce((curr, next) => (curr.sprint?.stats.pps??-1) > (next.sprint?.stats.pps??-1) ? curr : next).sprint?.stats.pps, + summaries.reduce((curr, next) => (curr.sprint?.stats.kps??-1) > (next.sprint?.stats.kps??-1) ? curr : next).sprint?.stats.kps, + ], + [ + summaries.reduce((curr, next) => (curr.blitz?.stats.score??-1) > (next.blitz?.stats.score??-1) ? curr : next).blitz?.stats.score, + summaries.reduce((curr, next) => (curr.blitz?.stats.piecesPlaced??-1) < (next.blitz?.stats.piecesPlaced??-1) ? curr : next).blitz?.stats.piecesPlaced, + summaries.reduce((curr, next) => (curr.blitz?.stats.inputs??-1) < (next.blitz?.stats.inputs??-1) ? curr : next).blitz?.stats.inputs, + summaries.reduce((curr, next) => (curr.blitz?.stats.kpp??-1) < (next.blitz?.stats.kpp??-1) ? curr : next).blitz?.stats.kpp, + summaries.reduce((curr, next) => (curr.blitz?.stats.pps??-1) > (next.blitz?.stats.pps??-1) ? curr : next).blitz?.stats.pps, + summaries.reduce((curr, next) => (curr.blitz?.stats.kps??-1) > (next.blitz?.stats.kps??-1) ? curr : next).blitz?.stats.kps, + ], + [ + summaries.reduce((curr, next) => curr.zen.score > next.zen.score ? curr : next).zen.score, + summaries.reduce((curr, next) => curr.zen.level > next.zen.level ? curr : next).zen.level, + ] + ]; + } + void getSummariesForInit() async { summaries.add(await teto.fetchSummaries(widget.initPlayer.userId)); - nicknames.add(players[0].username); + if (summaries[0].league.nerdStats != null) nicknames.add(players[0].username); + addvaluesEntrys(players.first, summaries.first); + best = recalculateBestEntries(); setState(() { }); @@ -78,7 +551,9 @@ class CompareState extends State { void addPlayer(String nickname) async { players.add(await teto.fetchPlayer(nickname)); summaries.add(await teto.fetchSummaries(players.last.userId)); - nicknames.add(players.last.username); + addvaluesEntrys(players.last, summaries.last); + best = recalculateBestEntries(); + if (summaries.last.league.nerdStats != null) nicknames.add(players.last.username); setState(() { }); @@ -123,314 +598,60 @@ class CompareState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SizedBox( - height: 175.0, - width: 300.0, - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(90.0, 18.0, 5.0, 0), - child: Text("Comparison", style: TextStyle(fontSize: 28)), - ), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + height: 175.0, + width: 300.0, + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(90.0, 18.0, 5.0, 0), + child: Text("Comparison", style: TextStyle(fontSize: 28)), + ), + ), + ), + for (var p in players) SizedBox( + width: 300.0, + child: HeaderCard(p), + ), + SizedBox(width: 300, child: AddNewColumnCard(addPlayer)) + ] ), - ), - for (var p in players) SizedBox( - width: 300.0, - child: HeaderCard(p), - ), - SizedBox(width: 300, child: AddNewColumnCard(addPlayer)) - ] - ), - Row( - children: [ - SizedBox( - width: 300.0, - child: Card( - child: Column(children: [ - Text("Registration Date"), - Text("XP"), - Text("Time Played"), - Text("Online Games Played"), - Text("Online Games Won"), - Text("Followers"), - ]), - ), - ), - for (var p in players) SizedBox( - width: 300.0, - child: Card( - child: Column( - mainAxisSize: MainAxisSize.min, + for (int i = 0; i < formattedValues.length; i++) SizedBox( + width: 300+300*summaries.length.toDouble(), + child: ExpansionTile( + title: Text(TitesForStats.keys.elementAt(i), style: _expansionTileTitleTextStyle), children: [ - Text(timestamp(p.registrationTime!)), - RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), - Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), - Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), - Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), - Text(intf.format(p.friendCount)) + Row( + children: [ + SizedBox( + width: 300.0, + child: Card( + child: Column(children: [ + for (String title in TitesForStats[TitesForStats.keys.elementAt(i)]!) Text(title), + ]), + ), + ), + for (int k = 0; k < formattedValues[i].length; k++) SizedBox( + width: 300.0, + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int l = 0; l < formattedValues[i][k].length; l++) Container(decoration: BoxDecoration(color: (rawValues[i][k][l] != null && best[i][l] == rawValues[i][k][l]) ? Colors.cyanAccent.withAlpha(96) : null), child: formattedValues[i][k][l]), + ], + ), + ), + ), + ] + ), + //VsGraphs(stats: [for (var s in summaries) if (s.league.nerdStats != null) AggregateStats.precalculated(s.league.apm!, s.league.pps!, s.league.vs!, s.league.nerdStats!, s.league.playstyle!)], nicknames: nicknames) ], ), ), - ), - ] - ), - SizedBox( - width: 300+300*summaries.length.toDouble(), - child: ExpansionTile( - title: Text("Tetra League", style: _expansionTileTitleTextStyle), - children: [ - Row( - children: [ - SizedBox( - width: 300.0, - child: Card( - child: Column(children: [ - Text("Tetra Rating"), - Text("Glicko"), - Text("RD"), - Text("GLIXARE"), - Text("S1-like TR"), - Text("Position"), - Text("Position (Country)"), - Text("Games Played"), - Text("Games Won"), - Text("Winrate"), - Text("Attack Per Minute"), - Text("Pieces Per Second"), - Text("Versus Score"), - Text("Nerd Stats"), - Text("Attack Per Piece"), - Text("VS / APM"), - Text("Downstack Per Second"), - Text("Downstack Per Piece"), - Text("APP + DSP"), - Text("Cheese Index"), - Text("Garbage Efficiency"), - Text("Weighted APP"), - Text("Area"), - Text("Playstyle"), - Text("Opener"), - Text("Plonk"), - Text("Stride"), - Text("Infinite Downstack"), - ]), - ), - ), - for (var s in summaries) SizedBox( - width: 300.0, - child: Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr)), - Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko)), - Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd), style: TextStyle(color: s.league.rd!.isNegative ? Colors.grey : Colors.white)), - Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe)), - Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr)), - Text(s.league.standing.isNegative ? "---" : "№ "+intf.format(s.league.standing)), - Text(s.league.standingLocal.isNegative ? "---" : "№ "+intf.format(s.league.standingLocal)), - // RichText(text: s.league.standingLocal.isNegative ? TextSpan(text: "---", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(s.league.standingLocal), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (in ${s.league.})", style: TextStyle(color: Colors.grey))])) - Text(intf.format(s.league.gamesPlayed)), - Text(intf.format(s.league.gamesWon)), - Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%"), - Text(s.league.apm != null ? f2.format(s.league.apm) : "---"), - Text(s.league.pps != null ? f2.format(s.league.pps) : "---"), - Text(s.league.vs != null ? f2.format(s.league.vs) : "---"), - Text(""), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---"), - Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---"), - Text(""), - Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.opener) : "---"), - Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.plonk) : "---"), - Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.stride) : "---"), - Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.infds) : "---"), - ], - ), - ), - ), - ] - ), - VsGraphs(stats: [for (var s in summaries) AggregateStats.precalculated(s.league.apm??0, s.league.pps??0, s.league.vs??0, s.league.nerdStats??NerdStats(0, 0, 0), s.league.playstyle??Playstyle(0, 0, 0, 0, 0, 0, 0.0001, 0))], nicknames: nicknames) - ], - ), - ), - SizedBox( - width: 300+300*summaries.length.toDouble(), - child: ExpansionTile( - title: Text("Quick Play", style: _expansionTileTitleTextStyle), - children: [ - Row( - children: [ - SizedBox( - width: 300.0, - child: Card( - child: Column(children: [ - Text("Altitude"), - Text("Position"), - Text("Position (Country)"), - Text("Attack Per Minute"), - Text("Pieces Per Second"), - Text("Versus Score"), - Text("KO's"), - Text("Top B2B"), - Text("Climb Speed"), - Text("Peak Climb Speed"), - Text("Time Spend"), - Text("Finesse"), - Text("Nerd Stats"), - Text("Attack Per Piece"), - Text("VS / APM"), - Text("Downstack Per Second"), - Text("Downstack Per Piece"), - Text("APP + DSP"), - Text("Cheese Index"), - Text("Garbage Efficiency"), - Text("Weighted APP"), - Text("Area"), - Text("Playstyle"), - Text("Opener"), - Text("Plonk"), - Text("Stride"), - Text("Infinite Downstack"), - ]), - ), - ), - for (var s in summaries) SizedBox( - width: 300.0, - child: Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.altitude) : "---"), - Text(s.zenith != null ? "№ "+intf.format(s.zenith!.rank) : "---"), - Text((s.zenith != null && !s.zenith!.countryRank.isNegative) ? "№ "+intf.format(s.zenith!.countryRank) : "---"), - Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.apm) : "---"), - Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.pps) : "---"), - Text(s.zenith != null ? f2.format(s.zenith!.aggregateStats.vs) : "---"), - Text(s.zenith != null ? intf.format(s.zenith!.stats.kills) : "---"), - Text(s.zenith != null ? intf.format(s.zenith!.stats.topBtB) : "---"), - Text(s.zenith != null ? f4.format(s.zenith!.stats.cps) : "---"), - Text(s.zenith != null ? f4.format(s.zenith!.stats.zenith!.peakrank) : "---"), - Text(s.zenith != null ? getMoreNormalTime(s.zenith!.stats.finalTime) : "---"), - Text(s.zenith != null ? f2.format(s.zenith!.stats.finessePercentage*100)+"%" : "---"), - Text(""), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.app) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.vsapm) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dss) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.dsp) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.appdsp) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.cheese) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.gbe) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.nyaapp) : "---"), - Text(s.zenith?.aggregateStats.nerdStats != null ? f4.format(s.zenith!.aggregateStats.nerdStats.area) : "---"), - Text(""), - Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.opener) : "---"), - Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.plonk) : "---"), - Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.stride) : "---"), - Text(s.zenith?.aggregateStats.playstyle != null ? f4.format(s.zenith!.aggregateStats.playstyle.infds) : "---"), - ], - ), - ), - ), - ] - ) - ], - ), - ), - SizedBox( - width: 300+300*summaries.length.toDouble(), - child: ExpansionTile( - title: Text("Quick Play Expert", style: _expansionTileTitleTextStyle), - children: [ - Row( - children: [ - SizedBox( - width: 300.0, - child: Card( - child: Column(children: [ - Text("Altitude"), - Text("Position"), - Text("Position (Country)"), - Text("Attack Per Minute"), - Text("Pieces Per Second"), - Text("Versus Score"), - Text("KO's"), - Text("Top B2B"), - Text("Climb Speed"), - Text("Peak Climb Speed"), - Text("Time Spend"), - Text("Finesse"), - Text("Nerd Stats"), - Text("Attack Per Piece"), - Text("VS / APM"), - Text("Downstack Per Second"), - Text("Downstack Per Piece"), - Text("APP + DSP"), - Text("Cheese Index"), - Text("Garbage Efficiency"), - Text("Weighted APP"), - Text("Area"), - Text("Playstyle"), - Text("Opener"), - Text("Plonk"), - Text("Stride"), - Text("Infinite Downstack"), - ]), - ), - ), - for (var s in summaries) SizedBox( - width: 300.0, - child: Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(s.zenithEx != null ? f4.format(s.zenithEx!.stats.zenith!.altitude) : "---"), - Text(s.zenithEx != null ? "№ "+intf.format(s.zenithEx!.rank) : "---"), - Text((s.zenithEx != null && !s.zenithEx!.countryRank.isNegative) ? "№ "+intf.format(s.zenithEx!.countryRank) : "---"), - Text(s.zenithEx != null ? f2.format(s.zenithEx!.aggregateStats.apm) : "---"), - Text(s.zenithEx != null ? f2.format(s.zenithEx!.aggregateStats.pps) : "---"), - Text(s.zenithEx != null ? f2.format(s.zenithEx!.aggregateStats.vs) : "---"), - Text(s.zenithEx != null ? intf.format(s.zenithEx!.stats.kills) : "---"), - Text(s.zenithEx != null ? intf.format(s.zenithEx!.stats.topBtB) : "---"), - Text(s.zenithEx != null ? f4.format(s.zenithEx!.stats.cps) : "---"), - Text(s.zenithEx != null ? f4.format(s.zenithEx!.stats.zenith!.peakrank) : "---"), - Text(s.zenithEx != null ? getMoreNormalTime(s.zenithEx!.stats.finalTime) : "---"), - Text(s.zenithEx != null ? f2.format(s.zenithEx!.stats.finessePercentage*100)+"%" : "---"), - Text(""), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.app) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.vsapm) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.dss) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.dsp) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.appdsp) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.cheese) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.gbe) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.nyaapp) : "---"), - Text(s.zenithEx?.aggregateStats.nerdStats != null ? f4.format(s.zenithEx!.aggregateStats.nerdStats.area) : "---"), - Text(""), - Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.opener) : "---"), - Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.plonk) : "---"), - Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.stride) : "---"), - Text(s.zenithEx?.aggregateStats.playstyle != null ? f4.format(s.zenithEx!.aggregateStats.playstyle.infds) : "---"), - ], - ), - ), - ), - ] - ), - ], - ), - ) - ]), + ]), ], ), ), @@ -592,234 +813,236 @@ class VsGraphs extends StatelessWidget{ @override Widget build(BuildContext context) { - return Column( - children: [ - Wrap( - direction: Axis.horizontal, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 20, - children: [ - for (int i = 0; i < stats.length; i++) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 0.0), - child: Container(width: 20.0, height: 10.0, decoration: BoxDecoration(color: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), border: Border.all(color: colorsForGraphs[i%colorsForGraphs.length])),), - ), - Text(nicknames[i], style: TextStyle(fontFamily: "Eurostile Round Extended")) - ], - ) - ], - ), - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle( - text: 'APM', - angle: angle, - positionPercentageOffset: 0.05 - ); - case 1: - return RadarChartTitle( - text: 'PPS', - angle: angle, - positionPercentageOffset: 0.05 - ); - case 2: - return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); - case 4: - return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); - case 5: - return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); - case 6: - return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); - case 7: - return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); - case 8: - return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); - case 9: - return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - for (int i = 0; i < stats.length; i++) RadarDataSet( - fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), - borderColor: colorsForGraphs[i%colorsForGraphs.length], - dataEntries: [ - RadarEntry(value: stats[i].apm * apmWeight), - RadarEntry(value: stats[i].pps * ppsWeight), - RadarEntry(value: stats[i].vs * vsWeight), - RadarEntry(value: stats[i].nerdStats.app * appWeight), - RadarEntry(value: stats[i].nerdStats.dss * dssWeight), - RadarEntry(value: stats[i].nerdStats.dsp * dspWeight), - RadarEntry(value: stats[i].nerdStats.appdsp * appdspWeight), - RadarEntry(value: stats[i].nerdStats.vsapm * vsapmWeight), - RadarEntry(value: stats[i].nerdStats.cheese * cheeseWeight), - RadarEntry(value: stats[i].nerdStats.gbe * gbeWeight), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ) - ], + return Card( + child: Column( + children: [ + Wrap( + direction: Axis.horizontal, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 20, + children: [ + for (int i = 0; i < stats.length; i++) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 0.0), + child: Container(width: 20.0, height: 10.0, decoration: BoxDecoration(color: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), border: Border.all(color: colorsForGraphs[i%colorsForGraphs.length])),), + ), + Text(nicknames[i], style: TextStyle(fontFamily: "Eurostile Round Extended")) + ], + ) + ], + ), + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle( + text: 'APM', + angle: angle, + positionPercentageOffset: 0.05 + ); + case 1: + return RadarChartTitle( + text: 'PPS', + angle: angle, + positionPercentageOffset: 0.05 + ); + case 2: + return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); + case 4: + return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 5: + return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 6: + return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 7: + return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); + case 8: + return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); + case 9: + return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].apm * apmWeight), + RadarEntry(value: stats[i].pps * ppsWeight), + RadarEntry(value: stats[i].vs * vsWeight), + RadarEntry(value: stats[i].nerdStats.app * appWeight), + RadarEntry(value: stats[i].nerdStats.dss * dssWeight), + RadarEntry(value: stats[i].nerdStats.dsp * dspWeight), + RadarEntry(value: stats[i].nerdStats.appdsp * appdspWeight), + RadarEntry(value: stats[i].nerdStats.vsapm * vsapmWeight), + RadarEntry(value: stats[i].nerdStats.cheese * cheeseWeight), + RadarEntry(value: stats[i].nerdStats.gbe * gbeWeight), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), + swapAnimationCurve: Curves.linear, ), - swapAnimationDuration: const Duration(milliseconds: 150), - swapAnimationCurve: Curves.linear, ), ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), - titleTextStyle: const TextStyle(height: 1.1), - radarTouchData: RadarTouchData(), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: 'Opener',angle: angle, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: 'Stride', angle: angle, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: 'Inf Ds', angle: angle + 180, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: 'Plonk', angle: angle, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - for (int i = 0; i < stats.length; i++) RadarDataSet( - fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), - borderColor: colorsForGraphs[i%colorsForGraphs.length], - dataEntries: [ - RadarEntry(value: stats[i].playstyle.opener), - RadarEntry(value: stats[i].playstyle.stride), - RadarEntry(value: stats[i].playstyle.infds), - RadarEntry(value: stats[i].playstyle.plonk), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 1), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ), - ], + Padding( + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: 'Opener',angle: angle, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: 'Stride', angle: angle, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: 'Inf Ds', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'Plonk', angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].playstyle.opener), + RadarEntry(value: stats[i].playstyle.stride), + RadarEntry(value: stats[i].playstyle.infds), + RadarEntry(value: stats[i].playstyle.plonk), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ), + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional ), ), - ), - Padding( // sq graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), - titleTextStyle: const TextStyle(height: 1.1), - radarTouchData: RadarTouchData(), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: t.graphs.attack, angle: 0, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: t.graphs.speed, angle: 0, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: t.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: t.graphs.cheese, angle: 0, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - for (int i = 0; i < stats.length; i++) RadarDataSet( - fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), - borderColor: colorsForGraphs[i%colorsForGraphs.length], - dataEntries: [ - RadarEntry(value: stats[i].apm / 60 * 0.4), - RadarEntry(value: stats[i].pps / 3.75), - RadarEntry(value: stats[i].nerdStats.dss * 1.15), - RadarEntry(value: stats[i].nerdStats.cheese / 110), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 1.2), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ) - ], + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: t.graphs.attack, angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: t.graphs.speed, angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: t.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: t.graphs.cheese, angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].apm / 60 * 0.4), + RadarEntry(value: stats[i].pps / 3.75), + RadarEntry(value: stats[i].nerdStats.dss * 1.15), + RadarEntry(value: stats[i].nerdStats.cheese / 110), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1.2), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ) ) ) ) - ) - ], - ), - ], + ], + ), + ], + ), ); } } \ No newline at end of file From 298f12e060d62d237df62a661dd46340e23e44d1 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 13 Oct 2024 01:08:29 +0300 Subject: [PATCH 44/86] I figured out what's wrong with damage calculator --- lib/views/compare_view_tiles.dart | 2 +- lib/views/main_view_tiles.dart | 87 ++++++++++++------------------- 2 files changed, 34 insertions(+), 55 deletions(-) diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index b9fa330..ad13355 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -640,7 +640,7 @@ class CompareState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - for (int l = 0; l < formattedValues[i][k].length; l++) Container(decoration: BoxDecoration(color: (rawValues[i][k][l] != null && best[i][l] == rawValues[i][k][l]) ? Colors.cyanAccent.withAlpha(96) : null), child: formattedValues[i][k][l]), + for (int l = 0; l < formattedValues[i][k].length; l++) Container(decoration: BoxDecoration(color: (rawValues[0].length > 1 && rawValues[i][k][l] != null && best[i][l] == rawValues[i][k][l]) ? Colors.cyanAccent.withAlpha(96) : null), child: formattedValues[i][k][l]), ], ), ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 5b55081..5ea9cce 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -285,67 +285,42 @@ class ClearData{ perfectClear = !perfectClear; } - Damage dealsDamage(int combo, int b2b, Rules rules){ - if (lines == 0) return Damage(0,0,0,0,0,rules.multiplier); - int clearDamage = 0; + int dealsDamage(int combo, int b2b, int previousB2B, Rules rules){ + if (lines == 0) return 0; + double damage = 0; if (spin){ - if (lines <= 5) clearDamage += garbage[lineclear]!; - else clearDamage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); } else if (miniSpin){ - clearDamage += garbage[lineclear]!; + damage += garbage[lineclear]!; } else { - if (lines <= 5) clearDamage += garbage[lineclear]!; - else clearDamage += garbage[Lineclears.PENTA]! + (lines - 5); + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.PENTA]! + (lines - 5); } - // Ok i can't figure out how b2b and combo works - - // From tetrio.js: - // const n = e.cm.constants.garbage.BACKTOBACK_BONUS * (Math.floor(1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG)) + (t.stats.btb - 1 == 1 ? 0 : (1 + Math.log1p((t.stats.btb - 1) * e.cm.constants.garbage.BACKTOBACK_BONUS_LOG) % 1) / 3)); - // if (h && (d += n), - - double b2bDamage = 0; - if (difficultClear && b2b >= 1 && rules.b2b){ - if (rules.b2bChaining) b2bDamage += BACKTOBACK_BONUS * ((1 + log((b2b+1) * BACKTOBACK_BONUS_LOG)).floor() + (b2b+1 == 1 ? 0 : (1 + log((b2b+1) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? - else b2bDamage += 1; // if b2b chaining off + if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log(1 + (b2b) * BACKTOBACK_BONUS_LOG)).floor() + (b2b == 1 ? 0 : (1 + log(1 +(b2b) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? + else damage += 1; // if b2b chaining off } - int surgeDamage = 0; - - if (!difficultClear && rules.surge){ - - } - - // From tetrio.js: - // if (t.stats.combo > 1) - // if (p += e.cm.constants.scoring.COMBO * (t.stats.combo - 1), - // "multiplier" === t.setoptions.combotable) - // d *= 1 + e.cm.constants.garbage.COMBO_BONUS * (t.stats.combo - 1), - // t.stats.combo > 2 && (d = Math.max(Math.log1p(e.cm.constants.garbage.COMBO_MINIFIER * (t.stats.combo - 1) * e.cm.constants.garbage.COMBO_MINIFIER_LOG), d)); - // else { - // const n = e.cm.constants.garbage.combotable[t.setoptions.combotable] || [0]; - // d += n[Math.max(0, Math.min(t.stats.combo - 2, n.length - 1))] - // } - - double comboDamage = 0; - - if (rules.combo) { - if (combo > 1){ - if (lines == 1 && rules.comboTable != ComboTables.multiplier) comboDamage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; - else comboDamage = (clearDamage + b2bDamage) * (1 + COMBO_BONUS * (combo-1)); + if (rules.combo && rules.comboTable != ComboTables.none) { + if (combo >= 1){ + if (lines == 1 && rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; + else damage *= (1 + COMBO_BONUS * (combo)); } - if (combo > 2) { - comboDamage = max(log(COMBO_MINIFIER * (combo-1) * COMBO_MINIFIER_LOG), comboDamage); + if (combo >= 2) { + damage = max(log(1 + COMBO_MINIFIER * (combo) * COMBO_MINIFIER_LOG), damage); } } - int pcDamage = 0; + if (!difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1){ + damage += rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b); + } - if (perfectClear) pcDamage += rules.pcDamage; + if (perfectClear) damage += rules.pcDamage; - return Damage(clearDamage, comboDamage, b2bDamage, surgeDamage, pcDamage, rules.multiplier); + return (damage * rules.multiplier).floor(); } } @@ -518,7 +493,7 @@ class _DestinationCalculatorState extends State { for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( child: ListTile( title: Text(data.title), - subtitle: Text("${data.dealsDamage(0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + subtitle: Text("${data.dealsDamage(0, 0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), trailing: Icon(Icons.arrow_forward_ios), onTap: (){ setState((){ @@ -557,13 +532,14 @@ class _DestinationCalculatorState extends State { int combo = -1; int b2b = -1; - Damage totalDamage = Damage(0,0,0,0,0,rules.multiplier); - int totalDamageNumber = 0; + int previousB2B = -1; + int totalDamage = 0; for (ClearData lineclear in clears){ + previousB2B = b2b; if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; if (lineclear.lines > 0) combo++; else combo = -1; - Damage dmg = lineclear.dealsDamage(combo, b2b, rules); + int dmg = lineclear.dealsDamage(combo, b2b, previousB2B, rules); lSideWidgets.add( ListTile( key: ValueKey(lineclear.id), @@ -575,7 +551,7 @@ class _DestinationCalculatorState extends State { ], ), title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), - subtitle: lineclear.lines > 0 ? Text("${dmg.clear} from clear, ${dmg.combo} from combo, ${dmg.b2b} from B2B, ${dmg.surge} from Surge and ${dmg.pc} from PC", style: TextStyle(color: Colors.grey)) : null, + subtitle: lineclear.lines > 0 ? Text("What should i write here?", style: TextStyle(color: Colors.grey)) : null, trailing: lineclear.lines > 0 ? Padding( padding: const EdgeInsets.only(right: 10.0), child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), @@ -583,7 +559,6 @@ class _DestinationCalculatorState extends State { ) ); totalDamage += dmg; - totalDamageNumber += dmg.total; } return Column( @@ -644,6 +619,11 @@ class _DestinationCalculatorState extends State { ), if (rules.combo) ListTile( title: Text("Combo Table"), + trailing: DropdownButton( + items: [for (var v in ComboTables.values) DropdownMenuItem(value: v.index, child: Text(v.name))], + value: rules.comboTable.index, + onChanged: (v) => setState((){rules.comboTable = ComboTables.values[v!];}), + ), ) ], ), @@ -730,11 +710,10 @@ class _DestinationCalculatorState extends State { children: [ Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), Spacer(), - Text(totalDamageNumber.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) + Text(totalDamage.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) ], ), ), - Text(totalDamage.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)), ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) ], ) From c5c5fab8ac385ccb64a911f3e5274ff331310197 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 13 Oct 2024 14:13:42 +0300 Subject: [PATCH 45/86] New 40 lines and Blitz averages --- lib/data_objects/tetrio_constants.dart | 74 ++++++++++++------------ lib/main.dart | 2 +- lib/utils/relative_timestamps.dart | 4 ++ lib/views/main_view.dart | 8 +-- lib/views/main_view_tiles.dart | 4 +- lib/views/sprint_and_blitz_averages.dart | 4 +- lib/widgets/singleplayer_record.dart | 8 +-- 7 files changed, 54 insertions(+), 50 deletions(-) diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index 934463d..2c3b217 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -166,46 +166,46 @@ const Map rankColors = { }; const Map sprintAverages = { - // based on https://discord.com/channels/673303546107658242/674421736162197515/1244287342965952562 - 'x': Duration(seconds: 25, milliseconds: 144), - 'u': Duration(seconds: 36, milliseconds: 115), - 'ss': Duration(seconds: 46, milliseconds: 396), - 's+': Duration(seconds: 55, milliseconds: 056), - 's': Duration(seconds: 61, milliseconds: 892), - 's-': Duration(seconds: 68, milliseconds: 918), - 'a+': Duration(seconds: 76, milliseconds: 187), - 'a': Duration(seconds: 83, milliseconds: 529), - 'a-': Duration(seconds: 88, milliseconds: 608), - 'b+': Duration(seconds: 97, milliseconds: 626), - 'b': Duration(seconds: 104, milliseconds: 687), - 'b-': Duration(seconds: 113, milliseconds: 372), - 'c+': Duration(seconds: 123, milliseconds: 461), - 'c': Duration(seconds: 135, milliseconds: 326), - 'c-': Duration(seconds: 147, milliseconds: 056), - 'd+': Duration(seconds: 156, milliseconds: 757), - 'd': Duration(seconds: 167, milliseconds: 393), - //'z': Duration(seconds: 66, milliseconds: 802) + // based on https://discord.com/channels/673303546107658242/674421736162197515/1277367281264889908 + 'x+': Duration(seconds: 18, milliseconds: 867), + 'x': Duration(seconds: 23, milliseconds: 277), + 'u': Duration(seconds: 28, milliseconds: 853), + 'ss': Duration(seconds: 35, milliseconds: 173), + 's+': Duration(seconds: 39, milliseconds: 028), + 's': Duration(seconds: 45, milliseconds: 807), + 's-': Duration(seconds: 48, milliseconds: 840), + 'a+': Duration(seconds: 54, milliseconds: 975), + 'a': Duration(seconds: 60, milliseconds: 287), + 'a-': Duration(seconds: 64, milliseconds: 019), + 'b+': Duration(seconds: 76, milliseconds: 531), + 'b': Duration(seconds: 77, milliseconds: 635), + 'b-': Duration(seconds: 92, milliseconds: 279), + 'c+': Duration(seconds: 97, milliseconds: 911), + 'c': Duration(seconds: 104, milliseconds: 700), + 'c-': Duration(seconds: 115, milliseconds: 173), + 'd+': Duration(seconds: 131, milliseconds: 486), + 'd': Duration(seconds: 158, milliseconds: 397), }; const Map blitzAverages = { - 'x': 600715, - 'u': 379418, - 'ss': 233740, - 's+': 158295, - 's': 125164, - 's-': 100933, - 'a+': 83593, - 'a': 68613, - 'a-': 60219, - 'b+': 51197, - 'b': 44171, - 'b-': 39045, - 'c+': 34130, - 'c': 28931, - 'c-': 25095, - 'd+': 22944, - 'd': 20728, - //'z': 72084 + 'x+': 879378, + 'x': 677479, + 'u': 485962, + 'ss': 369043, + 's+': 279242, + 's': 245619, + 's-': 199368, + 'a+': 162035, + 'a': 130949, + 'a-': 111505, + 'b+': 97251, + 'b': 83580, + 'b-': 70511, + 'c+': 56747, + 'c': 43002, + 'c-': 38925, + 'd+': 30483, + 'd': 22513, }; List seasonStarts = [ diff --git a/lib/main.dart b/lib/main.dart index c0b11f3..8e3a945 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/lib/utils/relative_timestamps.dart b/lib/utils/relative_timestamps.dart index 5176221..4096018 100644 --- a/lib/utils/relative_timestamps.dart +++ b/lib/utils/relative_timestamps.dart @@ -73,6 +73,10 @@ String get40lTime(int microseconds){ return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); } +String getALittleBitMoreNormalTime(Duration time){ + return "${intf.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}"; +} + String getMoreNormalTime(Duration time){ return "${nonsecs.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}"; } diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 9a593a6..c02d45f 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1137,10 +1137,10 @@ class _TwoRecordsThingy extends StatelessWidget { Widget build(BuildContext context) { late MapEntry closestAverageBlitz; late bool blitzBetterThanClosestAverage; - bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null; + bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null; late MapEntry closestAverageSprint; late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && sprint != null) ? sprint!.stats.finalTime < sprintAverages[rank]! : null; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.stats.finalTime < sprintAverages[rank]! : null; if (sprint != null) { closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.stats.finalTime).abs() < (b -sprint!.stats.finalTime).abs() ? a : b)); sprintBetterThanClosestAverage = sprint!.stats.finalTime < closestAverageSprint.value; @@ -1178,7 +1178,7 @@ class _TwoRecordsThingy extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.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: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( @@ -1261,7 +1261,7 @@ class _TwoRecordsThingy extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.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: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 5ea9cce..1cfd74e 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1823,7 +1823,7 @@ class RecordSummary extends StatelessWidget{ text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), _ => record!.stats.score.toString() @@ -2500,7 +2500,7 @@ class _DestinationHomeState extends State with SingleTickerProv text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), _ => record.stats.score.toString() diff --git a/lib/views/sprint_and_blitz_averages.dart b/lib/views/sprint_and_blitz_averages.dart index 9ac06ed..0fd8630 100644 --- a/lib/views/sprint_and_blitz_averages.dart +++ b/lib/views/sprint_and_blitz_averages.dart @@ -81,7 +81,7 @@ class SprintAndBlitzState extends State { Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/${sprintEntry.key}.png", height: 48)), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(get40lTime(sprintEntry.value.inMicroseconds), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + child: Text(getALittleBitMoreNormalTime(sprintEntry.value), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), @@ -91,7 +91,7 @@ class SprintAndBlitzState extends State { ) ], ), - Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 5, 26)))) + Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 8, 25)))) ], ), ), diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index 62bec0d..d2086b9 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -30,10 +30,10 @@ class SingleplayerRecord extends StatelessWidget { 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" && rank != "x+") ? record!.stats.score > blitzAverages[rank]! : null; + bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.score > blitzAverages[rank]! : null; late MapEntry closestAverageSprint; late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+") ? record!.stats.finalTime < sprintAverages[rank]! : null; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.finalTime < sprintAverages[rank]! : null; if (record!.gamemode == "40l") { closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.stats.finalTime).abs() < (b -record!.stats.finalTime).abs() ? a : b)); sprintBetterThanClosestAverage = record!.stats.finalTime < closestAverageSprint.value; @@ -76,13 +76,13 @@ class SingleplayerRecord extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (record!.gamemode == "40l" && (rank != null && rank != "z" && rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + if (record!.gamemode == "40l" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.stats.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!.gamemode == "40l" && (rank == null || rank == "z" || rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.stats.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!.gamemode == "blitz" && (rank != null && rank != "z" && rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + else if (record!.gamemode == "blitz" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.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!.gamemode == "blitz" && (rank == null || rank == "z" || rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( From 36aa31a0615582c40ce91466855b60c79ce61e13 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 13 Oct 2024 14:16:01 +0300 Subject: [PATCH 46/86] forgor to bump the version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index bfafc4d..6bd0b83 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.6.11+37 +version: 1.6.12+38 environment: sdk: '>=3.0.0' From ceb22716cc10ea610c20a113e591a3ba061e387e Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 16 Oct 2024 01:17:24 +0300 Subject: [PATCH 47/86] God why am i so slow --- lib/data_objects/tetrio_player.dart | 2 +- lib/main.dart | 2 +- lib/views/compare_view_tiles.dart | 6 +- lib/views/main_view_tiles.dart | 143 +++++++++++++++++++++++++++- web/index.html | 2 +- 5 files changed, 149 insertions(+), 6 deletions(-) diff --git a/lib/data_objects/tetrio_player.dart b/lib/data_objects/tetrio_player.dart index 02f3ed2..1746942 100644 --- a/lib/data_objects/tetrio_player.dart +++ b/lib/data_objects/tetrio_player.dart @@ -95,7 +95,7 @@ class TetrioPlayer { // data['_id'] = userId; // data['username'] = username; data['role'] = role; - if (registrationTime != null) data['ts'] = registrationTime?.toString(); + data['ts'] = registrationTime.toString(); if (badges.isNotEmpty) data['badges'] = badges.map((v) => v.toJson()).toList(); if (xp >= 0) data['xp'] = xp; if (gamesPlayed >= 0) data['gamesplayed'] = gamesPlayed; diff --git a/lib/main.dart b/lib/main.dart index 8e3a945..c0b11f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index ad13355..b772888 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -150,6 +150,8 @@ class CompareState extends State { "Pieces Per Second", "Key Presses Per Second", "" + // TODO: line clears + // TODO: spins ], "Blitz": [ "Score", @@ -407,7 +409,7 @@ class CompareState extends State { Text(s.sprint != null ? f4.format(s.sprint!.stats.kps) : "---") ]); formattedValues[5].add([ - Text(s.blitz != null ? getMoreNormalTime(s.blitz!.stats.finalTime) : "---"), + Text(s.blitz != null ? intf.format(s.sprint!.stats.score) : "---"), Text(s.blitz != null ? intf.format(s.blitz!.stats.piecesPlaced) : "---"), Text(s.blitz != null ? intf.format(s.blitz!.stats.inputs) : "---"), Text(s.blitz != null ? f4.format(s.blitz!.stats.kpp) : "---"), @@ -640,7 +642,7 @@ class CompareState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - for (int l = 0; l < formattedValues[i][k].length; l++) Container(decoration: BoxDecoration(color: (rawValues[0].length > 1 && rawValues[i][k][l] != null && best[i][l] == rawValues[i][k][l]) ? Colors.cyanAccent.withAlpha(96) : null), child: formattedValues[i][k][l]), + for (int l = 0; l < formattedValues[i][k].length; l++) Container(decoration: (rawValues[0].length > 1 && rawValues[i][k][l] != null && best[i][l] == rawValues[i][k][l]) ? BoxDecoration(boxShadow: [BoxShadow(color: Colors.cyanAccent.withAlpha(96), spreadRadius: 0, blurRadius: 4)]) : null, child: formattedValues[i][k][l]), ], ), ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 1cfd74e..c77697a 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -26,6 +26,7 @@ import 'package:tetra_stats/data_objects/record_single.dart'; import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; import 'package:tetra_stats/data_objects/summaries.dart'; import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_player.dart'; @@ -208,6 +209,7 @@ class _MainState extends State with TickerProviderStateMixin { 2 => DestinationLeaderboards(constraints: constraints), 3 => DestinationCutoffs(constraints: constraints), 4 => DestinationCalculator(constraints: constraints), + 6 => DestinationSavedData(constraints: constraints), _ => Text("Unknown destination $destination") }, ) @@ -217,6 +219,139 @@ class _MainState extends State with TickerProviderStateMixin { } } +class DestinationSavedData extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationSavedData({super.key, required this.constraints}); + + @override + State createState() => _DestinationSavedData(); +} + +class _DestinationSavedData extends State { + String? selectedID; + + Future<(List, List, List)> getDataAbout(String id) async { + return (await teto.getStates(id), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id)); + } + + Widget getTetraLeagueListTile(TetraLeague data){ + return ListTile( + title: Text(timestamp(data.timestamp)), + subtitle: Text("${intf.format(data.gamesPlayed)} games"), + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: teto.getAllPlayers(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + return Row( + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + const Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Saved Data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Spacer() + ], + ), + ), + for (String id in snapshot.data!.keys) Card( + child: ListTile( + title: Text(snapshot.data![id]!), + subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)), + onTap: () => setState(() { + selectedID = id; + }), + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: selectedID != null ? FutureBuilder<(List, List, List)>( + future: getDataAbout(selectedID!), + builder: (context, snapshot) { + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + return DefaultTabController( + length: 3, + child: Card( + child: Column( + children: [ + Card( + child: TabBar(tabs: [ + Tab(text: "S${currentSeason} TL States"), + Tab(text: "S1 TL States"), + Tab(text: "TL Records") + ]), + ), + SizedBox( + height: widget.constraints.maxHeight - 164, + child: TabBarView(children: [ + ListView.builder( + itemCount: snapshot.data!.$1.length, + itemBuilder: (context, index) { + return getTetraLeagueListTile(snapshot.data!.$1[index]); + },), + ListView.builder( + itemCount: snapshot.data!.$2.length, + itemBuilder: (context, index) { + return getTetraLeagueListTile(snapshot.data!.$2[index]); + },), + ListView.builder( + itemCount: snapshot.data!.$3.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(snapshot.data!.$3[index].toString()), + ); + },), + ] + ), + ) + ], + ), + ), + ); + } + return Text("what?"); + } + } + ) : + Text("Select nickname on the left to see data assosiated with it") + ) + ], + ); + } + } + return const Text("End of FutureBuilder"); + }, + ); + } +} + class DestinationCalculator extends StatefulWidget{ final BoxConstraints constraints; @@ -682,7 +817,13 @@ class _DestinationCalculatorState extends State { SizedBox( width: widget.constraints.maxWidth - 350 - 80, height: widget.constraints.maxHeight - 148, - child: clears.isEmpty ? Center(child: Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center)) : + child: clears.isEmpty ? Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), + Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center), + ], + )) : Card( child: Column( children: [ diff --git a/web/index.html b/web/index.html index 66c6956..a1c9ed2 100644 --- a/web/index.html +++ b/web/index.html @@ -131,7 +131,7 @@ } - +
From d27a57f0f85c9afdead6f131951108ae32ec779e Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 17 Oct 2024 02:10:08 +0300 Subject: [PATCH 48/86] Redesign Progress (?) --- lib/services/tetrio_crud.dart | 2 +- lib/views/compare_view_tiles.dart | 19 ++-- lib/views/main_view.dart | 4 - lib/views/main_view_tiles.dart | 148 +++++++++++++++++++++--------- lib/widgets/graphs.dart | 1 - lib/widgets/user_thingy.dart | 2 +- 6 files changed, 111 insertions(+), 65 deletions(-) diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index c74c16b..efce458 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -500,7 +500,7 @@ class TetrioService extends DB { } Future fetchCutoffsBeanserver() async { - Cutoffs? cached = _cache.get("", Cutoffs); + Cutoffs? cached = _cache.get("CutoffsTetrioleague_ranks", Cutoffs); if (cached != null) return cached; Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/cutoffs.json'); diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index b772888..5c9992a 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -7,13 +7,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/aggregate_stats.dart'; -import 'package:tetra_stats/data_objects/nerd_stats.dart'; -import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/data_objects/summaries.dart'; -import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_player.dart'; -import 'package:tetra_stats/data_objects/tetrio_zen.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/utils/numers_formats.dart'; @@ -21,9 +17,7 @@ import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/widgets/vs_graphs.dart'; import 'package:transparent_image/transparent_image.dart'; -import 'package:vector_math/vector_math_64.dart' hide Colors; import 'package:window_manager/window_manager.dart'; enum Mode{ @@ -308,7 +302,7 @@ class CompareState extends State { s.zen.level ]); formattedValues[0].add([ - Text(timestamp(p.registrationTime!)), + Text(timestamp(p.registrationTime)), RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round"), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), @@ -573,14 +567,14 @@ class CompareState extends State { )); } - void _justUpdate() { - setState(() {}); - } + // void _justUpdate() { + // setState(() {}); + // } @override Widget build(BuildContext context) { - final t = Translations.of(context); - bool bigScreen = MediaQuery.of(context).size.width > 768; + // final t = Translations.of(context); + // bool bigScreen = MediaQuery.of(context).size.width > 768; return Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.startTop, floatingActionButton: Padding( @@ -757,7 +751,6 @@ class _AddNewColumnCardState extends State with SingleTickerPr @override Widget build(BuildContext context) { - // TODO: implement build return SizedBox( height: 175.0, child: Card( diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index c02d45f..af9923b 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -295,10 +295,7 @@ class _MainState extends State with TickerProviderStateMixin { // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); // if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); // } - // Also i need previous Tetra League State for comparison if avaliable - TetraLeague? compareWith; if (states[1].length >= 2 || states[0].length >= 2){ - compareWith = states[1].length >= 2 ? states[1].elementAtOrNull(states.length - 2) : null; chartsData = [for (List s in states) >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), @@ -323,7 +320,6 @@ class _MainState extends State with TickerProviderStateMixin { DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), ]]; }else{ - compareWith = null; chartsData = []; } diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index c77697a..f918201 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -53,6 +53,8 @@ import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:transparent_image/transparent_image.dart'; import 'package:vector_math/vector_math_64.dart' hide Colors; +// TODO: Refactor it + var fDiff = NumberFormat("+#,###.####;-#,###.####"); late Future _data; late Future _newsData; @@ -232,13 +234,28 @@ class _DestinationSavedData extends State { String? selectedID; Future<(List, List, List)> getDataAbout(String id) async { - return (await teto.getStates(id), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id)); + return (await teto.getStates(id, season: currentSeason), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id)); } Widget getTetraLeagueListTile(TetraLeague data){ return ListTile( - title: Text(timestamp(data.timestamp)), - subtitle: Text("${intf.format(data.gamesPlayed)} games"), + title: Text("${timestamp(data.timestamp)}"), + subtitle: Text("${f2.format(data.apm)} APM, ${f2.format(data.pps)} PPS, ${f2.format(data.vs)} VS, ${intf.format(data.gamesPlayed)} games", style: TextStyle(color: Colors.grey)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(data.tr)} TR", style: TextStyle(fontSize: 28)), + Image.asset("res/tetrio_tl_alpha_ranks/${data.rank}.png", height: 36) + ], + ), + leading: IconButton( + onPressed: () { + teto.deleteState(data.id+data.timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(data.timestamp))))); + })); + }, + icon: Icon(Icons.delete_forever) + ), ); } @@ -274,7 +291,7 @@ class _DestinationSavedData extends State { for (String id in snapshot.data!.keys) Card( child: ListTile( title: Text(snapshot.data![id]!), - subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)), + //subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)), onTap: () => setState(() { selectedID = id; }), @@ -309,7 +326,7 @@ class _DestinationSavedData extends State { ]), ), SizedBox( - height: widget.constraints.maxHeight - 164, + height: widget.constraints.maxHeight - 64, child: TabBarView(children: [ ListView.builder( itemCount: snapshot.data!.$1.length, @@ -366,34 +383,6 @@ enum CalcCards{ damage } -class Damage{ - final int clear; - final double combo; - final double b2b; - final int surge; - final int pc; - final double multiplier; - - int get total => ((clear + combo + b2b + surge + pc) * multiplier).floor(); - - const Damage(this.clear, this.combo, this.b2b, this.surge, this.pc, this.multiplier); - - @override - String toString(){ - return total.toString(); - } - - Damage operator+(Damage other){ - return Damage( - clear+other.clear, - combo+other.combo, - b2b+other.b2b, - surge+other.surge, - pc+other.pc, - multiplier); - } -} - class ClearData{ final String title; final Lineclears lineclear; @@ -542,11 +531,11 @@ class _DestinationCalculatorState extends State { } } - void calcDamage(){ - for (ClearData lineclear in clears){ + // void calcDamage(){ + // for (ClearData lineclear in clears){ - } - } + // } + // } Widget getCalculator(){ return SingleChildScrollView( @@ -669,12 +658,22 @@ class _DestinationCalculatorState extends State { int b2b = -1; int previousB2B = -1; int totalDamage = 0; + int normalDamage = 0; + int comboDamage = 0; + int b2bDamage = 0; + int surgeDamage = 0; + int pcDamage = 0; for (ClearData lineclear in clears){ previousB2B = b2b; if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; if (lineclear.lines > 0) combo++; else combo = -1; + int pcDmg = lineclear.perfectClear ? (rules.pcDamage * rules.multiplier).floor() : 0; + int normalDmg = lineclear.dealsDamage(0, 0, 0, rules) - pcDmg; + int surgeDmg = (!lineclear.difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1) ? rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b) : 0; + int b2bDmg = lineclear.dealsDamage(0, b2b, b2b-1, rules) - normalDmg - pcDmg; int dmg = lineclear.dealsDamage(combo, b2b, previousB2B, rules); + int comboDmg = dmg - normalDmg - b2bDmg - surgeDmg - pcDmg; lSideWidgets.add( ListTile( key: ValueKey(lineclear.id), @@ -686,7 +685,7 @@ class _DestinationCalculatorState extends State { ], ), title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), - subtitle: lineclear.lines > 0 ? Text("What should i write here?", style: TextStyle(color: Colors.grey)) : null, + subtitle: lineclear.lines > 0 ? Text("${dmg == normalDmg ? "No bonuses" : ""}${b2bDmg > 0 ? "+${intf.format(b2bDmg)} for B2B" : ""}${(b2bDmg > 0 && comboDmg > 0) ? ", " : ""}${comboDmg > 0 ? "+${intf.format(comboDmg)} for combo" : ""}${(comboDmg > 0 && lineclear.perfectClear) ? ", " : ""}${lineclear.perfectClear ? "+${intf.format(pcDmg)} for PC" : ""}${(surgeDmg > 0 && (lineclear.perfectClear || comboDmg > 0)) ? ", " : ""}${surgeDmg > 0 ? "Surge released: +${intf.format(surgeDmg)}" : ""}", style: TextStyle(color: Colors.grey)) : null, trailing: lineclear.lines > 0 ? Padding( padding: const EdgeInsets.only(right: 10.0), child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), @@ -694,8 +693,17 @@ class _DestinationCalculatorState extends State { ) ); totalDamage += dmg; + normalDamage += normalDmg; + comboDamage += comboDmg; + b2bDamage += b2bDmg; + surgeDamage += surgeDmg; + pcDamage += pcDmg; } - + // values for "the bar" + double sec2end = normalDamage.toDouble()+comboDamage.toDouble(); + double sec3end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble(); + double sec4end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble(); + double sec5end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble()+pcDamage.toDouble(); return Column( children: [ Card( @@ -816,11 +824,12 @@ class _DestinationCalculatorState extends State { ), SizedBox( width: widget.constraints.maxWidth - 350 - 80, - height: widget.constraints.maxHeight - 148, + height: widget.constraints.maxHeight - 108, child: clears.isEmpty ? Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), + SizedBox(height: 5.0), Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center), ], )) : @@ -851,10 +860,58 @@ class _DestinationCalculatorState extends State { children: [ Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), Spacer(), - Text(totalDamage.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) + Text(intf.format(totalDamage), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) ], ), ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text("Lineclears: ${intf.format(normalDamage)}"), + Text("Combo: ${intf.format(comboDamage)}"), + Text("B2B: ${intf.format(b2bDamage)}"), + Text("Surge: ${intf.format(surgeDamage)}"), + Text("PC's: ${intf.format(pcDamage)}") + ], + ), + SfLinearGauge( + minimum: 0, + maximum: totalDamage.toDouble(), + showLabels: false, + showTicks: false, + ranges: [ + LinearGaugeRange( + color: Colors.green, + startValue: 0, + endValue: normalDamage.toDouble(), + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.yellow, + startValue: normalDamage.toDouble(), + endValue: sec2end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.blue, + startValue: sec2end, + endValue: sec3end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.red, + startValue: sec3end, + endValue: sec4end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.orange, + startValue: sec4end, + endValue: sec5end, + position: LinearElementPosition.cross, + ), + ], + ), ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) ], ) @@ -1387,6 +1444,7 @@ class _DestinationLeaderboardsState extends State { ), itemBuilder: (BuildContext context, int index){ return ListTile( + // TODO: make it clickable leading: Text(intf.format(index+1)), title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), trailing: switch (_currentLb){ @@ -1623,6 +1681,7 @@ class _DestinationGraphsState extends State { if (snapshot.hasData){ List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; yAxisTitle = chartsShortTitles[_Ychart]!; + // TODO: this graph can Krash return SfCartesianChart( tooltipBehavior: _historyTooltipBehavior, zoomPanBehavior: _zoomPanBehavior, @@ -3569,7 +3628,7 @@ class NewUserThingy extends StatelessWidget { style: const TextStyle(fontFamily: "Eurostile Round"), children: [ if (player.country != null) TextSpan(text: "${t.countries[player.country]} • "), - TextSpan(text: player.registrationTime == null ? t.wasFromBeginning : timestamp(player.registrationTime!), style: const TextStyle(color: Colors.grey)) + TextSpan(text: timestamp(player.registrationTime), style: const TextStyle(color: Colors.grey)) ] ) ), @@ -4203,11 +4262,10 @@ 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, this.separateScrollController = false}); + const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere}); @override Widget build(BuildContext context) { @@ -4225,7 +4283,7 @@ class _TLRecords extends StatelessWidget { int length = data.length; return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), - controller: separateScrollController ? ScrollController() : null, + //controller: separateScrollController ? ScrollController() : null, itemCount: oldMathcesHere ? length : length + 1, itemBuilder: (BuildContext context, int index) { if (index == length) { diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index 35e5153..3049eeb 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -7,7 +7,6 @@ import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart'; import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; import 'package:fl_chart/src/utils/canvas_wrapper.dart'; -import 'package:fl_chart/src/utils/utils.dart'; import 'package:tetra_stats/data_objects/nerd_stats.dart'; import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index ee6dc92..957f4d5 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} ${timestamp(player.registrationTime!)}'}"), + TextSpan(text: "${t.playerRole[player.role]}${t.playerRoleAccount}${t.created} ${timestamp(player.registrationTime)}"), 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)) From 7eb55f9638aca41b45857356b01c4845a669afd5 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 18 Oct 2024 01:17:23 +0300 Subject: [PATCH 49/86] `UserView` doesn't work as well, as i wanted, need different approach --- lib/views/destination_calculator.dart | 602 +++++ lib/views/destination_cutoffs.dart | 293 +++ lib/views/destination_graphs.dart | 475 ++++ lib/views/destination_home.dart | 1144 +++++++++ lib/views/destination_leaderboards.dart | 277 +++ lib/views/destination_saved_data.dart | 167 ++ lib/views/main_view_tiles.dart | 2942 +---------------------- lib/views/user_view.dart | 59 + 8 files changed, 3056 insertions(+), 2903 deletions(-) create mode 100644 lib/views/destination_calculator.dart create mode 100644 lib/views/destination_cutoffs.dart create mode 100644 lib/views/destination_graphs.dart create mode 100644 lib/views/destination_home.dart create mode 100644 lib/views/destination_leaderboards.dart create mode 100644 lib/views/destination_saved_data.dart create mode 100644 lib/views/user_view.dart diff --git a/lib/views/destination_calculator.dart b/lib/views/destination_calculator.dart new file mode 100644 index 0000000..b240c99 --- /dev/null +++ b/lib/views/destination_calculator.dart @@ -0,0 +1,602 @@ +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; + +class DestinationCalculator extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationCalculator({super.key, required this.constraints}); + + @override + State createState() => _DestinationCalculatorState(); +} + +enum CalcCards{ + calc, + damage +} + +class ClearData{ + final String title; + final Lineclears lineclear; + final int lines; + final bool miniSpin; + final bool spin; + bool perfectClear = false; + int id = -1; + + ClearData(this.title, this.lineclear, this.lines, this.miniSpin, this.spin); + + ClearData cloneWith(int i){ + ClearData newOne = ClearData(title, lineclear, lines, miniSpin, spin)..id = i; + return newOne; + } + + bool get difficultClear { + if (lines == 0) return false; + if (lines >= 4 || miniSpin || spin) return true; + else return false; + } + + void togglePC(){ + perfectClear = !perfectClear; + } + + int dealsDamage(int combo, int b2b, int previousB2B, Rules rules){ + if (lines == 0) return 0; + double damage = 0; + + if (spin){ + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); + } else if (miniSpin){ + damage += garbage[lineclear]!; + } else { + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.PENTA]! + (lines - 5); + } + + if (difficultClear && b2b >= 1 && rules.b2b){ + if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log(1 + (b2b) * BACKTOBACK_BONUS_LOG)).floor() + (b2b == 1 ? 0 : (1 + log(1 +(b2b) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? + else damage += 1; // if b2b chaining off + } + + if (rules.combo && rules.comboTable != ComboTables.none) { + if (combo >= 1){ + if (lines == 1 && rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; + else damage *= (1 + COMBO_BONUS * (combo)); + } + if (combo >= 2) { + damage = max(log(1 + COMBO_MINIFIER * (combo) * COMBO_MINIFIER_LOG), damage); + } + } + + if (!difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1){ + damage += rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b); + } + + if (perfectClear) damage += rules.pcDamage; + + return (damage * rules.multiplier).floor(); + } +} + +Map> clearsExisting = { + "No Spin Clears": [ + ClearData("No lineclear (Break Combo)", Lineclears.ZERO, 0, false, false), + ClearData("Single", Lineclears.SINGLE, 1, false, false), + ClearData("Double", Lineclears.DOUBLE, 2, false, false), + ClearData("Triple", Lineclears.TRIPLE, 3, false, false), + ClearData("Quad", Lineclears.QUAD, 4, false, false) + ], + "Spins": [ + ClearData("Spin Zero", Lineclears.TSPIN, 0, false, true), + ClearData("Spin Single", Lineclears.TSPIN_SINGLE, 1, false, true), + ClearData("Spin Double", Lineclears.TSPIN_DOUBLE, 2, false, true), + ClearData("Spin Triple", Lineclears.TSPIN_TRIPLE, 3, false, true), + ClearData("Spin Quad", Lineclears.TSPIN_QUAD, 4, false, true), + ], + "Mini spins": [ + ClearData("Mini Spin Zero", Lineclears.TSPIN_MINI, 0, true, false), + ClearData("Mini Spin Single", Lineclears.TSPIN_MINI_SINGLE, 1, true, false), + ClearData("Mini Spin Double", Lineclears.TSPIN_MINI_DOUBLE, 2, true, false), + ClearData("Mini Spin Triple", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false), + ] +}; + +class Rules{ + bool combo = true; + bool b2b = true; + bool b2bChaining = false; + bool surge = true; + int surgeInitAmount = 4; + int surgeInitAtB2b = 4; + ComboTables comboTable = ComboTables.multiplier; + int pcDamage = 5; + int pcB2B = 1; + double multiplier = 1.0; +} + +const TextStyle mainToggleInRules = TextStyle(fontSize: 18, fontWeight: ui.FontWeight.w800); + +class _DestinationCalculatorState extends State { + double? apm; + double? pps; + double? vs; + NerdStats? nerdStats; + EstTr? estTr; + Playstyle? playstyle; + TextEditingController ppsController = TextEditingController(); + TextEditingController apmController = TextEditingController(); + TextEditingController vsController = TextEditingController(); + + List clears = []; + Map customClearsChoice = { + "No Spin Clears": 5, + "Spins": 5 + }; + int idCounter = 0; + Rules rules = Rules(); + + CalcCards card = CalcCards.calc; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + void calc() { + apm = double.tryParse(apmController.text); + pps = double.tryParse(ppsController.text); + vs = double.tryParse(vsController.text); + if (apm != null && pps != null && vs != null) { + 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); + setState(() {}); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Please, enter valid values"))); + } + } + + // void calcDamage(){ + // for (ClearData lineclear in clears){ + + // } + // } + + Widget getCalculator(){ + return SingleChildScrollView( + child: Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Stats Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: apmController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("APM"), alignLabelWithHint: true, hintText: "Enter your APM"), + ), + ) + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: ppsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("PPS"), alignLabelWithHint: true, hintText: "Enter your PPS"), + ), + ) + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: vsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("VS"), alignLabelWithHint: true, hintText: "Enter your VS"), + ), + ) + ), + TextButton( + onPressed: () => calc(), + child: Text(t.calc), + ), + ], + ), + ), + ), + if (nerdStats != null) Card( + child: NerdStatsThingy(nerdStats: nerdStats!) + ), + if (playstyle != null) Card( + child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!) + ) + ], + ), + ); + } + + Widget getDamageCalculator(){ + List rSideWidgets = []; + List lSideWidgets = []; + + for (var key in clearsExisting.keys){ + rSideWidgets.add(Text(key)); + for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( + child: ListTile( + title: Text(data.title), + subtitle: Text("${data.dealsDamage(0, 0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + trailing: Icon(Icons.arrow_forward_ios), + onTap: (){ + setState((){ + clears.add(data.cloneWith(idCounter)); + }); + idCounter++; + }, + ), + )); + if (key != "Mini spins") rSideWidgets.add(Card( + child: ListTile( + title: Text("Custom"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 30.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: "5"), + onChanged: (value) => customClearsChoice[key] = int.parse(value), + )), + Text(" Lines", style: TextStyle(fontSize: 18)), + Icon(Icons.arrow_forward_ios) + ], + ), + onTap: (){ + setState((){ + clears.add(ClearData("${key == "Spins" ? "Spin " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} Lines)", key == "Spins" ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, customClearsChoice[key]!, false, key == "Spins").cloneWith(idCounter)); + }); + idCounter++; + }, + ), + )); + rSideWidgets.add(const Divider()); + } + + int combo = -1; + int b2b = -1; + int previousB2B = -1; + int totalDamage = 0; + int normalDamage = 0; + int comboDamage = 0; + int b2bDamage = 0; + int surgeDamage = 0; + int pcDamage = 0; + + for (ClearData lineclear in clears){ + previousB2B = b2b; + if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; + if (lineclear.lines > 0) combo++; else combo = -1; + int pcDmg = lineclear.perfectClear ? (rules.pcDamage * rules.multiplier).floor() : 0; + int normalDmg = lineclear.dealsDamage(0, 0, 0, rules) - pcDmg; + int surgeDmg = (!lineclear.difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1) ? rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b) : 0; + int b2bDmg = lineclear.dealsDamage(0, b2b, b2b-1, rules) - normalDmg - pcDmg; + int dmg = lineclear.dealsDamage(combo, b2b, previousB2B, rules); + int comboDmg = dmg - normalDmg - b2bDmg - surgeDmg - pcDmg; + lSideWidgets.add( + ListTile( + key: ValueKey(lineclear.id), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)), + if (lineclear.lines > 0) IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.local_parking_outlined, color: lineclear.perfectClear ? Colors.white : Colors.grey.shade800)), + ], + ), + title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), + subtitle: lineclear.lines > 0 ? Text("${dmg == normalDmg ? "No bonuses" : ""}${b2bDmg > 0 ? "+${intf.format(b2bDmg)} for B2B" : ""}${(b2bDmg > 0 && comboDmg > 0) ? ", " : ""}${comboDmg > 0 ? "+${intf.format(comboDmg)} for combo" : ""}${(comboDmg > 0 && lineclear.perfectClear) ? ", " : ""}${lineclear.perfectClear ? "+${intf.format(pcDmg)} for PC" : ""}${(surgeDmg > 0 && (lineclear.perfectClear || comboDmg > 0)) ? ", " : ""}${surgeDmg > 0 ? "Surge released: +${intf.format(surgeDmg)}" : ""}", style: TextStyle(color: Colors.grey)) : null, + trailing: lineclear.lines > 0 ? Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), + ) : null, + ) + ); + totalDamage += dmg; + normalDamage += normalDmg; + comboDamage += comboDmg; + b2bDamage += b2bDmg; + surgeDamage += surgeDmg; + pcDamage += pcDmg; + } + // values for "the bar" + double sec2end = normalDamage.toDouble()+comboDamage.toDouble(); + double sec3end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble(); + double sec4end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble(); + double sec5end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble()+pcDamage.toDouble(); + return Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Damage Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 350.0, + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: TabBar(tabs: [ + Tab(text: "Actions"), + Tab(text: "Rules"), + ]), + ), + SizedBox( + height: widget.constraints.maxHeight - 164, + child: TabBarView(children: [ + SingleChildScrollView( + child: Column( + children: rSideWidgets, + ), + ), + SingleChildScrollView( + child: Column( + children: [ + Card( + child: ListTile( + title: Text("Multiplier", style: mainToggleInRules), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))], + decoration: InputDecoration(hintText: rules.multiplier.toString()), + onChanged: (value) => setState((){rules.multiplier = double.parse(value);}), + )), + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Combo", style: mainToggleInRules), + trailing: Switch(value: rules.combo, onChanged: (v) => setState((){rules.combo = v;})), + ), + if (rules.combo) ListTile( + title: Text("Combo Table"), + trailing: DropdownButton( + items: [for (var v in ComboTables.values) DropdownMenuItem(value: v.index, child: Text(v.name))], + value: rules.comboTable.index, + onChanged: (v) => setState((){rules.comboTable = ComboTables.values[v!];}), + ), + ) + ], + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Back-To-Back (B2B)", style: mainToggleInRules), + trailing: Switch(value: rules.b2b, onChanged: (v) => setState((){rules.b2b = v;})), + ), + if (rules.b2b) ListTile( + title: Text("Back-To-Back Chaining"), + trailing: Switch(value: rules.b2bChaining, onChanged: (v) => setState((){rules.b2bChaining = v;})), + ), + ], + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Surge", style: mainToggleInRules), + trailing: Switch(value: rules.surge, onChanged: (v) => setState((){rules.surge = v;})), + ), + if (rules.surge) ListTile( + title: Text("Starts at B2B"), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: rules.surgeInitAtB2b.toString()), + onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}), + )), + ), + if (rules.surge) ListTile( + title: Text("Start amount"), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: rules.surgeInitAmount.toString()), + onChanged: (value) => setState((){rules.surgeInitAmount = int.parse(value);}), + )), + ), + ], + ), + ) + ], + ), + ) + ]), + ) + ], + ) + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 350 - 80, + height: widget.constraints.maxHeight - 108, + child: clears.isEmpty ? Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), + SizedBox(height: 5.0), + Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center), + ], + )) : + Card( + child: Column( + children: [ + Expanded( + child: ReorderableListView( + onReorder: (oldIndex, newIndex) { + setState((){ + if (oldIndex < newIndex) { + newIndex -= 1; + } + final ClearData item = clears.removeAt(oldIndex); + clears.insert(newIndex, item); + }); + }, + children: lSideWidgets, + ), + ), + Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 34.0, 0.0), + child: Row( + children: [ + Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), + Spacer(), + Text(intf.format(totalDamage), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text("Lineclears: ${intf.format(normalDamage)}"), + Text("Combo: ${intf.format(comboDamage)}"), + Text("B2B: ${intf.format(b2bDamage)}"), + Text("Surge: ${intf.format(surgeDamage)}"), + Text("PC's: ${intf.format(pcDamage)}") + ], + ), + SfLinearGauge( + minimum: 0, + maximum: totalDamage.toDouble(), + showLabels: false, + showTicks: false, + ranges: [ + LinearGaugeRange( + color: Colors.green, + startValue: 0, + endValue: normalDamage.toDouble(), + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.yellow, + startValue: normalDamage.toDouble(), + endValue: sec2end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.blue, + startValue: sec2end, + endValue: sec3end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.red, + startValue: sec3end, + endValue: sec4end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.orange, + startValue: sec4end, + endValue: sec5end, + position: LinearElementPosition.cross, + ), + ], + ), + ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) + ], + ) + ], + ), + ), + ) + ], + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: widget.constraints.maxHeight -32, + child: switch (card){ + CalcCards.calc => getCalculator(), + CalcCards.damage => getDamageCalculator() + } + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: CalcCards.calc, + label: Text('Stats Calculator'), + ), + ButtonSegment( + value: CalcCards.damage, + label: Text('Damage Calculator'), + ), + ], + selected: {card}, + onSelectionChanged: (Set newSelection) { + setState(() { + card = newSelection.first; + });}) + ], + ); + } + +} diff --git a/lib/views/destination_cutoffs.dart b/lib/views/destination_cutoffs.dart new file mode 100644 index 0000000..7c014a9 --- /dev/null +++ b/lib/views/destination_cutoffs.dart @@ -0,0 +1,293 @@ +import 'dart:async'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:vector_math/vector_math_64.dart' hide Colors; + +class FetchCutoffsResults{ + late bool success; + CutoffsTetrio? cutoffs; + Exception? exception; + + FetchCutoffsResults(this.success, this.cutoffs, this.exception); +} + +class DestinationCutoffs extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationCutoffs({super.key, required this.constraints}); + + @override + State createState() => _DestinationCutoffsState(); +} + +class _DestinationCutoffsState extends State { + + Future fetch() async { + TetrioPlayerFromLeaderboard top1; + CutoffsTetrio cutoffs; + List requests = await Future.wait([ + teto.fetchCutoffsTetrio(), + teto.fetchTopOneFromTheLeaderboard(), + ]); + cutoffs = requests[0]; + top1 = requests[1]; + cutoffs.data["top1"] = CutoffTetrio( + pos: 1, + percentile: 0.00, + tr: top1.tr, + targetTr: 25000, + apm: top1.apm, + pps: top1.pps, + vs: top1.vs, + count: 1, + countPercentile: 0.0 + ); + return cutoffs; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: fetch(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return SingleChildScrollView( + child: Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Tetra League State", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("as of ${timestamp(snapshot.data!.timestamp)}"), + ], + ), + )), + ), + Padding( + padding: const EdgeInsets.only(bottom:4.0), + child: Card( + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text("Actual"), + ), + Text("Target") + ] + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), + child: SfLinearGauge( + minimum: 0.00000000, + maximum: 25000.0000, + showTicks: false, + showLabels: false, + ranges: [ + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.outside, + startValue: snapshot.data!.data[cutoff]!.tr, + startWidth: 20.0, + endWidth: 20.0, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.tr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr + }, + color: cutoff != "top1" ? rankColors[cutoff] : Colors.grey.shade800, + ), + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.inside, + startValue: snapshot.data!.data[cutoff]!.targetTr, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.targetTr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr + }, + color: cutoff != "top1" ? rankColors[cutoff] : null, + ), + for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[cutoff]!.tr < snapshot.data!.data[cutoff]!.targetTr) LinearGaugeRange( + position: LinearElementPosition.cross, + startValue: snapshot.data!.data[cutoff]!.tr, + endValue: snapshot.data!.data[cutoff]!.targetTr, + color: Colors.green, + ), + for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr)LinearGaugeRange( + position: LinearElementPosition.cross, + startValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr, + endValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr, + color: Colors.red, + ), + ], + markerPointers: [ + for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.tr), style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(0, 35, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0), value: snapshot.data!.data[cutoff]!.tr, position: LinearElementPosition.outside, offset: 20), + for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.targetTr), textAlign: ui.TextAlign.right, style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(-15, 0, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0, transformAlignment: Alignment.topRight), value: snapshot.data!.data[cutoff]!.targetTr, position: LinearElementPosition.inside, offset: 6) + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all(color: Colors.grey.shade900), + columnWidths: const { + 0: FixedColumnWidth(48), + 1: FixedColumnWidth(155), + 2: FixedColumnWidth(140), + 3: FixedColumnWidth(160), + 4: FixedColumnWidth(150), + 5: FixedColumnWidth(90), + 6: FixedColumnWidth(130), + 7: FixedColumnWidth(120), + 8: FixedColumnWidth(125), + 9: FixedColumnWidth(70), + }, + children: [ + TableRow( + children: [ + Text("Rank", textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Cutoff TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Target TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("State", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("More info", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + ), + ] + ), + for (String rank in snapshot.data!.data.keys) if (rank != "top1") TableRow( + decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])), + children: [ + Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), + children: [ + if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), + TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), + if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) + ] + )), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} APP\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} VS/APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), + children: [ + TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)), + TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)), + TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null)) + ] + )) + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { + + },), + ), + ] + ) + ], + ) + ] + ), + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + } + ); + } +} diff --git a/lib/views/destination_graphs.dart b/lib/views/destination_graphs.dart new file mode 100644 index 0000000..6073bac --- /dev/null +++ b/lib/views/destination_graphs.dart @@ -0,0 +1,475 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class DestinationGraphs extends StatefulWidget{ + final String searchFor; + //final Function setState; + final BoxConstraints constraints; + + const DestinationGraphs({super.key, required this.searchFor, required this.constraints}); + + @override + State createState() => _DestinationGraphsState(); +} + +enum Graph{ + history, + leagueState, + leagueCutoffs +} + +class _DestinationGraphsState extends State { + bool fetchData = false; + bool _gamesPlayedInsteadOfDateAndTime = false; + late ZoomPanBehavior _zoomPanBehavior; + late TooltipBehavior _historyTooltipBehavior; + late TooltipBehavior _tooltipBehavior; + late TooltipBehavior _leagueTooltipBehavior; + String yAxisTitle = ""; + bool _smooth = false; + final List> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; + Graph _graph = Graph.history; + Stats _Ychart = Stats.tr; + Stats _Xchart = Stats.tr; + int _season = currentSeason-1; + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + + @override + void initState(){ + _historyTooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${f4.format(data.stat)} $yAxisTitle", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + ), + ), + Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) + ], + ), + ); + } + ); + _tooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${data.nickname} (${data.rank.toUpperCase()})", + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20), + ), + ), + Text('${f4.format(data.x)} ${chartsShortTitles[_Xchart]}\n${f4.format(data.y)} ${chartsShortTitles[_Ychart]}') + ], + ), + ); + } + ); + _leagueTooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${f4.format(point.y)} $yAxisTitle", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + ), + ), + Text(timestamp(data.ts)) + ], + ), + ); + } + ); + _zoomPanBehavior = ZoomPanBehavior( + enablePinching: true, + enableSelectionZooming: true, + enableMouseWheelZooming : true, + enablePanning: true, + ); + super.initState(); + } + + Future>>> getHistoryData(bool fetchHistory) async { + if(fetchHistory){ + try{ + var history = await teto.fetchAndsaveTLHistory(widget.searchFor); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); + }on TetrioHistoryNotExist{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noHistorySaved))); + }on P1nkl0bst3rForbidden { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); + }on P1nkl0bst3rInternalProblem { + 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))); + } + } + + List> states = await Future.wait>([ + teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), + ]); + List>> historyData = []; // [season][metric][spot] + for (int season = 0; season < currentSeason; season++){ + if (states[season].length >= 2){ + Map> statsMap = {}; + for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())]; + historyData.add(statsMap); + }else{ + historyData.add({}); + break; + } + } + fetchData = false; + + return historyData; + } + + Future> getTetraLeagueData(Stats x, Stats y) async { + TetrioPlayersLeaderboard leaderboard = await teto.fetchTLLeaderboard(); + List<_MyScatterSpot> _spots = [ + for (TetrioPlayerFromLeaderboard entry in leaderboard.leaderboard) + _MyScatterSpot( + entry.getStatByEnum(x).toDouble(), + entry.getStatByEnum(y).toDouble(), + entry.userId, + entry.username, + entry.rank, + rankColors[entry.rank]??Colors.white + ) + ]; + return _spots; + } + + Widget getHistoryGraph(){ + return FutureBuilder>>>( + future: getHistoryData(fetchData), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; + yAxisTitle = chartsShortTitles[_Ychart]!; + // TODO: this graph can Krash + return SfCartesianChart( + tooltipBehavior: _historyTooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( + rangePadding: ChartRangePadding.additional, + ), + margin: const EdgeInsets.all(0), + series: [ + if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( + enableTooltip: true, + dataSource: selectedGraph, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (selectedGraph.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ) + else StepLineSeries<_HistoryChartSpot, DateTime>( + enableTooltip: true, + dataSource: selectedGraph, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (selectedGraph.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ), + ], + ); + }else{ return FutureError(snapshot); } + } + } + ); + } + + Widget getLeagueState (){ + return FutureBuilder>( + future: getTetraLeagueData(_Xchart, _Ychart), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + //primaryXAxis: CategoryAxis(), + series: [ + ScatterSeries( + enableTooltip: true, + dataSource: snapshot.data, + animationDuration: 0, + pointColorMapper: (data, _) => data.color, + xValueMapper: (data, _) => data.x, + yValueMapper: (data, _) => data.y, + onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: snapshot.data![point.pointIndex!].nickname), maintainState: false)), + ) + ], + ); + }else{ return FutureError(snapshot); } + } + } + ); + } + + Widget getCutoffsHistory(){ + return FutureBuilder>( + future: teto.fetchCutoffsHistory(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + yAxisTitle = chartsShortTitles[_Ychart]!; + return SfCartesianChart( + tooltipBehavior: _leagueTooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: const DateTimeAxis(), + primaryYAxis: NumericAxis( + // isInversed: true, + maximum: switch (_Ychart){ + Stats.tr => 25000.0, + Stats.gxe => 100.00, + _ => null + }, + ), + margin: const EdgeInsets.all(0), + series: [ + for (String rank in ranks) StepLineSeries( + enableTooltip: true, + dataSource: snapshot.data, + animationDuration: 0, + //opacity: 0.5, + xValueMapper: (Cutoffs data, _) => data.ts, + yValueMapper: (Cutoffs data, _) => switch (_Ychart){ + Stats.glicko => data.glicko[rank], + Stats.gxe => data.gxe[rank], + _ => data.tr[rank] + }, + color: rankColors[rank]! + ) + ], + ); + }else{ return FutureError(snapshot); } + } + } + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Wrap( + spacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (_graph == Graph.history) Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], + value: _season, + onChanged: (value) { + setState(() { + _season = value!; + }); + } + ), + ], + ), + if (_graph != Graph.leagueCutoffs) Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: switch (_graph){ + Graph.history => [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], + Graph.leagueState => _yAxis, + Graph.leagueCutoffs => [], + }, + value: _graph == Graph.history ? _gamesPlayedInsteadOfDateAndTime : _Xchart, + onChanged: (value) { + setState(() { + if (_graph == Graph.history) + _gamesPlayedInsteadOfDateAndTime = value! as bool; + else _Xchart = value! as Stats; + }); + } + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: _graph == Graph.leagueCutoffs ? [DropdownMenuItem(value: Stats.tr, child: Text(chartsShortTitles[Stats.tr]!)), DropdownMenuItem(value: Stats.glicko, child: Text(chartsShortTitles[Stats.glicko]!)), DropdownMenuItem(value: Stats.gxe, child: Text(chartsShortTitles[Stats.gxe]!))] : _yAxis, + value: _Ychart, + onChanged: (value) { + setState(() { + _Ychart = value!; + }); + } + ), + ], + ), + if (_graph != Graph.leagueState) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox(value: _smooth, + checkColor: Colors.black, + onChanged: ((value) { + setState(() { + _smooth = value!; + }); + })), + Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) + ], + ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) + ], + ), + ), + Card( + child: SizedBox( + width: MediaQuery.of(context).size.width - 88, + height: MediaQuery.of(context).size.height - 96, + child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), + child: switch (_graph){ + Graph.history => getHistoryGraph(), + Graph.leagueState => getLeagueState(), + Graph.leagueCutoffs => getCutoffsHistory() + }, + ) + ), + ) + ], + ), + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: Graph.history, + label: Text('Player History')), + ButtonSegment( + value: Graph.leagueState, + label: Text('League State')), + ButtonSegment( + value: Graph.leagueCutoffs, + label: Text('League Cutoffs'), + ), + ], + selected: {_graph}, + onSelectionChanged: (Set newSelection) { + setState(() { + _graph = newSelection.first; + switch (newSelection.first){ + case Graph.leagueCutoffs: + case Graph.history: + _Ychart = Stats.tr; + case Graph.leagueState: + _Ychart = Stats.apm; + } + });}) + ], + ); + } +} + +class _HistoryChartSpot{ + final DateTime timestamp; + final int gamesPlayed; + final String rank; + final double stat; + const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat); +} + +class _MyScatterSpot{ + num x; + num y; + String id; + String nickname; + String rank; + Color color; + _MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); +} diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart new file mode 100644 index 0000000..97aa9d5 --- /dev/null +++ b/lib/views/destination_home.dart @@ -0,0 +1,1144 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/record_extras.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.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/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/singleplayer_record_view.dart'; +import 'package:tetra_stats/widgets/finesse_thingy.dart'; +import 'package:tetra_stats/widgets/lineclears_thingy.dart'; +import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class DestinationHome extends StatefulWidget{ + final String searchFor; + final Future dataFuture; + final Future? newsFuture; + final BoxConstraints constraints; + + const DestinationHome({super.key, required this.searchFor, required this.dataFuture, this.newsFuture, required this.constraints}); + + @override + State createState() => _DestinationHomeState(); +} + +class FetchResults{ + bool success; + TetrioPlayer? player; + List states; + Summaries? summaries; + Cutoffs? cutoffs; + Exception? exception; + + FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.exception); +} + +class RecordSummary extends StatelessWidget{ + final RecordSingle? record; + final bool hideRank; + final bool? betterThanRankAverage; + final MapEntry? closestAverage; + final bool? betterThanClosestAverage; + final String? rank; + + const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, this.hideRank = false}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null && record != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96)) + else !hideRank ? Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) : Container(), + if (record != null) Column( + crossAxisAlignment: hideRank ? CrossAxisAlignment.center : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + text: TextSpan( + text: switch(record!.gamemode){ + "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), + "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(record!.stats.zenith!.altitude)} m", + "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", + _ => record!.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white, height: 0.9), + ), + ), + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), + _ => record!.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), + "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), + _ => record!.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), + if (record!.rank != -1 && record!.countryRank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), + const TextSpan(text: "\n"), + TextSpan(text: timestamp(record!.timestamp)), + ] + ), + ), + ], + ) else if (hideRank) RichText(text: const TextSpan( + text: "---", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), + ), + ) + ], + ); + } + +} + +class LeagueCard extends StatelessWidget{ + final TetraLeague league; + final bool showSeasonNumber; + + const LeagueCard({super.key, required this.league, this.showSeasonNumber = false}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (showSeasonNumber) Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("Season ${league.season}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Spacer(), + Text( + "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey)), + ], + ) + else Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + TLRatingThingy(userID: "", tlData: league, showPositions: true), + const Divider(), + Text("${league.apm != null ? f2.format(league.apm) : "-.--"} APM • ${league.pps != null ? f2.format(league.pps) : "-.--"} PPS • ${league.vs != null ? f2.format(league.vs) : "-.--"} VS • ${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP • ${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ); + } + +} + +class _DestinationHomeState extends State with SingleTickerProviderStateMixin { + Cards rightCard = Cards.overview; + CardMod cardMod = CardMod.info; + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + late Map>> modeButtons; + late MapEntry? closestAverageBlitz; + late bool blitzBetterThanClosestAverage; + late MapEntry? closestAverageSprint; + late bool sprintBetterThanClosestAverage; + late AnimationController _transition; + late final Animation _offsetAnimation; + bool? sprintBetterThanRankAverage; + bool? blitzBetterThanRankAverage; + + Widget getOverviewCard(Summaries summaries){ + return Column( + children: [ + const Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Overview", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + ), + ), + ), + LeagueCard(league: summaries.league), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), + const Divider(), + Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), + const Divider(), + Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.zenith, hideRank: true), + const Divider(), + Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.zenithEx, hideRank: true,), + const Divider(), + Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), + Text("Score ${intf.format(summaries.zen.score)}"), + Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("f", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text("${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? + f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: const TextStyle( + //shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + Row( + children: [ + const Text("Total pieces placed:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"), + ], + ), + Row( + children: [ + const Text(" - Placed with perfect finesse:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"), + ], + ) + ], + ), + ), + ), + ), + ], + ), + if (summaries.achievements.isNotEmpty) Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), + child: Column( + children: [ + if (summaries.achievements.firstWhere((e) => e.k == 16).v != null) Row( + children: [ + const Text("Total height climbed in QP"), + const Spacer(), + Text("${f2.format(summaries.achievements.firstWhere((e) => e.k == 16).v!)} m"), + ], + ), + if (summaries.achievements.firstWhere((e) => e.k == 17).v != null) Row( + children: [ + const Text("KO's in QP"), + const Spacer(), + Text(intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)), + ], + ) + ], + ), + ), + ), + ] + ); + } + + Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, List states){ + TetraLeague? toCompare = states.length >= 2 ? states.elementAtOrNull(states.length-2) : null; + return Column( + children: [ + Card( + //surfaceTintColor: rankColors[data.rank], + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${states.last.timestamp} ${states.last.tr}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + TetraLeagueThingy(league: data, toCompare: toCompare, cutoffs: cutoffs), + if (data.nerdStats != null) Card( + //surfaceTintColor: rankColors[data.rank], + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats), + if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) + ], + ); + } + + Widget getPreviousSeasonsList(Map pastLeague){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Previous Seasons", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + for (var key in pastLeague.keys) Card( + child: LeagueCard(league: pastLeague[key]!, showSeasonNumber: true), + ) + ], + ); + } + + Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ + return Column( + children: [ + const Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const TabBar( + tabs: [ + Tab(text: "Recent"), + Tab(text: "Top"), + ], + ), + SizedBox( + height: 400, + child: TabBarView( + children: [ + FutureBuilder( + future: teto.fetchStream(widget.searchFor, recentStream), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => "40L", + "blitz" => "BLZ", + "5mblast" => "5MB", + "zenith" => "QP", + "zenithex" => "QPE", + String() => "huh", + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + FutureBuilder( + future: teto.fetchStream(widget.searchFor, topStream), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + "#${i+1}", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + ] + ), + ) + ], + ), + ) + ), + ], + ); + } + + Widget getRecentTLrecords(BoxConstraints constraints){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: FutureBuilder( + future: teto.fetchTLStream(widget.searchFor), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return SizedBox(height: constraints.maxHeight - 145, child: TLRecords(userID: widget.searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + ), + ], + ); + } + + Widget getZenithCard(RecordSingle? record){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), + ], + ), + ), + ), + ), + ZenithThingy(zenith: record), + if (record != null) Row( + children: [ + Expanded( + child: Card( + child: Column( + children: [ + FinesseThingy(record.stats.finesse, record.stats.finessePercentage), + LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins, showMoreClears: true), + if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") + ], + ), + ), + ), + Expanded( + child: Card( + child: SizedBox( + width: 300, + height: 318, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("T", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text(getMoreNormalTime(record.stats.finalTime), style: const TextStyle( + shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + SizedBox( + width: 300.0, + child: Table( + columnWidths: const { + 0: FixedColumnWidth(36) + }, + children: [ + const TableRow( + children: [ + Text("Floor"), + Text("Split", textAlign: TextAlign.right), + Text("Total", textAlign: TextAlign.right), + ] + ), + for (int i = 0; i < record.stats.zenith!.splits.length; i++) TableRow( + children: [ + Text((i+1).toString()), + Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]-(i-1 != -1 ? record.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), + Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), + ] + ) + ], + ), + ), + ], + ), + ), + ), + ), + ], + ), + if (record != null) Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + if (record != null) NerdStatsThingy(nerdStats: record.aggregateStats.nerdStats), + if (record != null) GraphsThingy(nerdStats: record.aggregateStats.nerdStats, playstyle: record.aggregateStats.playstyle, apm: record.aggregateStats.apm, pps: record.aggregateStats.pps, vs: record.aggregateStats.vs) + ], + ); + } + + Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ + if (record == null) { + return const Card( + child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), + ); + } + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(switch(record.gamemode){ + "40l" => t.sprint, + "blitz" => t.blitz, + "5mblast" => "5,000,000 Blast", + _ => record.gamemode + }, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)) + ], + ), + ), + ), + ), + Card( + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText(text: TextSpan( + text: switch(record.gamemode){ + "40l" => get40lTime(record.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record.stats.score), + "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), + _ => record.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), + _ => record.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z" || rank == "x+") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), + "blitz" => readableIntDifference(record.stats.score, closestAverage.value), + _ => record.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record.rank != -1) TextSpan(text: "№ ${intf.format(record.rank)}", style: TextStyle(color: getColorOfRank(record.rank))), + if (record.rank != -1) const TextSpan(text: " • "), + if (record.countryRank != -1) TextSpan(text: "№ ${intf.format(record.countryRank)} local", style: TextStyle(color: getColorOfRank(record.countryRank))), + if (record.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(record.timestamp)), + ] + ), + ), + ], + ), + ], + ), + Row( + children: [ + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => record.stats.piecesPlaced.toString(), + "blitz" => record.stats.level.toString(), + "5mblast" => NumberFormat.decimalPattern().format(record.stats.spp), + _ => "What if " + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " Pieces", + "blitz" => " Level", + "5mblast" => " SPP", + _ => " i wanted to" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" PPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => f2.format(record.stats.kpp), + "blitz" => f2.format(record.stats.spp), + "5mblast" => record.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " KPP", + "blitz" => " SPP", + "5mblast" => " Pieces", + _ => " no" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Key presses", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" KPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => " ", + "blitz" => record.stats.piecesPlaced.toString(), + "5mblast" => record.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " ", + "blitz" => " Pieces", + "5mblast" => " Pieces", + _ => " no" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + ], + ) + ], + ), + ), + Card( + child: Center( + child: Column( + children: [ + FinesseThingy(record.stats.finesse, record.stats.finessePercentage), + LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins), + if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") + ], + ), + ), + ) + ] + ); + } + + @override + initState(){ + modeButtons = { + Cards.overview: [ + const ButtonSegment( + value: CardMod.info, + label: Text('General'), + ), + ], + Cards.tetraLeague: [ + const ButtonSegment( + value: CardMod.info, + label: Text('Standing'), + ), + const ButtonSegment( + value: CardMod.ex, // yeah i misusing my own Enum shut the fuck up + label: Text('Previous Seasons'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Recent Matches'), + ), + ], + Cards.quickPlay: [ + const ButtonSegment( + value: CardMod.info, + label: Text('Normal'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ), + const ButtonSegment( + value: CardMod.ex, + label: Text('Expert'), + ), + const ButtonSegment( + value: CardMod.exRecords, + label: Text('Expert Records'), + ) + ], + Cards.blitz: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ) + ], + Cards.sprint: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ) + ] + }; + + _transition = AnimationController(vsync: this, duration: Durations.long4); + + // _transition.addListener((){ + // setState(() { + + // }); + // }); + + _offsetAnimation = Tween( + begin: Offset.zero, + end: const Offset(1.5, 0.0), + ).animate(CurvedAnimation( + parent: _transition, + curve: Curves.elasticIn, + )); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: widget.dataFuture, + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + if (!snapshot.data!.success) return FetchResultError(snapshot.data!); + blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; + sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; + if (snapshot.data!.summaries!.sprint != null) { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; + } + if (snapshot.data!.summaries!.blitz != null){ + closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); + blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; + } + return TweenAnimationBuilder( + duration: Durations.long4, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(0, 600-value*600, 0), + child: Opacity(opacity: value, child: child), + ); + }, + child: Row( + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + NewUserThingy(player: snapshot.data!.player!, showStateTimestamp: false, setState: setState), + if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), + if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), + if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), + if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) + else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), + if (snapshot.data!.player!.bio != null) Card( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], + ), + ), + //if (testNews != null && testNews!.news.isNotEmpty) + Expanded( + child: FutureBuilder( + future: widget.newsFuture, + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Card(child: Center(child: CircularProgressIndicator())); + case ConnectionState.done: + if (snapshot.hasData){ + return NewsThingy(snapshot.data!); + }else if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + } + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: Column( + children: [ + SizedBox( + height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, + child: SingleChildScrollView( + child: SlideTransition( + position: _offsetAnimation, + child: switch (rightCard){ + Cards.overview => getOverviewCard(snapshot.data!.summaries!), + Cards.tetraLeague => switch (cardMod){ + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), + CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), + CardMod.records => getRecentTLrecords(widget.constraints), + _ => const Center(child: Text("huh?")) + }, + Cards.quickPlay => switch (cardMod){ + CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), + CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), + CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), + CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), + }, + Cards.sprint => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + Cards.blitz => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + }, + ), + ), + ), + if (modeButtons[rightCard]!.length > 1) SegmentedButton( + showSelectedIcon: false, + selected: {cardMod}, + segments: modeButtons[rightCard]!, + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + //_transition.; + }); + }, + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: Cards.overview, + //label: Text('Overview'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + //label: Text('Tetra League'), + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.quickPlay, + //label: Text('Quick Play'), + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.sprint, + //label: Text('40 Lines'), + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.blitz, + //label: Text('Blitz'), + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ], + selected: {rightCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + cardMod = CardMod.info; + rightCard = newSelection.first; + });}) + ], + ) + ) + ], + ), + ); + } + } + return const Text("End of FutureBuilder"); + }, + ); + } +} diff --git a/lib/views/destination_leaderboards.dart b/lib/views/destination_leaderboards.dart new file mode 100644 index 0000000..431f25e --- /dev/null +++ b/lib/views/destination_leaderboards.dart @@ -0,0 +1,277 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.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/utils/relative_timestamps.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/user_view.dart'; + +class DestinationLeaderboards extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationLeaderboards({super.key, required this.constraints}); + + @override + State createState() => _DestinationLeaderboardsState(); +} + +enum Leaderboards{ + tl, + fullTL, + xp, + ar, + sprint, + blitz, + zenith, + zenithex, +} + +class _DestinationLeaderboardsState extends State { + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + final Map leaderboards = { + Leaderboards.tl: "Tetra League (Current Season)", + Leaderboards.fullTL: "Tetra League (Current Season, full one)", + Leaderboards.xp: "XP", + Leaderboards.ar: "Acievement Points", + Leaderboards.sprint: "40 Lines", + Leaderboards.blitz: "Blitz", + Leaderboards.zenith: "Quick Play", + Leaderboards.zenithex: "Quick Play Expert", + }; + Leaderboards _currentLb = Leaderboards.tl; + final StreamController> _dataStreamController = StreamController>.broadcast(); + late final ScrollController _scrollController; + Stream> get dataStream => _dataStreamController.stream; + List list = []; + bool _isFetchingData = false; + String? prisecter; + List _countries = [for (MapEntry e in t.countries.entries) DropdownMenuEntry(value: e.key, label: e.value)]; + List _stats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuEntry(value: e.key, label: e.value)]; + String? _country; + Stats stat = Stats.tr; + + Future _fetchData() async { + if (_isFetchingData) { + // Avoid fetching new data while already fetching + return; + } + try { + _isFetchingData = true; + setState(() {}); + + final items = switch(_currentLb){ + Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter, country: _country), + Leaderboards.fullTL => (await teto.fetchTLLeaderboard()).getStatRankingFromLB(stat, country: _country??""), + Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp", country: _country), + Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar", country: _country), + Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, country: _country), + Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global", country: _country), + Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global", country: _country), + Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global", country: _country), + }; + + list.addAll(items); + + _dataStreamController.add(list); + prisecter = list.last.prisecter.toString(); + } catch (e) { + _dataStreamController.addError(e); + } finally { + // Set to false when data fetching is complete + _isFetchingData = false; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _fetchData(); + _scrollController.addListener(() { + _scrollController.addListener(() { + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.position.pixels; + + if (currentScroll == maxScroll && _currentLb != Leaderboards.fullTL) { + // When the last item is fully visible, load the next page. + _fetchData(); + } + }); + }); + } + + static TextStyle trailingStyle = TextStyle(fontSize: 28); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 350.0, + height: widget.constraints.maxHeight, + child: Column( + children: [ + const Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Spacer() + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: leaderboards.length, + itemBuilder: (BuildContext context, int index) { + return Card( + surfaceTintColor: index == 1 ? Colors.redAccent : theme.colorScheme.primary, + child: ListTile( + title: Text(leaderboards.values.elementAt(index)), + subtitle: index == 1 ? Text("Heavy, but allows you to sort players by their stats", style: TextStyle(color: Colors.grey, fontSize: 12)) : null, + onTap: () { + _currentLb = leaderboards.keys.elementAt(index); + list.clear(); + prisecter = null; + _fetchData(); + }, + ), + ); + } + ), + ), + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 350 - 88, + child: Card( + child: StreamBuilder>( + stream: dataStream, + builder:(context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + Text(leaderboards[_currentLb]!, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownMenu( + leadingIcon: Icon(Icons.public), + inputDecorationTheme: InputDecorationTheme( + isDense: true, + ), + textStyle: TextStyle(fontSize: 14, height: 0.9), + dropdownMenuEntries: _countries, + initialSelection: "", + onSelected: ((value) { + _country = value as String?; + list.clear(); + prisecter = null; + _isFetchingData = false; + setState((){_fetchData();}); + }) + ), + if (_currentLb == Leaderboards.fullTL) SizedBox(width: 5.0), + if (_currentLb == Leaderboards.fullTL) DropdownMenu( + leadingIcon: Icon(Icons.sort), + inputDecorationTheme: InputDecorationTheme( + isDense: true, + ), + textStyle: TextStyle(fontSize: 14, height: 0.9), + dropdownMenuEntries: _stats, + initialSelection: stat, + onSelected: ((value) { + stat = value; + list.clear(); + prisecter = null; + _isFetchingData = false; + setState((){_fetchData();}); + }) + ) + ], + ), + const Divider(), + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length, + prototypeItem: ListTile( + leading: Text("0"), + title: Text("ehhh...", style: TextStyle(fontSize: 22)), + trailing: SizedBox(height: 36, width: 1), + subtitle: const Text("eh...", style: TextStyle(color: Colors.grey, fontSize: 12)), + ), + itemBuilder: (BuildContext context, int index){ + return ListTile( + leading: Text(intf.format(index+1)), + title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), + trailing: switch (_currentLb){ + Leaderboards.tl => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), + Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) + ], + ), + Leaderboards.fullTL => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), + Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) + ], + ), + Leaderboards.xp => Text("LVL ${f2.format(snapshot.data![index].level)}", style: trailingStyle), + Leaderboards.ar => Text("${intf.format(snapshot.data![index].ar)} AR", style: trailingStyle), + Leaderboards.sprint => Text(get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), style: trailingStyle), + Leaderboards.blitz => Text(intf.format(snapshot.data![index].stats.score), style: trailingStyle), + Leaderboards.zenith => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle), + Leaderboards.zenithex => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle) + }, + subtitle: Text(switch (_currentLb){ + Leaderboards.tl => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", + Leaderboards.fullTL => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", + Leaderboards.xp => "${f2.format(snapshot.data![index].xp)} XP${snapshot.data![index].playtime.isNegative ? "" : ", ${playtime(snapshot.data![index].playtime)} of gametime"}", + Leaderboards.ar => "${snapshot.data![index].ar_counts}", + Leaderboards.sprint => "${intf.format(snapshot.data![index].stats.finesse.faults)} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.kps)} KPS, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${intf.format(snapshot.data![index].stats.piecesPlaced)} P", + Leaderboards.blitz => "lvl ${snapshot.data![index].stats.level}, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${f2.format(snapshot.data![index].stats.spp)} SPP", + Leaderboards.zenith => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B", + Leaderboards.zenithex => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B" + }, style: TextStyle(color: Colors.grey, fontSize: 12)), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UserView(searchFor: snapshot.data![index].userId), + maintainState: false, + ), + ); + }, + ); + } + ), + ), + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/views/destination_saved_data.dart b/lib/views/destination_saved_data.dart new file mode 100644 index 0000000..51936dc --- /dev/null +++ b/lib/views/destination_saved_data.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.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_tiles.dart'; +import 'package:tetra_stats/views/state_view.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class DestinationSavedData extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationSavedData({super.key, required this.constraints}); + + @override + State createState() => _DestinationSavedData(); +} + +class _DestinationSavedData extends State { + String? selectedID; + + Future<(List, List, List)> getDataAbout(String id) async { + return (await teto.getStates(id, season: currentSeason), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id)); + } + + Widget getTetraLeagueListTile(TetraLeague data){ + return ListTile( + title: Text("${timestamp(data.timestamp)}"), + subtitle: Text("${f2.format(data.apm)} APM, ${f2.format(data.pps)} PPS, ${f2.format(data.vs)} VS, ${intf.format(data.gamesPlayed)} games", style: TextStyle(color: Colors.grey)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(data.tr)} TR", style: TextStyle(fontSize: 28)), + Image.asset("res/tetrio_tl_alpha_ranks/${data.rank}.png", height: 36) + ], + ), + leading: IconButton( + onPressed: () { + teto.deleteState(data.id+data.timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(data.timestamp))))); + })); + }, + icon: Icon(Icons.delete_forever) + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StateView(state: data), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: teto.getAllPlayers(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + return Row( + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + const Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Saved Data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Spacer() + ], + ), + ), + for (String id in snapshot.data!.keys) Card( + child: ListTile( + title: Text(snapshot.data![id]!), + //subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)), + onTap: () => setState(() { + selectedID = id; + }), + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: selectedID != null ? FutureBuilder<(List, List, List)>( + future: getDataAbout(selectedID!), + builder: (context, snapshot) { + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + return DefaultTabController( + length: 3, + child: Card( + child: Column( + children: [ + Card( + child: TabBar(tabs: [ + Tab(text: "S${currentSeason} TL States"), + Tab(text: "S1 TL States"), + Tab(text: "TL Records") + ]), + ), + SizedBox( + height: widget.constraints.maxHeight - 64, + child: TabBarView(children: [ + ListView.builder( + itemCount: snapshot.data!.$1.length, + itemBuilder: (context, index) { + return getTetraLeagueListTile(snapshot.data!.$1[index]); + },), + ListView.builder( + itemCount: snapshot.data!.$2.length, + itemBuilder: (context, index) { + return getTetraLeagueListTile(snapshot.data!.$2[index]); + },), + ListView.builder( + itemCount: snapshot.data!.$3.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(snapshot.data!.$3[index].toString()), + ); + },), + ] + ), + ) + ], + ), + ), + ); + } + return Text("what?"); + } + } + ) : + Text("Select nickname on the left to see data assosiated with it") + ) + ], + ); + } + } + return const Text("End of FutureBuilder"); + }, + ); + } +} diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index f918201..3933062 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,19 +1,13 @@ import 'dart:async'; -import 'dart:ui' as ui; -import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; -import 'package:flutter/services.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/badge.dart'; import 'package:tetra_stats/data_objects/beta_record.dart'; -import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; import 'package:tetra_stats/data_objects/distinguishment.dart'; import 'package:tetra_stats/data_objects/est_tr.dart'; import 'package:tetra_stats/data_objects/nerd_stats.dart'; @@ -23,35 +17,31 @@ import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/data_objects/record_extras.dart'; import 'package:tetra_stats/data_objects/record_single.dart'; -import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; import 'package:tetra_stats/data_objects/summaries.dart'; import 'package:tetra_stats/data_objects/tetra_league.dart'; -import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; -import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_player.dart'; -import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; -import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/singleplayer_record_view.dart'; +import 'package:tetra_stats/views/destination_calculator.dart'; +import 'package:tetra_stats/views/destination_cutoffs.dart'; +import 'package:tetra_stats/views/destination_graphs.dart'; +import 'package:tetra_stats/views/destination_home.dart'; +import 'package:tetra_stats/views/destination_leaderboards.dart'; +import 'package:tetra_stats/views/destination_saved_data.dart'; import 'package:tetra_stats/views/tl_match_view.dart'; import 'package:tetra_stats/views/compare_view_tiles.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; -import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/widgets/tl_progress_bar.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:transparent_image/transparent_image.dart'; -import 'package:vector_math/vector_math_64.dart' hide Colors; // TODO: Refactor it @@ -59,6 +49,35 @@ var fDiff = NumberFormat("+#,###.####;-#,###.####"); late Future _data; late Future _newsData; +Future getData(String searchFor) async { + TetrioPlayer player; + try{ + if (searchFor.startsWith("ds:")){ + player = await teto.fetchPlayer(searchFor.substring(3), isItDiscordID: true); // we trying to get him with that + }else{ + player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username + } + }on TetrioPlayerNotExist{ + return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); + } + late Summaries summaries; + late Cutoffs cutoffs; + List requests = await Future.wait([ + teto.fetchSummaries(player.userId), + teto.fetchCutoffsBeanserver(), + ]); + List states = await teto.getStates(player.userId, season: currentSeason); + summaries = requests[0]; + cutoffs = requests[1]; + + bool isTracking = await teto.isPlayerTracking(player.userId); + if (isTracking){ // if tracked - save data to local DB + await teto.storeState(summaries.league); + } + + return FetchResults(true, player, states, summaries, cutoffs, null); + } + class MainView extends StatefulWidget { final String? player; /// The very first view, that user see when he launch this programm. @@ -98,39 +117,10 @@ class _MainState extends State with TickerProviderStateMixin { super.initState(); } - Future _getData() async { - TetrioPlayer player; - try{ - if (_searchFor.startsWith("ds:")){ - player = await teto.fetchPlayer(_searchFor.substring(3), isItDiscordID: true); // we trying to get him with that - }else{ - player = await teto.fetchPlayer(_searchFor); // Otherwise it's probably a user id or username - } - }on TetrioPlayerNotExist{ - return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); - } - late Summaries summaries; - late Cutoffs cutoffs; - List requests = await Future.wait([ - teto.fetchSummaries(player.userId), - teto.fetchCutoffsBeanserver(), - ]); - List states = await teto.getStates(player.userId, season: currentSeason); - summaries = requests[0]; - cutoffs = requests[1]; - - bool isTracking = await teto.isPlayerTracking(player.userId); - if (isTracking){ // if tracked - save data to local DB - await teto.storeState(summaries.league); - } - - return FetchResults(true, player, states, summaries, cutoffs, null); - } - void changePlayer(String player) { setState(() { _searchFor = player; - _data = _getData(); + _data = getData(_searchFor); _newsData = teto.fetchNews(_searchFor); }); } @@ -206,7 +196,7 @@ class _MainState extends State with TickerProviderStateMixin { ), Expanded( child: switch (destination){ - 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), + 0 => DestinationHome(searchFor: _searchFor, constraints: constraints, dataFuture: _data, newsFuture: _newsData), 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), 2 => DestinationLeaderboards(constraints: constraints), 3 => DestinationCutoffs(constraints: constraints), @@ -221,2860 +211,6 @@ class _MainState extends State with TickerProviderStateMixin { } } -class DestinationSavedData extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationSavedData({super.key, required this.constraints}); - - @override - State createState() => _DestinationSavedData(); -} - -class _DestinationSavedData extends State { - String? selectedID; - - Future<(List, List, List)> getDataAbout(String id) async { - return (await teto.getStates(id, season: currentSeason), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id)); - } - - Widget getTetraLeagueListTile(TetraLeague data){ - return ListTile( - title: Text("${timestamp(data.timestamp)}"), - subtitle: Text("${f2.format(data.apm)} APM, ${f2.format(data.pps)} PPS, ${f2.format(data.vs)} VS, ${intf.format(data.gamesPlayed)} games", style: TextStyle(color: Colors.grey)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(data.tr)} TR", style: TextStyle(fontSize: 28)), - Image.asset("res/tetrio_tl_alpha_ranks/${data.rank}.png", height: 36) - ], - ), - leading: IconButton( - onPressed: () { - teto.deleteState(data.id+data.timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(data.timestamp))))); - })); - }, - icon: Icon(Icons.delete_forever) - ), - ); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: teto.getAllPlayers(), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasError){ return FutureError(snapshot); } - if (snapshot.hasData){ - return Row( - children: [ - SizedBox( - width: 450, - child: Column( - children: [ - const Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Text("Saved Data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), - Spacer() - ], - ), - ), - for (String id in snapshot.data!.keys) Card( - child: ListTile( - title: Text(snapshot.data![id]!), - //subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)), - onTap: () => setState(() { - selectedID = id; - }), - ), - ) - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: selectedID != null ? FutureBuilder<(List, List, List)>( - future: getDataAbout(selectedID!), - builder: (context, snapshot) { - switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasError){ return FutureError(snapshot); } - if (snapshot.hasData){ - return DefaultTabController( - length: 3, - child: Card( - child: Column( - children: [ - Card( - child: TabBar(tabs: [ - Tab(text: "S${currentSeason} TL States"), - Tab(text: "S1 TL States"), - Tab(text: "TL Records") - ]), - ), - SizedBox( - height: widget.constraints.maxHeight - 64, - child: TabBarView(children: [ - ListView.builder( - itemCount: snapshot.data!.$1.length, - itemBuilder: (context, index) { - return getTetraLeagueListTile(snapshot.data!.$1[index]); - },), - ListView.builder( - itemCount: snapshot.data!.$2.length, - itemBuilder: (context, index) { - return getTetraLeagueListTile(snapshot.data!.$2[index]); - },), - ListView.builder( - itemCount: snapshot.data!.$3.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(snapshot.data!.$3[index].toString()), - ); - },), - ] - ), - ) - ], - ), - ), - ); - } - return Text("what?"); - } - } - ) : - Text("Select nickname on the left to see data assosiated with it") - ) - ], - ); - } - } - return const Text("End of FutureBuilder"); - }, - ); - } -} - -class DestinationCalculator extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationCalculator({super.key, required this.constraints}); - - @override - State createState() => _DestinationCalculatorState(); -} - -enum CalcCards{ - calc, - damage -} - -class ClearData{ - final String title; - final Lineclears lineclear; - final int lines; - final bool miniSpin; - final bool spin; - bool perfectClear = false; - int id = -1; - - ClearData(this.title, this.lineclear, this.lines, this.miniSpin, this.spin); - - ClearData cloneWith(int i){ - ClearData newOne = ClearData(title, lineclear, lines, miniSpin, spin)..id = i; - return newOne; - } - - bool get difficultClear { - if (lines == 0) return false; - if (lines >= 4 || miniSpin || spin) return true; - else return false; - } - - void togglePC(){ - perfectClear = !perfectClear; - } - - int dealsDamage(int combo, int b2b, int previousB2B, Rules rules){ - if (lines == 0) return 0; - double damage = 0; - - if (spin){ - if (lines <= 5) damage += garbage[lineclear]!; - else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); - } else if (miniSpin){ - damage += garbage[lineclear]!; - } else { - if (lines <= 5) damage += garbage[lineclear]!; - else damage += garbage[Lineclears.PENTA]! + (lines - 5); - } - - if (difficultClear && b2b >= 1 && rules.b2b){ - if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log(1 + (b2b) * BACKTOBACK_BONUS_LOG)).floor() + (b2b == 1 ? 0 : (1 + log(1 +(b2b) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? - else damage += 1; // if b2b chaining off - } - - if (rules.combo && rules.comboTable != ComboTables.none) { - if (combo >= 1){ - if (lines == 1 && rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; - else damage *= (1 + COMBO_BONUS * (combo)); - } - if (combo >= 2) { - damage = max(log(1 + COMBO_MINIFIER * (combo) * COMBO_MINIFIER_LOG), damage); - } - } - - if (!difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1){ - damage += rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b); - } - - if (perfectClear) damage += rules.pcDamage; - - return (damage * rules.multiplier).floor(); - } -} - -Map> clearsExisting = { - "No Spin Clears": [ - ClearData("No lineclear (Break Combo)", Lineclears.ZERO, 0, false, false), - ClearData("Single", Lineclears.SINGLE, 1, false, false), - ClearData("Double", Lineclears.DOUBLE, 2, false, false), - ClearData("Triple", Lineclears.TRIPLE, 3, false, false), - ClearData("Quad", Lineclears.QUAD, 4, false, false) - ], - "Spins": [ - ClearData("Spin Zero", Lineclears.TSPIN, 0, false, true), - ClearData("Spin Single", Lineclears.TSPIN_SINGLE, 1, false, true), - ClearData("Spin Double", Lineclears.TSPIN_DOUBLE, 2, false, true), - ClearData("Spin Triple", Lineclears.TSPIN_TRIPLE, 3, false, true), - ClearData("Spin Quad", Lineclears.TSPIN_QUAD, 4, false, true), - ], - "Mini spins": [ - ClearData("Mini Spin Zero", Lineclears.TSPIN_MINI, 0, true, false), - ClearData("Mini Spin Single", Lineclears.TSPIN_MINI_SINGLE, 1, true, false), - ClearData("Mini Spin Double", Lineclears.TSPIN_MINI_DOUBLE, 2, true, false), - ClearData("Mini Spin Triple", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false), - ] -}; - -class Rules{ - bool combo = true; - bool b2b = true; - bool b2bChaining = false; - bool surge = true; - int surgeInitAmount = 4; - int surgeInitAtB2b = 4; - ComboTables comboTable = ComboTables.multiplier; - int pcDamage = 5; - int pcB2B = 1; - double multiplier = 1.0; -} - -const TextStyle mainToggleInRules = TextStyle(fontSize: 18, fontWeight: ui.FontWeight.w800); - -class _DestinationCalculatorState extends State { - double? apm; - double? pps; - double? vs; - NerdStats? nerdStats; - EstTr? estTr; - Playstyle? playstyle; - TextEditingController ppsController = TextEditingController(); - TextEditingController apmController = TextEditingController(); - TextEditingController vsController = TextEditingController(); - - List clears = []; - Map customClearsChoice = { - "No Spin Clears": 5, - "Spins": 5 - }; - int idCounter = 0; - Rules rules = Rules(); - - CalcCards card = CalcCards.calc; - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - void calc() { - apm = double.tryParse(apmController.text); - pps = double.tryParse(ppsController.text); - vs = double.tryParse(vsController.text); - if (apm != null && pps != null && vs != null) { - 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); - setState(() {}); - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Please, enter valid values"))); - } - } - - // void calcDamage(){ - // for (ClearData lineclear in clears){ - - // } - // } - - Widget getCalculator(){ - return SingleChildScrollView( - child: Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text("Stats Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - )), - ), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), - child: TextField( - onSubmitted: (value) => calc(), - controller: apmController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("APM"), alignLabelWithHint: true, hintText: "Enter your APM"), - ), - ) - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), - child: TextField( - onSubmitted: (value) => calc(), - controller: ppsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("PPS"), alignLabelWithHint: true, hintText: "Enter your PPS"), - ), - ) - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), - child: TextField( - onSubmitted: (value) => calc(), - controller: vsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("VS"), alignLabelWithHint: true, hintText: "Enter your VS"), - ), - ) - ), - TextButton( - onPressed: () => calc(), - child: Text(t.calc), - ), - ], - ), - ), - ), - if (nerdStats != null) Card( - child: NerdStatsThingy(nerdStats: nerdStats!) - ), - if (playstyle != null) Card( - child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!) - ) - ], - ), - ); - } - - Widget getDamageCalculator(){ - List rSideWidgets = []; - List lSideWidgets = []; - - for (var key in clearsExisting.keys){ - rSideWidgets.add(Text(key)); - for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( - child: ListTile( - title: Text(data.title), - subtitle: Text("${data.dealsDamage(0, 0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), - trailing: Icon(Icons.arrow_forward_ios), - onTap: (){ - setState((){ - clears.add(data.cloneWith(idCounter)); - }); - idCounter++; - }, - ), - )); - if (key != "Mini spins") rSideWidgets.add(Card( - child: ListTile( - title: Text("Custom"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 30.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration(hintText: "5"), - onChanged: (value) => customClearsChoice[key] = int.parse(value), - )), - Text(" Lines", style: TextStyle(fontSize: 18)), - Icon(Icons.arrow_forward_ios) - ], - ), - onTap: (){ - setState((){ - clears.add(ClearData("${key == "Spins" ? "Spin " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} Lines)", key == "Spins" ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, customClearsChoice[key]!, false, key == "Spins").cloneWith(idCounter)); - }); - idCounter++; - }, - ), - )); - rSideWidgets.add(const Divider()); - } - - int combo = -1; - int b2b = -1; - int previousB2B = -1; - int totalDamage = 0; - int normalDamage = 0; - int comboDamage = 0; - int b2bDamage = 0; - int surgeDamage = 0; - int pcDamage = 0; - - for (ClearData lineclear in clears){ - previousB2B = b2b; - if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; - if (lineclear.lines > 0) combo++; else combo = -1; - int pcDmg = lineclear.perfectClear ? (rules.pcDamage * rules.multiplier).floor() : 0; - int normalDmg = lineclear.dealsDamage(0, 0, 0, rules) - pcDmg; - int surgeDmg = (!lineclear.difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1) ? rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b) : 0; - int b2bDmg = lineclear.dealsDamage(0, b2b, b2b-1, rules) - normalDmg - pcDmg; - int dmg = lineclear.dealsDamage(combo, b2b, previousB2B, rules); - int comboDmg = dmg - normalDmg - b2bDmg - surgeDmg - pcDmg; - lSideWidgets.add( - ListTile( - key: ValueKey(lineclear.id), - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)), - if (lineclear.lines > 0) IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.local_parking_outlined, color: lineclear.perfectClear ? Colors.white : Colors.grey.shade800)), - ], - ), - title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), - subtitle: lineclear.lines > 0 ? Text("${dmg == normalDmg ? "No bonuses" : ""}${b2bDmg > 0 ? "+${intf.format(b2bDmg)} for B2B" : ""}${(b2bDmg > 0 && comboDmg > 0) ? ", " : ""}${comboDmg > 0 ? "+${intf.format(comboDmg)} for combo" : ""}${(comboDmg > 0 && lineclear.perfectClear) ? ", " : ""}${lineclear.perfectClear ? "+${intf.format(pcDmg)} for PC" : ""}${(surgeDmg > 0 && (lineclear.perfectClear || comboDmg > 0)) ? ", " : ""}${surgeDmg > 0 ? "Surge released: +${intf.format(surgeDmg)}" : ""}", style: TextStyle(color: Colors.grey)) : null, - trailing: lineclear.lines > 0 ? Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), - ) : null, - ) - ); - totalDamage += dmg; - normalDamage += normalDmg; - comboDamage += comboDmg; - b2bDamage += b2bDmg; - surgeDamage += surgeDmg; - pcDamage += pcDmg; - } - // values for "the bar" - double sec2end = normalDamage.toDouble()+comboDamage.toDouble(); - double sec3end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble(); - double sec4end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble(); - double sec5end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble()+pcDamage.toDouble(); - return Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text("Damage Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - )), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 350.0, - child: DefaultTabController(length: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: TabBar(tabs: [ - Tab(text: "Actions"), - Tab(text: "Rules"), - ]), - ), - SizedBox( - height: widget.constraints.maxHeight - 164, - child: TabBarView(children: [ - SingleChildScrollView( - child: Column( - children: rSideWidgets, - ), - ), - SingleChildScrollView( - child: Column( - children: [ - Card( - child: ListTile( - title: Text("Multiplier", style: mainToggleInRules), - trailing: SizedBox(width: 90.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))], - decoration: InputDecoration(hintText: rules.multiplier.toString()), - onChanged: (value) => setState((){rules.multiplier = double.parse(value);}), - )), - ), - ), - Card( - child: Column( - children: [ - ListTile( - title: Text("Combo", style: mainToggleInRules), - trailing: Switch(value: rules.combo, onChanged: (v) => setState((){rules.combo = v;})), - ), - if (rules.combo) ListTile( - title: Text("Combo Table"), - trailing: DropdownButton( - items: [for (var v in ComboTables.values) DropdownMenuItem(value: v.index, child: Text(v.name))], - value: rules.comboTable.index, - onChanged: (v) => setState((){rules.comboTable = ComboTables.values[v!];}), - ), - ) - ], - ), - ), - Card( - child: Column( - children: [ - ListTile( - title: Text("Back-To-Back (B2B)", style: mainToggleInRules), - trailing: Switch(value: rules.b2b, onChanged: (v) => setState((){rules.b2b = v;})), - ), - if (rules.b2b) ListTile( - title: Text("Back-To-Back Chaining"), - trailing: Switch(value: rules.b2bChaining, onChanged: (v) => setState((){rules.b2bChaining = v;})), - ), - ], - ), - ), - Card( - child: Column( - children: [ - ListTile( - title: Text("Surge", style: mainToggleInRules), - trailing: Switch(value: rules.surge, onChanged: (v) => setState((){rules.surge = v;})), - ), - if (rules.surge) ListTile( - title: Text("Starts at B2B"), - trailing: SizedBox(width: 90.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration(hintText: rules.surgeInitAtB2b.toString()), - onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}), - )), - ), - if (rules.surge) ListTile( - title: Text("Start amount"), - trailing: SizedBox(width: 90.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration(hintText: rules.surgeInitAmount.toString()), - onChanged: (value) => setState((){rules.surgeInitAmount = int.parse(value);}), - )), - ), - ], - ), - ) - ], - ), - ) - ]), - ) - ], - ) - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 350 - 80, - height: widget.constraints.maxHeight - 108, - child: clears.isEmpty ? Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), - SizedBox(height: 5.0), - Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center), - ], - )) : - Card( - child: Column( - children: [ - Expanded( - child: ReorderableListView( - onReorder: (oldIndex, newIndex) { - setState((){ - if (oldIndex < newIndex) { - newIndex -= 1; - } - final ClearData item = clears.removeAt(oldIndex); - clears.insert(newIndex, item); - }); - }, - children: lSideWidgets, - ), - ), - Divider(), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 34.0, 0.0), - child: Row( - children: [ - Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), - Spacer(), - Text(intf.format(totalDamage), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text("Lineclears: ${intf.format(normalDamage)}"), - Text("Combo: ${intf.format(comboDamage)}"), - Text("B2B: ${intf.format(b2bDamage)}"), - Text("Surge: ${intf.format(surgeDamage)}"), - Text("PC's: ${intf.format(pcDamage)}") - ], - ), - SfLinearGauge( - minimum: 0, - maximum: totalDamage.toDouble(), - showLabels: false, - showTicks: false, - ranges: [ - LinearGaugeRange( - color: Colors.green, - startValue: 0, - endValue: normalDamage.toDouble(), - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.yellow, - startValue: normalDamage.toDouble(), - endValue: sec2end, - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.blue, - startValue: sec2end, - endValue: sec3end, - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.red, - startValue: sec3end, - endValue: sec4end, - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.orange, - startValue: sec4end, - endValue: sec5end, - position: LinearElementPosition.cross, - ), - ], - ), - ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) - ], - ) - ], - ), - ), - ) - ], - ) - ], - ); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - SizedBox( - height: widget.constraints.maxHeight -32, - child: switch (card){ - CalcCards.calc => getCalculator(), - CalcCards.damage => getDamageCalculator() - } - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: CalcCards.calc, - label: Text('Stats Calculator'), - ), - ButtonSegment( - value: CalcCards.damage, - label: Text('Damage Calculator'), - ), - ], - selected: {card}, - onSelectionChanged: (Set newSelection) { - setState(() { - card = newSelection.first; - });}) - ], - ); - } - -} - -class FetchCutoffsResults{ - late bool success; - CutoffsTetrio? cutoffs; - Exception? exception; - - FetchCutoffsResults(this.success, this.cutoffs, this.exception); -} - -class DestinationCutoffs extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationCutoffs({super.key, required this.constraints}); - - @override - State createState() => _DestinationCutoffsState(); -} - -class _DestinationCutoffsState extends State { - - Future fetch() async { - TetrioPlayerFromLeaderboard top1; - CutoffsTetrio cutoffs; - List requests = await Future.wait([ - teto.fetchCutoffsTetrio(), - teto.fetchTopOneFromTheLeaderboard(), - ]); - cutoffs = requests[0]; - top1 = requests[1]; - cutoffs.data["top1"] = CutoffTetrio( - pos: 1, - percentile: 0.00, - tr: top1.tr, - targetTr: 25000, - apm: top1.apm, - pps: top1.pps, - vs: top1.vs, - count: 1, - countPercentile: 0.0 - ); - return cutoffs; - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: fetch(), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.hasData){ - return SingleChildScrollView( - child: Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text("Tetra League State", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - Text("as of ${timestamp(snapshot.data!.timestamp)}"), - ], - ), - )), - ), - Padding( - padding: const EdgeInsets.only(bottom:4.0), - child: Card( - child: Column( - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Text("Actual"), - ), - Text("Target") - ] - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), - child: SfLinearGauge( - minimum: 0.00000000, - maximum: 25000.0000, - showTicks: false, - showLabels: false, - ranges: [ - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.outside, - startValue: snapshot.data!.data[cutoff]!.tr, - startWidth: 20.0, - endWidth: 20.0, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.tr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr - }, - color: cutoff != "top1" ? rankColors[cutoff] : Colors.grey.shade800, - ), - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.inside, - startValue: snapshot.data!.data[cutoff]!.targetTr, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.targetTr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr - }, - color: cutoff != "top1" ? rankColors[cutoff] : null, - ), - for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[cutoff]!.tr < snapshot.data!.data[cutoff]!.targetTr) LinearGaugeRange( - position: LinearElementPosition.cross, - startValue: snapshot.data!.data[cutoff]!.tr, - endValue: snapshot.data!.data[cutoff]!.targetTr, - color: Colors.green, - ), - for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr)LinearGaugeRange( - position: LinearElementPosition.cross, - startValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr, - endValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr, - color: Colors.red, - ), - ], - markerPointers: [ - for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.tr), style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(0, 35, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0), value: snapshot.data!.data[cutoff]!.tr, position: LinearElementPosition.outside, offset: 20), - for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.targetTr), textAlign: ui.TextAlign.right, style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(-15, 0, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0, transformAlignment: Alignment.topRight), value: snapshot.data!.data[cutoff]!.targetTr, position: LinearElementPosition.inside, offset: 6) - ], - ), - ), - ), - ], - ), - ], - ), - ), - ), - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - border: TableBorder.all(color: Colors.grey.shade900), - columnWidths: const { - 0: FixedColumnWidth(48), - 1: FixedColumnWidth(155), - 2: FixedColumnWidth(140), - 3: FixedColumnWidth(160), - 4: FixedColumnWidth(150), - 5: FixedColumnWidth(90), - 6: FixedColumnWidth(130), - 7: FixedColumnWidth(120), - 8: FixedColumnWidth(125), - 9: FixedColumnWidth(70), - }, - children: [ - TableRow( - children: [ - Text("Rank", textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("Cutoff TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("Target TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("State", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("More info", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - ] - ), - for (String rank in snapshot.data!.data.keys) if (rank != "top1") TableRow( - decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])), - children: [ - Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: RichText( - textAlign: TextAlign.right, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), - children: [ - if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) - else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), - TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), - if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) - else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) - ] - )), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} APP\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} VS/APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: RichText( - textAlign: TextAlign.right, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), - children: [ - TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)), - TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)), - TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null)) - ] - )) - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { - - },), - ), - ] - ) - ], - ) - ] - ), - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return Text("huh?"); - } - ); - } -} - -class DestinationLeaderboards extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationLeaderboards({super.key, required this.constraints}); - - @override - State createState() => _DestinationLeaderboardsState(); -} - -enum Leaderboards{ - tl, - fullTL, - xp, - ar, - sprint, - blitz, - zenith, - zenithex, -} - -class _DestinationLeaderboardsState extends State { - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - final Map leaderboards = { - Leaderboards.tl: "Tetra League (Current Season)", - Leaderboards.fullTL: "Tetra League (Current Season, full one)", - Leaderboards.xp: "XP", - Leaderboards.ar: "Acievement Points", - Leaderboards.sprint: "40 Lines", - Leaderboards.blitz: "Blitz", - Leaderboards.zenith: "Quick Play", - Leaderboards.zenithex: "Quick Play Expert", - }; - Leaderboards _currentLb = Leaderboards.tl; - final StreamController> _dataStreamController = StreamController>.broadcast(); - late final ScrollController _scrollController; - Stream> get dataStream => _dataStreamController.stream; - List list = []; - bool _isFetchingData = false; - String? prisecter; - List _countries = [for (MapEntry e in t.countries.entries) DropdownMenuEntry(value: e.key, label: e.value)]; - List _stats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuEntry(value: e.key, label: e.value)]; - String? _country; - Stats stat = Stats.tr; - - Future _fetchData() async { - if (_isFetchingData) { - // Avoid fetching new data while already fetching - return; - } - try { - _isFetchingData = true; - setState(() {}); - - final items = switch(_currentLb){ - Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter, country: _country), - Leaderboards.fullTL => (await teto.fetchTLLeaderboard()).getStatRankingFromLB(stat, country: _country??""), - Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp", country: _country), - Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar", country: _country), - Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, country: _country), - Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global", country: _country), - Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global", country: _country), - Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global", country: _country), - }; - - list.addAll(items); - - _dataStreamController.add(list); - prisecter = list.last.prisecter.toString(); - } catch (e) { - _dataStreamController.addError(e); - } finally { - // Set to false when data fetching is complete - _isFetchingData = false; - setState(() {}); - } - } - - @override - void initState() { - super.initState(); - _scrollController = ScrollController(); - _fetchData(); - _scrollController.addListener(() { - _scrollController.addListener(() { - final maxScroll = _scrollController.position.maxScrollExtent; - final currentScroll = _scrollController.position.pixels; - - if (currentScroll == maxScroll && _currentLb != Leaderboards.fullTL) { - // When the last item is fully visible, load the next page. - _fetchData(); - } - }); - }); - } - - static TextStyle trailingStyle = TextStyle(fontSize: 28); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - width: 350.0, - height: widget.constraints.maxHeight, - child: Column( - children: [ - const Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), - Spacer() - ], - ), - ), - Expanded( - child: ListView.builder( - itemCount: leaderboards.length, - itemBuilder: (BuildContext context, int index) { - return Card( - surfaceTintColor: index == 1 ? Colors.redAccent : theme.colorScheme.primary, - child: ListTile( - title: Text(leaderboards.values.elementAt(index)), - subtitle: index == 1 ? Text("Heavy, but allows you to sort players by their stats", style: TextStyle(color: Colors.grey, fontSize: 12)) : null, - onTap: () { - _currentLb = leaderboards.keys.elementAt(index); - list.clear(); - prisecter = null; - _fetchData(); - }, - ), - ); - } - ), - ), - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 350 - 88, - child: Card( - child: StreamBuilder>( - stream: dataStream, - builder:(context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.hasData){ - return Column( - children: [ - Text(leaderboards[_currentLb]!, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DropdownMenu( - leadingIcon: Icon(Icons.public), - inputDecorationTheme: InputDecorationTheme( - isDense: true, - ), - textStyle: TextStyle(fontSize: 14, height: 0.9), - dropdownMenuEntries: _countries, - initialSelection: "", - onSelected: ((value) { - _country = value as String?; - list.clear(); - prisecter = null; - _isFetchingData = false; - setState((){_fetchData();}); - }) - ), - if (_currentLb == Leaderboards.fullTL) SizedBox(width: 5.0), - if (_currentLb == Leaderboards.fullTL) DropdownMenu( - leadingIcon: Icon(Icons.sort), - inputDecorationTheme: InputDecorationTheme( - isDense: true, - ), - textStyle: TextStyle(fontSize: 14, height: 0.9), - dropdownMenuEntries: _stats, - initialSelection: stat, - onSelected: ((value) { - stat = value; - list.clear(); - prisecter = null; - _isFetchingData = false; - setState((){_fetchData();}); - }) - ) - ], - ), - const Divider(), - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: list.length, - prototypeItem: ListTile( - leading: Text("0"), - title: Text("ehhh...", style: TextStyle(fontSize: 22)), - trailing: SizedBox(height: 36, width: 1), - subtitle: const Text("eh...", style: TextStyle(color: Colors.grey, fontSize: 12)), - ), - itemBuilder: (BuildContext context, int index){ - return ListTile( - // TODO: make it clickable - leading: Text(intf.format(index+1)), - title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), - trailing: switch (_currentLb){ - Leaderboards.tl => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), - Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) - ], - ), - Leaderboards.fullTL => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), - Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) - ], - ), - Leaderboards.xp => Text("LVL ${f2.format(snapshot.data![index].level)}", style: trailingStyle), - Leaderboards.ar => Text("${intf.format(snapshot.data![index].ar)} AR", style: trailingStyle), - Leaderboards.sprint => Text(get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), style: trailingStyle), - Leaderboards.blitz => Text(intf.format(snapshot.data![index].stats.score), style: trailingStyle), - Leaderboards.zenith => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle), - Leaderboards.zenithex => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle) - }, - subtitle: Text(switch (_currentLb){ - Leaderboards.tl => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", - Leaderboards.fullTL => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", - Leaderboards.xp => "${f2.format(snapshot.data![index].xp)} XP${snapshot.data![index].playtime.isNegative ? "" : ", ${playtime(snapshot.data![index].playtime)} of gametime"}", - Leaderboards.ar => "${snapshot.data![index].ar_counts}", - Leaderboards.sprint => "${intf.format(snapshot.data![index].stats.finesse.faults)} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.kps)} KPS, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${intf.format(snapshot.data![index].stats.piecesPlaced)} P", - Leaderboards.blitz => "lvl ${snapshot.data![index].stats.level}, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${f2.format(snapshot.data![index].stats.spp)} SPP", - Leaderboards.zenith => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B", - Leaderboards.zenithex => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B" - }, style: TextStyle(color: Colors.grey, fontSize: 12)), - ); - } - ), - ), - ], - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return Text("huh?"); - }, - ), - ), - ), - ], - ); - } -} - -class DestinationGraphs extends StatefulWidget{ - final String searchFor; - //final Function setState; - final BoxConstraints constraints; - - const DestinationGraphs({super.key, required this.searchFor, required this.constraints}); - - @override - State createState() => _DestinationGraphsState(); -} - -enum Graph{ - history, - leagueState, - leagueCutoffs -} - -class _DestinationGraphsState extends State { - bool fetchData = false; - bool _gamesPlayedInsteadOfDateAndTime = false; - late ZoomPanBehavior _zoomPanBehavior; - late TooltipBehavior _historyTooltipBehavior; - late TooltipBehavior _tooltipBehavior; - late TooltipBehavior _leagueTooltipBehavior; - String yAxisTitle = ""; - bool _smooth = false; - final List> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; - Graph _graph = Graph.history; - Stats _Ychart = Stats.tr; - Stats _Xchart = Stats.tr; - int _season = currentSeason-1; - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - - @override - void initState(){ - _historyTooltipBehavior = TooltipBehavior( - color: Colors.black, - borderColor: Colors.white, - enable: true, - animationDuration: 0, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "${f4.format(data.stat)} $yAxisTitle", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), - ), - ), - Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) - ], - ), - ); - } - ); - _tooltipBehavior = TooltipBehavior( - color: Colors.black, - borderColor: Colors.white, - enable: true, - animationDuration: 0, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "${data.nickname} (${data.rank.toUpperCase()})", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20), - ), - ), - Text('${f4.format(data.x)} ${chartsShortTitles[_Xchart]}\n${f4.format(data.y)} ${chartsShortTitles[_Ychart]}') - ], - ), - ); - } - ); - _leagueTooltipBehavior = TooltipBehavior( - color: Colors.black, - borderColor: Colors.white, - enable: true, - animationDuration: 0, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "${f4.format(point.y)} $yAxisTitle", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), - ), - ), - Text(timestamp(data.ts)) - ], - ), - ); - } - ); - _zoomPanBehavior = ZoomPanBehavior( - enablePinching: true, - enableSelectionZooming: true, - enableMouseWheelZooming : true, - enablePanning: true, - ); - super.initState(); - } - - Future>>> getHistoryData(bool fetchHistory) async { - if(fetchHistory){ - try{ - var history = await teto.fetchAndsaveTLHistory(widget.searchFor); - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); - }on TetrioHistoryNotExist{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noHistorySaved))); - }on P1nkl0bst3rForbidden { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); - }on P1nkl0bst3rInternalProblem { - 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))); - } - } - - List> states = await Future.wait>([ - teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), - ]); - List>> historyData = []; // [season][metric][spot] - for (int season = 0; season < currentSeason; season++){ - if (states[season].length >= 2){ - Map> statsMap = {}; - for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())]; - historyData.add(statsMap); - }else{ - historyData.add({}); - break; - } - } - fetchData = false; - - return historyData; - } - - Future> getTetraLeagueData(Stats x, Stats y) async { - TetrioPlayersLeaderboard leaderboard = await teto.fetchTLLeaderboard(); - List<_MyScatterSpot> _spots = [ - for (TetrioPlayerFromLeaderboard entry in leaderboard.leaderboard) - _MyScatterSpot( - entry.getStatByEnum(x).toDouble(), - entry.getStatByEnum(y).toDouble(), - entry.userId, - entry.username, - entry.rank, - rankColors[entry.rank]??Colors.white - ) - ]; - return _spots; - } - - Widget getHistoryGraph(){ - return FutureBuilder>>>( - future: getHistoryData(fetchData), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; - yAxisTitle = chartsShortTitles[_Ychart]!; - // TODO: this graph can Krash - return SfCartesianChart( - tooltipBehavior: _historyTooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, - ), - margin: const EdgeInsets.all(0), - series: [ - if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( - enableTooltip: true, - dataSource: selectedGraph, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (selectedGraph.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ) - else StepLineSeries<_HistoryChartSpot, DateTime>( - enableTooltip: true, - dataSource: selectedGraph, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (selectedGraph.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ), - ], - ); - }else{ return FutureError(snapshot); } - } - } - ); - } - - Widget getLeagueState (){ - return FutureBuilder>( - future: getTetraLeagueData(_Xchart, _Ychart), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return SfCartesianChart( - tooltipBehavior: _tooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - //primaryXAxis: CategoryAxis(), - series: [ - ScatterSeries( - enableTooltip: true, - dataSource: snapshot.data, - animationDuration: 0, - pointColorMapper: (data, _) => data.color, - xValueMapper: (data, _) => data.x, - yValueMapper: (data, _) => data.y, - onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: snapshot.data![point.pointIndex!].nickname), maintainState: false)), - ) - ], - ); - }else{ return FutureError(snapshot); } - } - } - ); - } - - Widget getCutoffsHistory(){ - return FutureBuilder>( - future: teto.fetchCutoffsHistory(), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - yAxisTitle = chartsShortTitles[_Ychart]!; - return SfCartesianChart( - tooltipBehavior: _leagueTooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: const DateTimeAxis(), - primaryYAxis: NumericAxis( - // isInversed: true, - maximum: switch (_Ychart){ - Stats.tr => 25000.0, - Stats.gxe => 100.00, - _ => null - }, - ), - margin: const EdgeInsets.all(0), - series: [ - for (String rank in ranks) StepLineSeries( - enableTooltip: true, - dataSource: snapshot.data, - animationDuration: 0, - //opacity: 0.5, - xValueMapper: (Cutoffs data, _) => data.ts, - yValueMapper: (Cutoffs data, _) => switch (_Ychart){ - Stats.glicko => data.glicko[rank], - Stats.gxe => data.gxe[rank], - _ => data.tr[rank] - }, - color: rankColors[rank]! - ) - ], - ); - }else{ return FutureError(snapshot); } - } - } - ); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (_graph == Graph.history) Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], - value: _season, - onChanged: (value) { - setState(() { - _season = value!; - }); - } - ), - ], - ), - if (_graph != Graph.leagueCutoffs) Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: switch (_graph){ - Graph.history => [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], - Graph.leagueState => _yAxis, - Graph.leagueCutoffs => [], - }, - value: _graph == Graph.history ? _gamesPlayedInsteadOfDateAndTime : _Xchart, - onChanged: (value) { - setState(() { - if (_graph == Graph.history) - _gamesPlayedInsteadOfDateAndTime = value! as bool; - else _Xchart = value! as Stats; - }); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: _graph == Graph.leagueCutoffs ? [DropdownMenuItem(value: Stats.tr, child: Text(chartsShortTitles[Stats.tr]!)), DropdownMenuItem(value: Stats.glicko, child: Text(chartsShortTitles[Stats.glicko]!)), DropdownMenuItem(value: Stats.gxe, child: Text(chartsShortTitles[Stats.gxe]!))] : _yAxis, - value: _Ychart, - onChanged: (value) { - setState(() { - _Ychart = value!; - }); - } - ), - ], - ), - if (_graph != Graph.leagueState) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox(value: _smooth, - checkColor: Colors.black, - onChanged: ((value) { - setState(() { - _smooth = value!; - }); - })), - Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - ), - Card( - child: SizedBox( - width: MediaQuery.of(context).size.width - 88, - height: MediaQuery.of(context).size.height - 96, - child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), - child: switch (_graph){ - Graph.history => getHistoryGraph(), - Graph.leagueState => getLeagueState(), - Graph.leagueCutoffs => getCutoffsHistory() - }, - ) - ), - ) - ], - ), - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: Graph.history, - label: Text('Player History')), - ButtonSegment( - value: Graph.leagueState, - label: Text('League State')), - ButtonSegment( - value: Graph.leagueCutoffs, - label: Text('League Cutoffs'), - ), - ], - selected: {_graph}, - onSelectionChanged: (Set newSelection) { - setState(() { - _graph = newSelection.first; - switch (newSelection.first){ - case Graph.leagueCutoffs: - case Graph.history: - _Ychart = Stats.tr; - case Graph.leagueState: - _Ychart = Stats.apm; - } - });}) - ], - ); - } -} - -class _HistoryChartSpot{ - final DateTime timestamp; - final int gamesPlayed; - final String rank; - final double stat; - const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat); -} - -class _MyScatterSpot{ - num x; - num y; - String id; - String nickname; - String rank; - Color color; - _MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); -} - -class DestinationHome extends StatefulWidget{ - final String searchFor; - //final Function setState; - final BoxConstraints constraints; - - const DestinationHome({super.key, required this.searchFor, required this.constraints}); - - @override - State createState() => _DestinationHomeState(); -} - -class FetchResults{ - bool success; - TetrioPlayer? player; - List states; - Summaries? summaries; - Cutoffs? cutoffs; - Exception? exception; - - FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.exception); -} - -class RecordSummary extends StatelessWidget{ - final RecordSingle? record; - final bool hideRank; - final bool? betterThanRankAverage; - final MapEntry? closestAverage; - final bool? betterThanClosestAverage; - final String? rank; - - const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, this.hideRank = false}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (closestAverage != null && record != null) Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96)) - else !hideRank ? Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) : Container(), - if (record != null) Column( - crossAxisAlignment: hideRank ? CrossAxisAlignment.center : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RichText( - textAlign: hideRank ? TextAlign.center : TextAlign.start, - text: TextSpan( - text: switch(record!.gamemode){ - "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), - "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), - "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(record!.stats.zenith!.altitude)} m", - "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", - _ => record!.stats.score.toString() - }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white, height: 0.9), - ), - ), - RichText( - textAlign: hideRank ? TextAlign.center : TextAlign.start, - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ - "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), - "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), - _ => record!.stats.score.toString() - }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ - "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), - "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), - _ => record!.stats.score.toString() - }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )), - if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), - if (record!.rank != -1 && record!.countryRank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), - const TextSpan(text: "\n"), - TextSpan(text: timestamp(record!.timestamp)), - ] - ), - ), - ], - ) else if (hideRank) RichText(text: const TextSpan( - text: "---", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), - ), - ) - ], - ); - } - -} - -class LeagueCard extends StatelessWidget{ - final TetraLeague league; - final bool showSeasonNumber; - - const LeagueCard({super.key, required this.league, this.showSeasonNumber = false}); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (showSeasonNumber) Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("Season ${league.season}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - Spacer(), - Text( - "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey)), - ], - ) - else Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - TLRatingThingy(userID: "", tlData: league, showPositions: true), - const Divider(), - Text("${league.apm != null ? f2.format(league.apm) : "-.--"} APM • ${league.pps != null ? f2.format(league.pps) : "-.--"} PPS • ${league.vs != null ? f2.format(league.vs) : "-.--"} VS • ${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP • ${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ); - } - -} - -class _DestinationHomeState extends State with SingleTickerProviderStateMixin { - Cards rightCard = Cards.overview; - CardMod cardMod = CardMod.info; - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - late Map>> modeButtons; - late MapEntry? closestAverageBlitz; - late bool blitzBetterThanClosestAverage; - late MapEntry? closestAverageSprint; - late bool sprintBetterThanClosestAverage; - late AnimationController _transition; - late final Animation _offsetAnimation; - bool? sprintBetterThanRankAverage; - bool? blitzBetterThanRankAverage; - - Widget getOverviewCard(Summaries summaries){ - return Column( - children: [ - const Card( - child: Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Overview", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - ), - ), - ), - LeagueCard(league: summaries.league), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), - const Divider(), - Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), - const Divider(), - Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.zenith, hideRank: true), - const Divider(), - Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.zenithEx, hideRank: true,), - const Divider(), - Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), - Text("Score ${intf.format(summaries.zen.score)}"), - Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - const Text("f", style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 65, - height: 1.2, - )), - const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text("${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? - f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: const TextStyle( - //shadows: textShadow, - fontFamily: "Eurostile Round Extended", - fontSize: 36, - fontWeight: FontWeight.w500, - color: Colors.white - )), - ) - ], - ), - Row( - children: [ - const Text("Total pieces placed:"), - const Spacer(), - Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"), - ], - ), - Row( - children: [ - const Text(" - Placed with perfect finesse:"), - const Spacer(), - Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"), - ], - ) - ], - ), - ), - ), - ), - ], - ), - if (summaries.achievements.isNotEmpty) Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), - child: Column( - children: [ - if (summaries.achievements.firstWhere((e) => e.k == 16).v != null) Row( - children: [ - const Text("Total height climbed in QP"), - const Spacer(), - Text("${f2.format(summaries.achievements.firstWhere((e) => e.k == 16).v!)} m"), - ], - ), - if (summaries.achievements.firstWhere((e) => e.k == 17).v != null) Row( - children: [ - const Text("KO's in QP"), - const Spacer(), - Text(intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)), - ], - ) - ], - ), - ), - ), - ] - ); - } - - Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, List states){ - TetraLeague? toCompare = states.length >= 2 ? states.elementAtOrNull(states.length-2) : null; - return Column( - children: [ - Card( - //surfaceTintColor: rankColors[data.rank], - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${states.last.timestamp} ${states.last.tr}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - TetraLeagueThingy(league: data, toCompare: toCompare, cutoffs: cutoffs), - if (data.nerdStats != null) Card( - //surfaceTintColor: rankColors[data.rank], - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats), - if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) - ], - ); - } - - Widget getPreviousSeasonsList(Map pastLeague){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Previous Seasons", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - for (var key in pastLeague.keys) Card( - child: LeagueCard(league: pastLeague[key]!, showSeasonNumber: true), - ) - ], - ); - } - - Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ - return Column( - children: [ - const Card( - child: Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - Card( - clipBehavior: Clip.antiAlias, - child: DefaultTabController(length: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const TabBar( - tabs: [ - Tab(text: "Recent"), - Tab(text: "Top"), - ], - ), - SizedBox( - height: 400, - child: TabBarView( - children: [ - FutureBuilder( - future: teto.fetchStream(widget.searchFor, recentStream), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return Column( - children: [ - for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), - leading: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => "40L", - "blitz" => "BLZ", - "5mblast" => "5MB", - "zenith" => "QP", - "zenithex" => "QPE", - String() => "huh", - }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), - "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - String() => "huh", - }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) - ) - ], - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - }, - ), - FutureBuilder( - future: teto.fetchStream(widget.searchFor, topStream), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return Column( - children: [ - for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), - leading: Text( - "#${i+1}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), - "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - String() => "huh", - }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) - ) - ], - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - }, - ), - ] - ), - ) - ], - ), - ) - ), - ], - ); - } - - Widget getRecentTLrecords(BoxConstraints constraints){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - ), - ), - ), - Card( - clipBehavior: Clip.antiAlias, - child: FutureBuilder( - future: teto.fetchTLStream(widget.searchFor), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return SizedBox(height: constraints.maxHeight - 145, child: _TLRecords(userID: widget.searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - }, - ), - ), - ], - ); - } - - Widget getZenithCard(RecordSingle? record){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), - ], - ), - ), - ), - ), - ZenithThingy(zenith: record), - if (record != null) Row( - children: [ - Expanded( - child: Card( - child: Column( - children: [ - FinesseThingy(record.stats.finesse, record.stats.finessePercentage), - LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins, showMoreClears: true), - if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") - ], - ), - ), - ), - Expanded( - child: Card( - child: SizedBox( - width: 300, - height: 318, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - const Text("T", style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 65, - height: 1.2, - )), - const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text(getMoreNormalTime(record.stats.finalTime), style: const TextStyle( - shadows: textShadow, - fontFamily: "Eurostile Round Extended", - fontSize: 36, - fontWeight: FontWeight.w500, - color: Colors.white - )), - ) - ], - ), - SizedBox( - width: 300.0, - child: Table( - columnWidths: const { - 0: FixedColumnWidth(36) - }, - children: [ - const TableRow( - children: [ - Text("Floor"), - Text("Split", textAlign: TextAlign.right), - Text("Total", textAlign: TextAlign.right), - ] - ), - for (int i = 0; i < record.stats.zenith!.splits.length; i++) TableRow( - children: [ - Text((i+1).toString()), - Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]-(i-1 != -1 ? record.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), - Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), - ] - ) - ], - ), - ), - ], - ), - ), - ), - ), - ], - ), - if (record != null) Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - if (record != null) NerdStatsThingy(nerdStats: record.aggregateStats.nerdStats), - if (record != null) GraphsThingy(nerdStats: record.aggregateStats.nerdStats, playstyle: record.aggregateStats.playstyle, apm: record.aggregateStats.apm, pps: record.aggregateStats.pps, vs: record.aggregateStats.vs) - ], - ); - } - - Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ - if (record == null) { - return const Card( - child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), - ); - } - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(switch(record.gamemode){ - "40l" => t.sprint, - "blitz" => t.blitz, - "5mblast" => "5,000,000 Blast", - _ => record.gamemode - }, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)) - ], - ), - ), - ), - ), - Card( - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RichText(text: TextSpan( - text: switch(record.gamemode){ - "40l" => get40lTime(record.stats.finalTime.inMicroseconds), - "blitz" => NumberFormat.decimalPattern().format(record.stats.score), - "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), - _ => record.stats.score.toString() - }, - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), - "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), - _ => record.stats.score.toString() - }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if ((rank == null || rank == "z" || rank == "x+") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), - "blitz" => readableIntDifference(record.stats.score, closestAverage.value), - _ => record.stats.score.toString() - }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage.key.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )), - if (record.rank != -1) TextSpan(text: "№ ${intf.format(record.rank)}", style: TextStyle(color: getColorOfRank(record.rank))), - if (record.rank != -1) const TextSpan(text: " • "), - if (record.countryRank != -1) TextSpan(text: "№ ${intf.format(record.countryRank)} local", style: TextStyle(color: getColorOfRank(record.countryRank))), - if (record.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(record.timestamp)), - ] - ), - ), - ], - ), - ], - ), - Row( - children: [ - Expanded( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => record.stats.piecesPlaced.toString(), - "blitz" => record.stats.level.toString(), - "5mblast" => NumberFormat.decimalPattern().format(record.stats.spp), - _ => "What if " - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " Pieces", - "blitz" => " Level", - "5mblast" => " SPP", - _ => " i wanted to" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" PPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => f2.format(record.stats.kpp), - "blitz" => f2.format(record.stats.spp), - "5mblast" => record.stats.piecesPlaced.toString(), - _ => "but god said" - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " KPP", - "blitz" => " SPP", - "5mblast" => " Pieces", - _ => " no" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - Expanded( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Key presses", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" KPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => " ", - "blitz" => record.stats.piecesPlaced.toString(), - "5mblast" => record.stats.piecesPlaced.toString(), - _ => "but god said" - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " ", - "blitz" => " Pieces", - "5mblast" => " Pieces", - _ => " no" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - ], - ) - ], - ), - ), - Card( - child: Center( - child: Column( - children: [ - FinesseThingy(record.stats.finesse, record.stats.finessePercentage), - LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins), - if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") - ], - ), - ), - ) - ] - ); - } - - @override - initState(){ - modeButtons = { - Cards.overview: [ - const ButtonSegment( - value: CardMod.info, - label: Text('General'), - ), - ], - Cards.tetraLeague: [ - const ButtonSegment( - value: CardMod.info, - label: Text('Standing'), - ), - const ButtonSegment( - value: CardMod.ex, // yeah i misusing my own Enum shut the fuck up - label: Text('Previous Seasons'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Recent Matches'), - ), - ], - Cards.quickPlay: [ - const ButtonSegment( - value: CardMod.info, - label: Text('Normal'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ), - const ButtonSegment( - value: CardMod.ex, - label: Text('Expert'), - ), - const ButtonSegment( - value: CardMod.exRecords, - label: Text('Expert Records'), - ) - ], - Cards.blitz: [ - const ButtonSegment( - value: CardMod.info, - label: Text('PB'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ) - ], - Cards.sprint: [ - const ButtonSegment( - value: CardMod.info, - label: Text('PB'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ) - ] - }; - - _transition = AnimationController(vsync: this, duration: Durations.long4); - - // _transition.addListener((){ - // setState(() { - - // }); - // }); - - _offsetAnimation = Tween( - begin: Offset.zero, - end: const Offset(1.5, 0.0), - ).animate(CurvedAnimation( - parent: _transition, - curve: Curves.elasticIn, - )); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _data, - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasError){ return FutureError(snapshot); } - if (snapshot.hasData){ - if (!snapshot.data!.success) return FetchResultError(snapshot.data!); - blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; - sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; - if (snapshot.data!.summaries!.sprint != null) { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; - } - if (snapshot.data!.summaries!.blitz != null){ - closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); - blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; - } - return TweenAnimationBuilder( - duration: Durations.long4, - tween: Tween(begin: 0, end: 1), - curve: Easing.standard, - builder: (context, value, child) { - return Container( - transform: Matrix4.translationValues(0, 600-value*600, 0), - child: Opacity(opacity: value, child: child), - ); - }, - child: Row( - children: [ - SizedBox( - width: 450, - child: Column( - children: [ - NewUserThingy(player: snapshot.data!.player!, showStateTimestamp: false, setState: setState), - if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), - if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), - if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), - if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) - else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), - if (snapshot.data!.player!.bio != null) Card( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ], - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), - ) - ], - ), - ), - //if (testNews != null && testNews!.news.isNotEmpty) - Expanded( - child: FutureBuilder( - future: _newsData, - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Card(child: Center(child: CircularProgressIndicator())); - case ConnectionState.done: - if (snapshot.hasData){ - return NewsThingy(snapshot.data!); - }else if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - } - ), - ) - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: Column( - children: [ - SizedBox( - height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, - child: SingleChildScrollView( - child: SlideTransition( - position: _offsetAnimation, - child: switch (rightCard){ - Cards.overview => getOverviewCard(snapshot.data!.summaries!), - Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), - CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), - CardMod.records => getRecentTLrecords(widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.quickPlay => switch (cardMod){ - CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), - CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), - CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), - CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), - }, - Cards.sprint => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.blitz => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - }, - ), - ), - ), - if (modeButtons[rightCard]!.length > 1) SegmentedButton( - showSelectedIcon: false, - selected: {cardMod}, - segments: modeButtons[rightCard]!, - onSelectionChanged: (p0) { - setState(() { - cardMod = p0.first; - //_transition.; - }); - }, - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: Cards.overview, - //label: Text('Overview'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Cards.tetraLeague, - //label: Text('Tetra League'), - icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.quickPlay, - //label: Text('Quick Play'), - icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.sprint, - //label: Text('40 Lines'), - icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.blitz, - //label: Text('Blitz'), - icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ], - selected: {rightCard}, - onSelectionChanged: (Set newSelection) { - setState(() { - cardMod = CardMod.info; - rightCard = newSelection.first; - });}) - ], - ) - ) - ], - ), - ); - } - } - return const Text("End of FutureBuilder"); - }, - ); - } -} - class NewsThingy extends StatelessWidget{ final News news; @@ -4256,7 +1392,7 @@ class ZenithThingy extends StatelessWidget{ } -class _TLRecords extends StatelessWidget { +class TLRecords extends StatelessWidget { final String userID; final Function changePlayer; final List data; @@ -4265,7 +1401,7 @@ class _TLRecords extends StatelessWidget { /// 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}); @override Widget build(BuildContext context) { diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart new file mode 100644 index 0000000..5040240 --- /dev/null +++ b/lib/views/user_view.dart @@ -0,0 +1,59 @@ +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/main.dart'; +import 'package:tetra_stats/views/destination_home.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; + +final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); + +class UserView extends StatefulWidget { + final String searchFor; + const UserView({super.key, required this.searchFor}); + + @override + State createState() => UserState(); +} + +late String oldWindowTitle; + +class UserState extends State { + late ScrollController _scrollController; + + @override + void initState() { + _scrollController = ScrollController(); + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ + // windowManager.getTitle().then((value) => oldWindowTitle = value); + // windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}"); + } + super.initState(); + } + + @override + void dispose() { + _scrollController.dispose(); + // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + //final t = Translations.of(context); + return Scaffold( + appBar: AppBar( + title: Text("Search For"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return DestinationHome(searchFor: widget.searchFor, dataFuture: getData(widget.searchFor), newsFuture: teto.fetchNews(widget.searchFor), constraints: constraints); + } + ) + ) + ); + } +} From b1e49ee70d76d0870252a3c6f2a39f2265a0c6fe Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 20 Oct 2024 02:19:10 +0300 Subject: [PATCH 50/86] Settings menu layout + animaion for a button, that doesn't work yet --- lib/services/tetrio_crud.dart | 20 + lib/views/compare_view_tiles.dart | 1 - lib/views/destination_cutoffs.dart | 2 +- lib/views/main_view_tiles.dart | 607 +++++++++++++++++++++++++++-- 4 files changed, 587 insertions(+), 43 deletions(-) diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index efce458..f26c14f 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; +import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; @@ -352,6 +353,25 @@ class TetrioService extends DB { return data; } + + /// Returns three integers, representing size of the database in bytes, amount of TL records in it and amount of TL states in it + Future<(int, int, int)> getDatabaseData() async { + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + String dbPath; + if (kIsWeb) { + dbPath = dbName; + } else { + final docsPath = await getApplicationDocumentsDirectory(); + dbPath = join(docsPath.path, dbName); + } + var dbFile = File(dbPath); + var dbSize = (await dbFile.stat()).size; + var dbTLRecordsQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetraLeagueMatchesTable}`')).first['COUNT(*)']! as int; + var dbTLStatesQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetrioLeagueTable}`')).first['COUNT(*)']! as int; + return (dbSize, dbTLRecordsQuery, dbTLStatesQuery); + } + /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream). /// Throws an exception if fails to retrieve. Future fetchStream(String userID, String stream) async { diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index 5c9992a..6a92aa4 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -748,7 +748,6 @@ class _AddNewColumnCardState extends State with SingleTickerPr super.dispose(); } - @override Widget build(BuildContext context) { return SizedBox( diff --git a/lib/views/destination_cutoffs.dart b/lib/views/destination_cutoffs.dart index 7c014a9..f4c426a 100644 --- a/lib/views/destination_cutoffs.dart +++ b/lib/views/destination_cutoffs.dart @@ -234,7 +234,7 @@ class _DestinationCutoffsState extends State { style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), children: [ if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) - else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), + else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 3933062..e3b2450 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; @@ -24,6 +26,7 @@ import 'package:tetra_stats/data_objects/tetrio_player.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; +import 'package:tetra_stats/utils/filesizes_converter.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; @@ -202,6 +205,7 @@ class _MainState extends State with TickerProviderStateMixin { 3 => DestinationCutoffs(constraints: constraints), 4 => DestinationCalculator(constraints: constraints), 6 => DestinationSavedData(constraints: constraints), + 7 => DestinationSettings(constraints: constraints), _ => Text("Unknown destination $destination") }, ) @@ -211,6 +215,456 @@ class _MainState extends State with TickerProviderStateMixin { } } +class DestinationSettings extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationSettings({super.key, required this.constraints}); + + @override + State createState() => _DestinationSettings(); +} + +enum SettingsCardMod{ + general("General"), + customization("Custonization"), + database("Local database"); + + const SettingsCardMod(this.title); + + final String title; +} + +const TextStyle settingsTitlesStyle = TextStyle(fontSize: 18); +const EdgeInsets descriptionPadding = EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 8.0); + +class _DestinationSettings extends State { + SettingsCardMod mod = SettingsCardMod.general; + List> locales = >[]; + String defaultNickname = "Checking..."; + late bool oskKagariGimmick; + late bool sheetbotRadarGraphs; + late int ratingMode; + late int timestampMode; + late bool showPositions; + late bool showAverages; + late bool updateInBG; + final TextEditingController _playertext = TextEditingController(); + + @override + void initState() { + // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ + // windowManager.getTitle().then((value) => oldWindowTitle = value); + // windowManager.setTitle("Tetra Stats: ${t.settings}"); + // } + _getPreferences(); + super.initState(); + } + + @override + void dispose(){ + // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); + super.dispose(); + } + + void _getPreferences() { + showPositions = prefs.getBool("showPositions") ?? false; + showAverages = prefs.getBool("showAverages") ?? true; + updateInBG = prefs.getBool("updateInBG") ?? false; + oskKagariGimmick = prefs.getBool("oskKagariGimmick") ?? true; + sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")?? false; + ratingMode = prefs.getInt("ratingMode") ?? 0; + timestampMode = prefs.getInt("timestampMode") ?? 0; + _setDefaultNickname(prefs.getString("player")); + } + + Future _setDefaultNickname(String? n) async { + if (n != null) { + try { + defaultNickname = await teto.getNicknameByID(n); + } on TetrioPlayerNotExist { + defaultNickname = n; + } + } else { + defaultNickname = "dan63047"; + } + setState(() {}); + } + + Future _setPlayer(String player) async { + await prefs.setString('player', player); + await _setDefaultNickname(player); + } + + Future _removePlayer() async { + await prefs.remove('player'); + await _setDefaultNickname("6098518e3d5155e6ec429cdc"); + } + + Widget getGeneralSettings(){ + return Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text(SettingsCardMod.general.title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Your account in TETR.IO", style: settingsTitlesStyle), + trailing: SizedBox(width: 150.0, child: TextField( + keyboardType: TextInputType.text, + decoration: InputDecoration(hintText: defaultNickname), + //onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}), + )), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("Stats of that player will be loaded initially right after launching this app. By default it loads my (dan63) stats. To change that, enter your nickname here."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Language", style: settingsTitlesStyle), + trailing: DropdownButton( + items: locales, + value: LocaleSettings.currentLocale, + onChanged: (value){ + LocaleSettings.setLocale(value!); + if(value.languageCode == Platform.localeName.substring(0, 2)){ + prefs.remove('locale'); + }else{ + prefs.setString('locale', value.languageCode); + } + }, + ), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("Tetra Stats was translated on ${locales.length} languages. By default, app will pick your system one or English, if locale of your system isn't avaliable."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Update data in the background", style: settingsTitlesStyle), + trailing: Switch(value: updateInBG, onChanged: (bool value){ + prefs.setBool("updateInBG", value); + setState(() { + updateInBG = value; + }); + }) + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("If on, Tetra Stats will attempt to retrieve new info once cache expires. Usually that happen every 5 minutes"), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Show leaderboard based stats", style: settingsTitlesStyle), + trailing: Switch(value: showAverages, onChanged: (bool value){ + prefs.setBool("showAverages", value); + setState(() { + showAverages = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("If on, Tetra Stats gonnna provide additional metrics, which will allow you to compare yourself with average player on your rank. The way you'll see it — stats will be highlited with corresponding color, hover over them with cursor for more info."), + ) + ], + ), + ), + Card( + surfaceTintColor: Colors.redAccent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Show position on leaderboard by stats", style: settingsTitlesStyle), + trailing: Switch(value: showPositions, onChanged: (bool value){ + prefs.setBool("showPositions", value); + setState(() { + showPositions = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("This can take some time (and traffic) to load, but will allow you to see your position on the leaderboard, sorted by a stat"), + ) + ], + ), + ) + ] + ); + } + + Widget getCustomizationSettings(){ + return Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text(SettingsCardMod.customization.title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Accent color", style: settingsTitlesStyle), + trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("That color is seen across this app and usually highlites interactive UI elements."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text("Timestamps format", style: settingsTitlesStyle), + trailing: DropdownButton( + value: timestampMode, + items: [ + DropdownMenuItem(value: 0, child: Text(t.timestampsAbsoluteGMT)), + DropdownMenuItem(value: 1, child: Text(t.timestampsAbsoluteLocalTime)), + DropdownMenuItem(value: 2, child: Text(t.timestampsRelative)) + ], + onChanged: (dynamic value){ + prefs.setInt("timestampMode", value); + setState(() { + timestampMode = value; + }); + }, + ), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("You can choose, in which way timestamps shows time. By default, they show time in GMT timezone, formatted according to chosen locale, example: ${DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19))}."), + ), + Padding( + padding: descriptionPadding, + child: Text("There is also:\n• Locale formatted in your timezone: ${DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19).toLocal())}\n• Relative timestamp: ${relativeDateTime(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19))}"), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Sheetbot-like behavior for radar graphs", style: settingsTitlesStyle), + trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){ + prefs.setBool("sheetbotRadarGraphs", value); + setState(() { + sheetbotRadarGraphs = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("Altough it was considered by me, that the way graphs work in SheetBot is not very correct, some people were confused to see, that -0.5 stride dosen't look the way it looks on SheetBot graph. Hence, he we are: if this toggle is on, points on the graphs can appear on the opposite half of the graph if value is negative."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Osk-Kagari gimmick", style: settingsTitlesStyle), + trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){ + prefs.setBool("oskKagariGimmick", value); + setState(() { + oskKagariGimmick = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("If on, instead of osk's rank, :kagari: will be rendered."), + ) + ], + ), + ) + ], + ); + } + + Widget getDatabaseSettings(){ + return Column( + children: [ + Card( + child: Center(child: Column( + children: [ + Text(SettingsCardMod.database.title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Divider(), + FutureBuilder<(int, int, int)>(future: teto.getDatabaseData(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return RichText( + text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white), + children: [ + TextSpan(text: "${bytesToSize(snapshot.data!.$1)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + TextSpan(text: "of data stored\n"), + TextSpan(text: "${intf.format(snapshot.data!.$2)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + TextSpan(text: "Tetra League records saved\n"), + TextSpan(text: "${intf.format(snapshot.data!.$3)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + TextSpan(text: "Tetra League playerstates saved"), + ] + ) + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + } + ), + Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: (){teto.removeDuplicatesFromTLMatches().then((_) => setState((){}));}, + icon: const Icon(Icons.build), + label: Text("Fix"), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0))))) + ) + ), + Expanded( + child: ElevatedButton.icon( + onPressed: (){teto.compressDB().then((_) => setState((){}));}, + icon: const Icon(Icons.compress), + label: Text("Compress"), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))) + ) + ) + ], + ) + ], + )), + ), + Card( + child: ListTile( + title: Text("Export Database", style: settingsTitlesStyle), + ), + ), + Card( + child: ListTile( + title: Text("Import Database", style: settingsTitlesStyle), + ), + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + if (locales.isEmpty) for (var v in AppLocale.values){ + locales.add(DropdownMenuItem( + value: v, child: Text(t.locales[v.languageTag]!))); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + const Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Settings", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Spacer() + ], + ), + ), + for (SettingsCardMod m in SettingsCardMod.values) Card( + child: ListTile( + title: Text(m.title), + trailing: Icon(Icons.arrow_right, color: mod == m ? Colors.white : Colors.grey), + onTap: () { + setState(() { + mod = m; + }); + }, + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: SingleChildScrollView( + child: switch (mod){ + SettingsCardMod.general => getGeneralSettings(), + SettingsCardMod.customization => getCustomizationSettings(), + SettingsCardMod.database => getDatabaseSettings(), + }, + ) + ) + ], + ); + } +} + class NewsThingy extends StatelessWidget{ final News news; @@ -638,13 +1092,44 @@ class BadgesThingy extends StatelessWidget{ } } -class NewUserThingy extends StatelessWidget { +class NewUserThingy extends StatefulWidget { final TetrioPlayer player; final bool showStateTimestamp; final Function setState; const NewUserThingy({super.key, required this.player, required this.showStateTimestamp, required this.setState}); + @override + State createState() => _NewUserThingyState(); +} + +class _NewUserThingyState extends State with SingleTickerProviderStateMixin { + late AnimationController _addToTrackAnimController; + late Animation _addToTrackAnim; + + @override + void initState(){ + _addToTrackAnimController = AnimationController( + duration: Durations.medium3, + vsync: this, + ); + _addToTrackAnim = new Tween( + begin: 0.0, + end: 1.0, + ).animate(new CurvedAnimation( + parent: _addToTrackAnimController, + curve: Easing.standardDecelerate, + reverseCurve: Easing.standardAccelerate + )); + super.initState(); + } + + @override + void dispose() { + _addToTrackAnimController.dispose(); + super.dispose(); + } + Color roleColor(String role){ switch (role){ case "sysop": @@ -677,7 +1162,7 @@ class NewUserThingy extends StatelessWidget { double pfpHeight = 128; int xpTableID = 0; - while (player.xp > xpTableScuffed.values.toList()[xpTableID]) { + while (widget.player.xp > xpTableScuffed.values.toList()[xpTableID]) { xpTableID++; } @@ -689,41 +1174,41 @@ class NewUserThingy extends StatelessWidget { padding: const EdgeInsets.only(bottom: 4.0), child: Container( constraints: const BoxConstraints(maxWidth: 960), - height: player.bannerRevision != null ? 218.0 : 138.0, + height: widget.player.bannerRevision != null ? 218.0 : 138.0, child: Stack( //clipBehavior: Clip.none, children: [ // TODO: osk banner can cause memory leak - if (player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", + if (widget.player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${widget.player.userId}&rv=${widget.player.bannerRevision}" : "https://tetr.io/user-content/banners/${widget.player.userId}.jpg?rv=${widget.player.bannerRevision}", placeholder: kTransparentImage, fit: BoxFit.cover, height: 120, fadeInCurve: Easing.standard, fadeInDuration: Durations.long4 ), Positioned( - top: player.bannerRevision != null ? 90.0 : 10.0, + top: widget.player.bannerRevision != null ? 90.0 : 10.0, left: 16.0, child: ClipRRect( borderRadius: BorderRadius.circular(1000), - child: player.role == "banned" + child: widget.player.role == "banned" ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) - : player.avatarRevision != null - ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + : widget.player.avatarRevision != null + ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${widget.player.userId}&rv=${widget.player.avatarRevision}" : "https://tetr.io/user-content/avatars/${widget.player.userId}.jpg?rv=${widget.player.avatarRevision}", fit: BoxFit.fitHeight, height: 128, placeholder: kTransparentImage, fadeInCurve: Easing.emphasizedDecelerate, fadeInDuration: Durations.long4) : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight), ) ), Positioned( - top: player.bannerRevision != null ? 120.0 : 40.0, + top: widget.player.bannerRevision != null ? 120.0 : 40.0, left: 160.0, child: Tooltip( - message: "${player.userId}\n(Click to copy user ID)", - child: RichText(text: TextSpan(text: player.username, style: TextStyle( - fontFamily: fontStyle(player.username.length), + message: "${widget.player.userId}\n(Click to copy user ID)", + child: RichText(text: TextSpan(text: widget.player.username, style: TextStyle( + fontFamily: fontStyle(widget.player.username.length), fontSize: 28, ), recognizer: TapGestureRecognizer()..onTap = (){ - copyToClipboard(player.userId); + copyToClipboard(widget.player.userId); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); } ) @@ -731,23 +1216,23 @@ class NewUserThingy extends StatelessWidget { ), ), Positioned( - top: player.bannerRevision != null ? 160.0 : 80.0, + top: widget.player.bannerRevision != null ? 160.0 : 80.0, left: 160.0, child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 4.0), - child: Chip(label: Text(player.role.toUpperCase(), style: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(player.role))), + child: Chip(label: Text(widget.player.role.toUpperCase(), style: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(widget.player.role))), ), RichText( text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - if (player.friendCount > 0) const WidgetSpan(child: Icon(Icons.person), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - if (player.friendCount > 0) TextSpan(text: "${intf.format(player.friendCount)} "), - 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)), + if (widget.player.friendCount > 0) const WidgetSpan(child: Icon(Icons.person), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (widget.player.friendCount > 0) TextSpan(text: "${intf.format(widget.player.friendCount)} "), + if (widget.player.supporterTier > 0) WidgetSpan(child: Icon(widget.player.supporterTier > 1 ? Icons.star : Icons.star_border, color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (widget.player.supporterTier > 0) TextSpan(text: widget.player.supporterTier.toString(), style: TextStyle(color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)), ] ) ) @@ -755,7 +1240,7 @@ class NewUserThingy extends StatelessWidget { ), ), Positioned( - top: player.bannerRevision != null ? 193.0 : 113.0, + top: widget.player.bannerRevision != null ? 193.0 : 113.0, left: 160.0, child: SizedBox( width: 270, @@ -763,30 +1248,30 @@ class NewUserThingy extends StatelessWidget { text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - if (player.country != null) TextSpan(text: "${t.countries[player.country]} • "), - TextSpan(text: timestamp(player.registrationTime), style: const TextStyle(color: Colors.grey)) + if (widget.player.country != null) TextSpan(text: "${t.countries[widget.player.country]} • "), + TextSpan(text: timestamp(widget.player.registrationTime), style: const TextStyle(color: Colors.grey)) ] ) ), ) ), Positioned( - top: player.bannerRevision != null ? 126.0 : 46.0, + top: widget.player.bannerRevision != null ? 126.0 : 46.0, right: 16.0, child: RichText( textAlign: TextAlign.end, text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - TextSpan(text: "Level ${(player.level.isNegative || player.level.isNaN) ? "---" : intf.format(player.level.floor())}", style: TextStyle(decoration: (player.level.isNegative || player.level.isNaN) ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: (player.level.isNegative || player.level.isNaN) ? Colors.grey : Colors.white), recognizer: (player.level.isNegative || player.level.isNaN) ? null : TapGestureRecognizer()?..onTap = (){ + TextSpan(text: "Level ${(widget.player.level.isNegative || widget.player.level.isNaN) ? "---" : intf.format(widget.player.level.floor())}", style: TextStyle(decoration: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: (widget.player.level.isNegative || widget.player.level.isNaN) ? Colors.grey : Colors.white), recognizer: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TapGestureRecognizer()?..onTap = (){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( - title: Text("Level ${intf.format(player.level.floor())}", textAlign: TextAlign.center), + title: Text("Level ${intf.format(widget.player.level.floor())}", textAlign: TextAlign.center), content: SingleChildScrollView( child: ListBody(children: [ Text( - "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(player.xp)} XP", + "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(widget.player.xp)} XP", style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold) ), Padding( @@ -796,15 +1281,15 @@ class NewUserThingy extends StatelessWidget { maximum: 1, interval: 1, ranges: [ - LinearGaugeRange(startValue: 0, endValue: player.level - player.level.floor(), color: Colors.cyanAccent), - LinearGaugeRange(startValue: 0, endValue: (player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) + LinearGaugeRange(startValue: 0, endValue: widget.player.level - widget.player.level.floor(), color: Colors.cyanAccent), + LinearGaugeRange(startValue: 0, endValue: (widget.player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) ], showTicks: true, showLabels: false ), ), - Text("${t.statCellNum.xpProgress}: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"), - Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - player.xp)} ${t.statCellNum.xpLeft})") + Text("${t.statCellNum.xpProgress}: ${((widget.player.level - widget.player.level.floor()) * 100).toStringAsFixed(2)} %"), + Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((widget.player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - widget.player.xp)} ${t.statCellNum.xpLeft})") ] ), ), @@ -818,7 +1303,7 @@ class NewUserThingy extends StatelessWidget { ); }), const TextSpan(text:"\n"), - TextSpan(text: player.gameTime.isNegative ? "-h --m" : playtime(player.gameTime), style: TextStyle(color: player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ + TextSpan(text: widget.player.gameTime.isNegative ? "-h --m" : playtime(widget.player.gameTime), style: TextStyle(color: widget.player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: widget.player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !widget.player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( @@ -826,17 +1311,17 @@ class NewUserThingy extends StatelessWidget { content: SingleChildScrollView( child: ListBody(children: [ Text( - "${intf.format(player.gameTime.inDays)}d ${nonsecs.format(player.gameTime.inHours%24)}h ${nonsecs.format(player.gameTime.inMinutes%60)}m ${nonsecs.format(player.gameTime.inSeconds%60)}s ${nonsecs3.format(player.gameTime.inMilliseconds%1000)}ms ${nonsecs3.format(player.gameTime.inMicroseconds%1000)}μs", + "${intf.format(widget.player.gameTime.inDays)}d ${nonsecs.format(widget.player.gameTime.inHours%24)}h ${nonsecs.format(widget.player.gameTime.inMinutes%60)}m ${nonsecs.format(widget.player.gameTime.inSeconds%60)}s ${nonsecs3.format(widget.player.gameTime.inMilliseconds%1000)}ms ${nonsecs3.format(widget.player.gameTime.inMicroseconds%1000)}μs", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24) ), Padding( padding: const EdgeInsets.only(top: 8.0), - child: Text("It's ${f4.format(player.gameTime.inSeconds/31536000)} years,"), + child: Text("It's ${f4.format(widget.player.gameTime.inSeconds/31536000)} years,"), ), - Text("${f4.format(player.gameTime.inSeconds/2628000)} monts,"), - Text("${f4.format(player.gameTime.inSeconds/3600)} hours,"), - Text("${f2.format(player.gameTime.inMilliseconds/60000)} minutes,"), - Text("${intf.format(player.gameTime.inSeconds)} seconds"), + Text("${f4.format(widget.player.gameTime.inSeconds/2628000)} monts,"), + Text("${f4.format(widget.player.gameTime.inSeconds/3600)} hours,"), + Text("${f2.format(widget.player.gameTime.inMilliseconds/60000)} minutes,"), + Text("${intf.format(widget.player.gameTime.inSeconds)} seconds"), ] ), ), @@ -850,8 +1335,8 @@ class NewUserThingy extends StatelessWidget { ); }) : null), const TextSpan(text:"\n"), - TextSpan(text: player.gamesWon > -1 ? intf.format(player.gamesWon) : "---", style: TextStyle(color: player.gamesWon > -1 ? Colors.white : Colors.grey)), - TextSpan(text: "/${player.gamesPlayed > -1 ? intf.format(player.gamesPlayed) : "---"}", style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), + TextSpan(text: widget.player.gamesWon > -1 ? intf.format(widget.player.gamesWon) : "---", style: TextStyle(color: widget.player.gamesWon > -1 ? Colors.white : Colors.grey)), + TextSpan(text: "/${widget.player.gamesPlayed > -1 ? intf.format(widget.player.gamesPlayed) : "---"}", style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), ] ) ) @@ -863,12 +1348,52 @@ class NewUserThingy extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.person_add), label: Text(t.track), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0))))))), + Expanded( + child: AnimatedBuilder( + animation: _addToTrackAnim, + builder: (context, child) { + double firstButtonPosition = 0-(_addToTrackAnim.value as double)*25; + double secondButtonPosition = 25-(_addToTrackAnim.value as double)*25; + double firstButtonOpacity = 1-(_addToTrackAnim.value as double)*2; + double secondButtonOpacity = _addToTrackAnim.value*2-1; + return ElevatedButton.icon( + onPressed: (){ + _addToTrackAnim.isCompleted ? _addToTrackAnimController.reverse() : _addToTrackAnimController.forward(); + }, + icon: _addToTrackAnim.value < 0.5 ? Container( + transform: Matrix4.translationValues(0, firstButtonPosition, 0), + child: Opacity( + opacity: firstButtonOpacity, + child: const Icon(Icons.person_add) + ) + ) : Container( + transform: Matrix4.translationValues(0, secondButtonPosition, 0), + child: Opacity( + opacity: secondButtonOpacity, + child: const Icon(Icons.person_remove) + ) + ), + label: _addToTrackAnim.value < 0.5 ? Container( + transform: Matrix4.translationValues(0, firstButtonPosition, 0), + child: Opacity( + opacity: firstButtonOpacity, + child: Text(t.track) + ) + ) : Container( + transform: Matrix4.translationValues(0, secondButtonPosition, 0), + child: Opacity( + opacity: secondButtonOpacity, + child: Text(t.stopTracking) + ) + ), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0)))))); + }, + )), Expanded( child: ElevatedButton.icon( onPressed: (){ Navigator.push(context, MaterialPageRoute( - builder: (context) => CompareView(player), + builder: (context) => CompareView(widget.player), ), ); }, From 6a615f82349d7f7619eb5314c47e69e7287d61b4 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 20 Oct 2024 20:03:15 +0300 Subject: [PATCH 51/86] Animation was updated, button does work now --- lib/views/destination_home.dart | 5 ++-- lib/views/main_view_tiles.dart | 47 ++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 97aa9d5..9fea795 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -44,9 +44,10 @@ class FetchResults{ List states; Summaries? summaries; Cutoffs? cutoffs; + bool isTracked; Exception? exception; - FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.exception); + FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.isTracked, this.exception); } class RecordSummary extends StatelessWidget{ @@ -1006,7 +1007,7 @@ class _DestinationHomeState extends State with SingleTickerProv width: 450, child: Column( children: [ - NewUserThingy(player: snapshot.data!.player!, showStateTimestamp: false, setState: setState), + NewUserThingy(player: snapshot.data!.player!, initIsTracking: snapshot.data!.isTracked, showStateTimestamp: false, setState: setState), if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index e3b2450..c498194 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; @@ -45,6 +46,7 @@ import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/widgets/tl_progress_bar.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:transparent_image/transparent_image.dart'; +import 'package:vector_math/vector_math_64.dart' hide Colors; // TODO: Refactor it @@ -61,7 +63,7 @@ Future getData(String searchFor) async { player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username } }on TetrioPlayerNotExist{ - return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); + return FetchResults(false, null, [], null, null, false, TetrioPlayerNotExist()); } late Summaries summaries; late Cutoffs cutoffs; @@ -78,7 +80,7 @@ Future getData(String searchFor) async { await teto.storeState(summaries.league); } - return FetchResults(true, player, states, summaries, cutoffs, null); + return FetchResults(true, player, states, summaries, cutoffs, isTracking, null); } class MainView extends StatefulWidget { @@ -1095,9 +1097,10 @@ class BadgesThingy extends StatelessWidget{ class NewUserThingy extends StatefulWidget { final TetrioPlayer player; final bool showStateTimestamp; + final bool initIsTracking; final Function setState; - const NewUserThingy({super.key, required this.player, required this.showStateTimestamp, required this.setState}); + const NewUserThingy({super.key, required this.player, required this.initIsTracking, required this.showStateTimestamp, required this.setState}); @override State createState() => _NewUserThingyState(); @@ -1110,7 +1113,8 @@ class _NewUserThingyState extends State with SingleTickerProvider @override void initState(){ _addToTrackAnimController = AnimationController( - duration: Durations.medium3, + value: widget.initIsTracking ? 1.0 : 0.0, + duration: Durations.extralong4, vsync: this, ); _addToTrackAnim = new Tween( @@ -1118,9 +1122,10 @@ class _NewUserThingyState extends State with SingleTickerProvider end: 1.0, ).animate(new CurvedAnimation( parent: _addToTrackAnimController, - curve: Easing.standardDecelerate, - reverseCurve: Easing.standardAccelerate + curve: Cubic(.15,-0.40,.86,-0.39), + reverseCurve: Cubic(0,.99,.99,1.01) )); + super.initState(); } @@ -1352,38 +1357,42 @@ class _NewUserThingyState extends State with SingleTickerProvider child: AnimatedBuilder( animation: _addToTrackAnim, builder: (context, child) { - double firstButtonPosition = 0-(_addToTrackAnim.value as double)*25; - double secondButtonPosition = 25-(_addToTrackAnim.value as double)*25; + double firstButtonPosition = 0+(_addToTrackAnim.value as double)*25; + double secondButtonPosition = -25+(_addToTrackAnim.value as double)*25; double firstButtonOpacity = 1-(_addToTrackAnim.value as double)*2; double secondButtonOpacity = _addToTrackAnim.value*2-1; return ElevatedButton.icon( onPressed: (){ + _addToTrackAnimController.value == 1 ? teto.deletePlayerToTrack(widget.player.userId) : teto.addPlayerToTrack(widget.player); _addToTrackAnim.isCompleted ? _addToTrackAnimController.reverse() : _addToTrackAnimController.forward(); }, - icon: _addToTrackAnim.value < 0.5 ? Container( - transform: Matrix4.translationValues(0, firstButtonPosition, 0), - child: Opacity( - opacity: firstButtonOpacity, - child: const Icon(Icons.person_add) - ) + icon: _addToTrackAnim.value < 0.5 ? Opacity( + opacity: min(1, firstButtonOpacity), + child: Transform.translate( + offset: Offset(0, _addToTrackAnim.status == AnimationStatus.forward ? firstButtonPosition*4 : firstButtonPosition), + child: Transform.rotate( + angle:_addToTrackAnim.status == AnimationStatus.forward ? (_addToTrackAnim.value as double)*2 : 0, + child: const Icon(Icons.person_add), + ), + ), ) : Container( transform: Matrix4.translationValues(0, secondButtonPosition, 0), child: Opacity( - opacity: secondButtonOpacity, + opacity: max(0, secondButtonOpacity), child: const Icon(Icons.person_remove) ) ), label: _addToTrackAnim.value < 0.5 ? Container( transform: Matrix4.translationValues(0, firstButtonPosition, 0), child: Opacity( - opacity: firstButtonOpacity, - child: Text(t.track) + opacity: min(1, firstButtonOpacity), + child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.forward ? "Done!" : "Track") ) ) : Container( transform: Matrix4.translationValues(0, secondButtonPosition, 0), child: Opacity( - opacity: secondButtonOpacity, - child: Text(t.stopTracking) + opacity: max(0, secondButtonOpacity), + child: Text("Stop tracking") ) ), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0)))))); From bf87f3a8e5d199aedd1407b66532113a2b6906f4 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 21 Oct 2024 02:05:23 +0300 Subject: [PATCH 52/86] History graph fix --- lib/views/destination_graphs.dart | 15 +++++++-------- lib/views/destination_home.dart | 2 +- lib/views/main_view_tiles.dart | 17 +++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/views/destination_graphs.dart b/lib/views/destination_graphs.dart index 6073bac..422ca6d 100644 --- a/lib/views/destination_graphs.dart +++ b/lib/views/destination_graphs.dart @@ -10,6 +10,7 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/views/destination_home.dart'; import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; @@ -132,7 +133,7 @@ class _DestinationGraphsState extends State { super.initState(); } - Future>>> getHistoryData(bool fetchHistory) async { + Future>>> getHistoryData(bool fetchHistory) async { if(fetchHistory){ try{ var history = await teto.fetchAndsaveTLHistory(widget.searchFor); @@ -151,15 +152,12 @@ class _DestinationGraphsState extends State { List> states = await Future.wait>([ teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), ]); - List>> historyData = []; // [season][metric][spot] + Map>> historyData = {}; // [season][metric][spot] for (int season = 0; season < currentSeason; season++){ if (states[season].length >= 2){ Map> statsMap = {}; for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())]; - historyData.add(statsMap); - }else{ - historyData.add({}); - break; + historyData[season] = statsMap; } } fetchData = false; @@ -184,7 +182,7 @@ class _DestinationGraphsState extends State { } Widget getHistoryGraph(){ - return FutureBuilder>>>( + return FutureBuilder>>>( future: getHistoryData(fetchData), builder: (context, snapshot) { switch (snapshot.connectionState){ @@ -194,7 +192,8 @@ class _DestinationGraphsState extends State { return const Center(child: CircularProgressIndicator()); case ConnectionState.done: if (snapshot.hasData){ - List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; + if (snapshot.data!.isEmpty || !snapshot.data!.containsKey(_season)) return ErrorThingy(eText: "Not enough data"); + List<_HistoryChartSpot> selectedGraph = snapshot.data![_season]![_Ychart]!; yAxisTitle = chartsShortTitles[_Ychart]!; // TODO: this graph can Krash return SfCartesianChart( diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 9fea795..5c0e183 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -980,7 +980,7 @@ class _DestinationHomeState extends State with SingleTickerProv case ConnectionState.done: if (snapshot.hasError){ return FutureError(snapshot); } if (snapshot.hasData){ - if (!snapshot.data!.success) return FetchResultError(snapshot.data!); + if (!snapshot.data!.success) return ErrorThingy(data: snapshot.data!); blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; if (snapshot.data!.summaries!.sprint != null) { diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index c498194..2316ff5 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1385,7 +1385,7 @@ class _NewUserThingyState extends State with SingleTickerProvider label: _addToTrackAnim.value < 0.5 ? Container( transform: Matrix4.translationValues(0, firstButtonPosition, 0), child: Opacity( - opacity: min(1, firstButtonOpacity), + opacity: max(min(1, firstButtonOpacity), 0), child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.forward ? "Done!" : "Track") ) ) : Container( @@ -2169,17 +2169,18 @@ class FutureError extends StatelessWidget{ } } -class FetchResultError extends StatelessWidget{ - final FetchResults data; +class ErrorThingy extends StatelessWidget{ + final FetchResults? data; + final String? eText; - FetchResultError(this.data); + ErrorThingy({this.data, this.eText}); @override Widget build(BuildContext context) { IconData icon = Icons.error_outline; - String errText = ""; + String errText = eText??""; String? subText; - switch (data.exception.runtimeType){ + if (data?.exception != null) switch (data!.exception!.runtimeType){ case TetrioPlayerNotExist: icon = Icons.search_off; errText = t.errors.noSuchUser; @@ -2190,7 +2191,7 @@ class FetchResultError extends StatelessWidget{ errText = t.errors.discordNotAssigned; subText = t.errors.discordNotAssignedSub; case ConnectionIssue: - var err = data.exception as ConnectionIssue; + var err = data!.exception as ConnectionIssue; errText = t.errors.connection(code: err.code, message: err.message); break; case TetrioForbidden: @@ -2214,7 +2215,7 @@ class FetchResultError extends StatelessWidget{ errText = t.errors.clientException; break; default: - errText = data.exception.toString(); + errText = data!.exception.toString(); } return TweenAnimationBuilder( duration: Durations.medium3, From c12d4508844ba70dec85856b94c898e85ebd8d15 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 22 Oct 2024 01:38:41 +0300 Subject: [PATCH 53/86] Some `TextStyle`s was moved into themes, button fully animated Also some bugfix and plans on animating Default Nickname setting --- lib/main.dart | 7 ++ lib/views/destination_calculator.dart | 6 +- lib/views/destination_cutoffs.dart | 2 +- lib/views/destination_home.dart | 38 ++++++---- lib/views/destination_leaderboards.dart | 6 +- lib/views/destination_saved_data.dart | 4 +- lib/views/main_view.dart | 12 +-- lib/views/main_view_tiles.dart | 98 ++++++++++++++++--------- lib/views/user_view.dart | 31 ++++++-- lib/widgets/recent_sp_games.dart | 2 +- lib/widgets/singleplayer_record.dart | 2 +- 11 files changed, 134 insertions(+), 74 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c0b11f3..9ff7a57 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,6 +32,12 @@ ThemeData theme = ThemeData( surface: Color.fromARGB(255, 10, 10, 10), secondary: Color(0xFF00838F), ), + textTheme: TextTheme( + titleLarge: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), + titleSmall: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9, fontWeight: FontWeight.w200), + headlineMedium: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36), + displayLarge: TextStyle(fontSize: 18), + ), cardTheme: const CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), searchBarTheme: const SearchBarThemeData( @@ -56,6 +62,7 @@ ThemeData theme = ThemeData( expansionAnimationStyle: AnimationStyle(curve: Easing.standard, reverseCurve: Easing.standard), expandedAlignment: Alignment.bottomCenter, ), + dropdownMenuTheme: DropdownMenuThemeData(textStyle: TextStyle(fontFamily: "Eurostile Round", fontSize: 18)), scaffoldBackgroundColor: Colors.black ); diff --git a/lib/views/destination_calculator.dart b/lib/views/destination_calculator.dart index b240c99..81b7c23 100644 --- a/lib/views/destination_calculator.dart +++ b/lib/views/destination_calculator.dart @@ -188,7 +188,7 @@ class _DestinationCalculatorState extends State { padding: const EdgeInsets.only(bottom: 8.0), child: Column( children: [ - Text("Stats Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("Stats Calucator", style: Theme.of(context).textTheme.titleLarge), ], ), )), @@ -281,7 +281,7 @@ class _DestinationCalculatorState extends State { decoration: InputDecoration(hintText: "5"), onChanged: (value) => customClearsChoice[key] = int.parse(value), )), - Text(" Lines", style: TextStyle(fontSize: 18)), + Text(" Lines", style: Theme.of(context).textTheme.displayLarge), Icon(Icons.arrow_forward_ios) ], ), @@ -353,7 +353,7 @@ class _DestinationCalculatorState extends State { padding: const EdgeInsets.only(bottom: 8.0), child: Column( children: [ - Text("Damage Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("Damage Calucator", style: Theme.of(context).textTheme.titleLarge), ], ), )), diff --git a/lib/views/destination_cutoffs.dart b/lib/views/destination_cutoffs.dart index f4c426a..3a8ea02 100644 --- a/lib/views/destination_cutoffs.dart +++ b/lib/views/destination_cutoffs.dart @@ -75,7 +75,7 @@ class _DestinationCutoffsState extends State { padding: const EdgeInsets.only(bottom: 8.0), child: Column( children: [ - Text("Tetra League State", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("Tetra League State", style: Theme.of(context).textTheme.titleLarge), Text("as of ${timestamp(snapshot.data!.timestamp)}"), ], ), diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 5c0e183..59c0293 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -145,7 +145,7 @@ class LeagueCard extends StatelessWidget{ crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ - Text("Season ${league.season}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("Season ${league.season}", style: Theme.of(context).textTheme.titleSmall), Spacer(), Text( "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", @@ -153,7 +153,7 @@ class LeagueCard extends StatelessWidget{ style: TextStyle(color: Colors.grey)), ], ) - else Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + else Text("Tetra League", style: Theme.of(context).textTheme.titleSmall), const Divider(), TLRatingThingy(userID: "", tlData: league, showPositions: true), const Divider(), @@ -209,7 +209,7 @@ class _DestinationHomeState extends State with SingleTickerProv child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("40 Lines", style: Theme.of(context).textTheme.titleSmall), const Divider(), RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), const Divider(), @@ -226,7 +226,7 @@ class _DestinationHomeState extends State with SingleTickerProv child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("Blitz", style: Theme.of(context).textTheme.titleSmall), const Divider(), RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), const Divider(), @@ -248,7 +248,7 @@ class _DestinationHomeState extends State with SingleTickerProv child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("QP", style: Theme.of(context).textTheme.titleSmall), const Divider(), RecordSummary(record: summaries.zenith, hideRank: true), const Divider(), @@ -265,7 +265,7 @@ class _DestinationHomeState extends State with SingleTickerProv child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("QP Expert", style: Theme.of(context).textTheme.titleSmall), const Divider(), RecordSummary(record: summaries.zenithEx, hideRank: true,), const Divider(), @@ -287,7 +287,7 @@ class _DestinationHomeState extends State with SingleTickerProv child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("Zen", style: Theme.of(context).textTheme.titleSmall), Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), Text("Score ${intf.format(summaries.zen.score)}"), Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: const TextStyle(color: Colors.grey)) @@ -386,7 +386,7 @@ class _DestinationHomeState extends State with SingleTickerProv mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(t.tetraLeague, style: Theme.of(context).textTheme.titleLarge), //Text("${states.last.timestamp} ${states.last.tr}", textAlign: TextAlign.center) ], ), @@ -400,7 +400,7 @@ class _DestinationHomeState extends State with SingleTickerProv mainAxisSize: MainAxisSize.min, children: [ const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(t.nerdStats, style: Theme.of(context).textTheme.titleLarge), const Spacer() ], ), @@ -422,7 +422,7 @@ class _DestinationHomeState extends State with SingleTickerProv mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text("Previous Seasons", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("Previous Seasons", style: Theme.of(context).textTheme.titleLarge), //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) ], ), @@ -504,7 +504,7 @@ class _DestinationHomeState extends State with SingleTickerProv "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", String() => "huh", }, - style: const TextStyle(fontSize: 18)), + style: Theme.of(context).textTheme.displayLarge), subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) ) @@ -543,7 +543,7 @@ class _DestinationHomeState extends State with SingleTickerProv "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", String() => "huh", }, - style: const TextStyle(fontSize: 18)), + style: Theme.of(context).textTheme.displayLarge), subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) ) @@ -577,7 +577,7 @@ class _DestinationHomeState extends State with SingleTickerProv mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(t.recent, style: Theme.of(context).textTheme.titleLarge), ], ), ), @@ -618,7 +618,7 @@ class _DestinationHomeState extends State with SingleTickerProv mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(t.quickPlay, style: Theme.of(context).textTheme.titleLarge), //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), ], ), @@ -704,7 +704,7 @@ class _DestinationHomeState extends State with SingleTickerProv mainAxisSize: MainAxisSize.min, children: [ const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(t.nerdStats, style: Theme.of(context).textTheme.titleLarge), const Spacer() ], ), @@ -736,7 +736,7 @@ class _DestinationHomeState extends State with SingleTickerProv "blitz" => t.blitz, "5mblast" => "5,000,000 Blast", _ => record.gamemode - }, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)) + }, style: Theme.of(context).textTheme.titleLarge) ], ), ), @@ -986,10 +986,16 @@ class _DestinationHomeState extends State with SingleTickerProv if (snapshot.data!.summaries!.sprint != null) { closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; + } else { + closestAverageSprint = sprintAverages.entries.last; + sprintBetterThanClosestAverage = false; } if (snapshot.data!.summaries!.blitz != null){ closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; + } else { + closestAverageBlitz = blitzAverages.entries.last; + blitzBetterThanClosestAverage = false; } return TweenAnimationBuilder( duration: Durations.long4, diff --git a/lib/views/destination_leaderboards.dart b/lib/views/destination_leaderboards.dart index 431f25e..4fad288 100644 --- a/lib/views/destination_leaderboards.dart +++ b/lib/views/destination_leaderboards.dart @@ -114,12 +114,12 @@ class _DestinationLeaderboardsState extends State { height: widget.constraints.maxHeight, child: Column( children: [ - const Card( + Card( child: Row( mainAxisSize: MainAxisSize.min, children: [ Spacer(), - Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Text("Leaderboards", style: Theme.of(context).textTheme.headlineMedium), Spacer() ], ), @@ -162,7 +162,7 @@ class _DestinationLeaderboardsState extends State { if (snapshot.hasData){ return Column( children: [ - Text(leaderboards[_currentLb]!, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text(leaderboards[_currentLb]!, style: Theme.of(context).textTheme.titleSmall), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/views/destination_saved_data.dart b/lib/views/destination_saved_data.dart index 51936dc..04ba512 100644 --- a/lib/views/destination_saved_data.dart +++ b/lib/views/destination_saved_data.dart @@ -75,12 +75,12 @@ class _DestinationSavedData extends State { width: 450, child: Column( children: [ - const Card( + Card( child: Row( mainAxisSize: MainAxisSize.min, children: [ Spacer(), - Text("Saved Data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Text("Saved Data", style: Theme.of(context).textTheme.headlineMedium), Spacer() ], ), diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index af9923b..4ef9c32 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1220,7 +1220,7 @@ class _TwoRecordsThingy extends StatelessWidget { onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), title: Text(get40lTime(sprintStream.records[i].stats.finalTime.inMicroseconds), - style: const TextStyle(fontSize: 18)), + style: Theme.of(context).textTheme.displayLarge), subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), trailing: SpTrailingStats(sprintStream.records[i], sprintStream.records[i].gamemode) ) @@ -1306,7 +1306,7 @@ class _TwoRecordsThingy extends StatelessWidget { onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].stats.score)} points", - style: const TextStyle(fontSize: 18)), + style: Theme.of(context).textTheme.displayLarge), subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), trailing: SpTrailingStats(blitzStream.records[i], blitzStream.records[i].gamemode) ) @@ -1558,7 +1558,7 @@ class _OtherThingy extends StatelessWidget { children: getDistinguishmentTitle(distinguishment?.header), ), ), - Text(getDistinguishmentSubtitle(distinguishment?.footer), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), + Text(getDistinguishmentSubtitle(distinguishment?.footer), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), ], ), ), @@ -1568,7 +1568,7 @@ class _OtherThingy extends StatelessWidget { child: Column( children: [ Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended",fontSize: bigScreen ? 42 : 28)), - MarkdownBody(data: bio!, styleSheet: MarkdownStyleSheet(textScaler: TextScaler.linear(1.5), textAlign: WrapAlignment.center)) // Text(bio!, style: const TextStyle(fontSize: 18)), + MarkdownBody(data: bio!, styleSheet: MarkdownStyleSheet(textScaler: TextScaler.linear(1.5), textAlign: WrapAlignment.center)) // Text(bio!, style: const Theme.of(context).textTheme.displayLarge), ], ), ), @@ -1579,7 +1579,7 @@ class _OtherThingy extends StatelessWidget { 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)), + Text("${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: Theme.of(context).textTheme.displayLarge), Container( constraints: const BoxConstraints(maxWidth: 300.0), child: Row(children: [ @@ -1609,7 +1609,7 @@ class _OtherThingy extends StatelessWidget { physics: const AlwaysScrollableScrollPhysics(), itemCount: newsletter!.news.length+1, itemBuilder: (BuildContext context, int index) { - return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter!.news[index-1]); + return index == 0 ? Center(child: Text(t.news, style: Theme.of(context).textTheme.titleLarge)) : getNewsTile(newsletter!.news[index-1]); } )) ] diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 2316ff5..5648170 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -236,10 +236,9 @@ enum SettingsCardMod{ final String title; } -const TextStyle settingsTitlesStyle = TextStyle(fontSize: 18); const EdgeInsets descriptionPadding = EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 8.0); -class _DestinationSettings extends State { +class _DestinationSettings extends State with SingleTickerProviderStateMixin { SettingsCardMod mod = SettingsCardMod.general; List> locales = >[]; String defaultNickname = "Checking..."; @@ -251,6 +250,8 @@ class _DestinationSettings extends State { late bool showAverages; late bool updateInBG; final TextEditingController _playertext = TextEditingController(); + late AnimationController _defaultNicknameAnimController; + late Animation _defaultNicknameAnim; @override void initState() { @@ -258,6 +259,18 @@ class _DestinationSettings extends State { // windowManager.getTitle().then((value) => oldWindowTitle = value); // windowManager.setTitle("Tetra Stats: ${t.settings}"); // } + _defaultNicknameAnimController = AnimationController( + duration: Durations.extralong4, + vsync: this, + ); + _defaultNicknameAnim = new Tween( + begin: 0.0, + end: 1.0, + ).animate(new CurvedAnimation( + parent: _defaultNicknameAnimController, + curve: Cubic(.15,-0.40,.86,-0.39), + reverseCurve: Cubic(0,.99,.99,1.01) + )); _getPreferences(); super.initState(); } @@ -279,22 +292,26 @@ class _DestinationSettings extends State { _setDefaultNickname(prefs.getString("player")); } - Future _setDefaultNickname(String? n) async { + Future _setDefaultNickname(String? n) async { if (n != null) { try { defaultNickname = await teto.getNicknameByID(n); + return true; } on TetrioPlayerNotExist { defaultNickname = n; + return false; } } else { defaultNickname = "dan63047"; + return true; } - setState(() {}); + //setState(() {}); } - Future _setPlayer(String player) async { - await prefs.setString('player', player); - await _setDefaultNickname(player); + Future _setPlayer(String player) async { + bool success = await _setDefaultNickname(player); + if (success) await prefs.setString('player', player); + return success; } Future _removePlayer() async { @@ -310,7 +327,7 @@ class _DestinationSettings extends State { padding: const EdgeInsets.only(bottom: 8.0), child: Column( children: [ - Text(SettingsCardMod.general.title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(SettingsCardMod.general.title, style: Theme.of(context).textTheme.titleLarge), ], ), )), @@ -320,11 +337,21 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Your account in TETR.IO", style: settingsTitlesStyle), - trailing: SizedBox(width: 150.0, child: TextField( - keyboardType: TextInputType.text, - decoration: InputDecoration(hintText: defaultNickname), - //onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}), + title: Text("Your account in TETR.IO", style: Theme.of(context).textTheme.displayLarge), + trailing: SizedBox(width: 150.0, child: AnimatedBuilder( + animation: _defaultNicknameAnim, + builder: (context, child) { + return TextField( + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: defaultNickname, + helper: Text("Press Enter to submit", style: TextStyle(color: Colors.grey, height: 0.2)), + ), + onChanged: (value) { + _setPlayer(value).then((v) {}); + }, + ); + }, )), ), Divider(), @@ -340,7 +367,7 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Language", style: settingsTitlesStyle), + title: Text("Language", style: Theme.of(context).textTheme.displayLarge), trailing: DropdownButton( items: locales, value: LocaleSettings.currentLocale, @@ -367,7 +394,7 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Update data in the background", style: settingsTitlesStyle), + title: Text("Update data in the background", style: Theme.of(context).textTheme.displayLarge), trailing: Switch(value: updateInBG, onChanged: (bool value){ prefs.setBool("updateInBG", value); setState(() { @@ -388,7 +415,7 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Show leaderboard based stats", style: settingsTitlesStyle), + title: Text("Show leaderboard based stats", style: Theme.of(context).textTheme.displayLarge), trailing: Switch(value: showAverages, onChanged: (bool value){ prefs.setBool("showAverages", value); setState(() { @@ -410,7 +437,7 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Show position on leaderboard by stats", style: settingsTitlesStyle), + title: Text("Show position on leaderboard by stats", style: Theme.of(context).textTheme.displayLarge), trailing: Switch(value: showPositions, onChanged: (bool value){ prefs.setBool("showPositions", value); setState(() { @@ -438,7 +465,7 @@ class _DestinationSettings extends State { padding: const EdgeInsets.only(bottom: 8.0), child: Column( children: [ - Text(SettingsCardMod.customization.title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(SettingsCardMod.customization.title, style: Theme.of(context).textTheme.titleLarge), ], ), )), @@ -448,7 +475,7 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Accent color", style: settingsTitlesStyle), + title: Text("Accent color", style: Theme.of(context).textTheme.displayLarge), trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25), ), Divider(), @@ -465,7 +492,7 @@ class _DestinationSettings extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( - title: Text("Timestamps format", style: settingsTitlesStyle), + title: Text("Timestamps format", style: Theme.of(context).textTheme.displayLarge), trailing: DropdownButton( value: timestampMode, items: [ @@ -498,7 +525,7 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Sheetbot-like behavior for radar graphs", style: settingsTitlesStyle), + title: Text("Sheetbot-like behavior for radar graphs", style: Theme.of(context).textTheme.displayLarge), trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){ prefs.setBool("sheetbotRadarGraphs", value); setState(() { @@ -519,7 +546,7 @@ class _DestinationSettings extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Osk-Kagari gimmick", style: settingsTitlesStyle), + title: Text("Osk-Kagari gimmick", style: Theme.of(context).textTheme.displayLarge), trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){ prefs.setBool("oskKagariGimmick", value); setState(() { @@ -545,7 +572,7 @@ class _DestinationSettings extends State { Card( child: Center(child: Column( children: [ - Text(SettingsCardMod.database.title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(SettingsCardMod.database.title, style: Theme.of(context).textTheme.titleLarge), Divider(), FutureBuilder<(int, int, int)>(future: teto.getDatabaseData(), builder: (context, snapshot) { @@ -602,12 +629,12 @@ class _DestinationSettings extends State { ), Card( child: ListTile( - title: Text("Export Database", style: settingsTitlesStyle), + title: Text("Export Database", style: Theme.of(context).textTheme.displayLarge), ), ), Card( child: ListTile( - title: Text("Import Database", style: settingsTitlesStyle), + title: Text("Import Database", style: Theme.of(context).textTheme.displayLarge), ), ) ], @@ -628,12 +655,12 @@ class _DestinationSettings extends State { width: 450, child: Column( children: [ - const Card( + Card( child: Row( mainAxisSize: MainAxisSize.min, children: [ Spacer(), - Text("Settings", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Text("Settings", style: Theme.of(context).textTheme.headlineMedium), Spacer() ], ), @@ -939,7 +966,7 @@ class DistinguishmentThingy extends StatelessWidget{ children: getDistinguishmentTitle(distinguishment.header), ), ), - Text(getDistinguishmentSubtitle(distinguishment.footer), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), + Text(getDistinguishmentSubtitle(distinguishment.footer), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), ], ), ); @@ -1001,7 +1028,7 @@ class FakeDistinguishmentThingy extends StatelessWidget{ ), ), ), - Text(getDistinguishmentSubtitle(), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), + Text(getDistinguishmentSubtitle(), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), ], ), ), @@ -1376,10 +1403,13 @@ class _NewUserThingyState extends State with SingleTickerProvider ), ), ) : Container( - transform: Matrix4.translationValues(0, secondButtonPosition, 0), + transform: Matrix4.translationValues(secondButtonPosition*5, -secondButtonPosition*25, 0), child: Opacity( - opacity: max(0, secondButtonOpacity), - child: const Icon(Icons.person_remove) + opacity: max(0, min(1, secondButtonOpacity)), + child: Transform.rotate( + angle:_addToTrackAnim.status == AnimationStatus.reverse ? (1-_addToTrackAnim.value as double)*-20 : 0, + child: const Icon(Icons.person_remove) + ) ) ), label: _addToTrackAnim.value < 0.5 ? Container( @@ -1391,8 +1421,8 @@ class _NewUserThingyState extends State with SingleTickerProvider ) : Container( transform: Matrix4.translationValues(0, secondButtonPosition, 0), child: Opacity( - opacity: max(0, secondButtonOpacity), - child: Text("Stop tracking") + opacity: max(0, min(1, secondButtonOpacity)), + child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.reverse ? "Done! " : "Stop tracking") ) ), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0)))))); diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart index 5040240..03cceac 100644 --- a/lib/views/user_view.dart +++ b/lib/views/user_view.dart @@ -43,15 +43,32 @@ class UserState extends State { Widget build(BuildContext context) { //final t = Translations.of(context); return Scaffold( - appBar: AppBar( - title: Text("Search For"), - ), backgroundColor: Colors.black, + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + floatingActionButton: Padding( + padding: const EdgeInsets.all(8.0), + child: FloatingActionButton( + onPressed: () => Navigator.pop(context), + tooltip: 'Fuck go back', + child: const Icon(Icons.arrow_back), + ), + ), body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return DestinationHome(searchFor: widget.searchFor, dataFuture: getData(widget.searchFor), newsFuture: teto.fetchNews(widget.searchFor), constraints: constraints); - } + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Row( + children: [ + Card( + child: Column( + children: [ + Text("Pornograph", style: TextStyle(),) + ] + ), + ), + DestinationHome(searchFor: widget.searchFor, dataFuture: getData(widget.searchFor), newsFuture: teto.fetchNews(widget.searchFor), constraints: constraints), + ], + ); + } ) ) ); diff --git a/lib/widgets/recent_sp_games.dart b/lib/widgets/recent_sp_games.dart index 92a2ff9..028815d 100644 --- a/lib/widgets/recent_sp_games.dart +++ b/lib/widgets/recent_sp_games.dart @@ -41,7 +41,7 @@ class RecentSingleplayerGames extends StatelessWidget{ "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), String() => "huh", }, - style: const TextStyle(fontSize: 18)), + style: Theme.of(context).textTheme.displayLarge), subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), trailing: SpTrailingStats(record, record.gamemode) ) diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index d2086b9..2276d8d 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -141,7 +141,7 @@ class SingleplayerRecord extends StatelessWidget { "5mblast" => get40lTime(stream!.records[i].stats.finalTime.inMicroseconds), String() => "huh", }, - style: const TextStyle(fontSize: 18)), + style: Theme.of(context).textTheme.displayLarge), subtitle: Text(timestamp(stream!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), trailing: SpTrailingStats(stream!.records[i], stream!.records[i].gamemode) ) From b8a8ddf0c9c13db4b971d6a28cb2b1db88bfefed Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 23 Oct 2024 01:28:55 +0300 Subject: [PATCH 54/86] default profile and comparing with averages in TL are now work --- .github/workflows/main.yml | 6 +- lib/data_objects/cutoff_tetrio.dart | 8 +- lib/utils/colors_functions.dart | 10 ++ lib/views/destination_home.dart | 36 ++++-- lib/views/main_view_tiles.dart | 170 +++++++++++++++++----------- lib/views/user_view.dart | 2 +- 6 files changed, 153 insertions(+), 79 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29964e1..737977e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: discussionCategory: autobuilded-releases artifacts: "build/windows/x64/runner/Release/TetraStats-${{github.ref_name}}-windows.zip" tag: Auto-${{ github.run_number }} - body: Builded with GitHub Action workflow + body: Build with GitHub Action workflow token: ${{ secrets.TOKEN }} build-and-release-linux: name: Build Linux App @@ -71,7 +71,7 @@ jobs: discussionCategory: autobuilded-releases artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip" tag: Auto-${{ github.run_number }} - body: Builded with GitHub Action workflow + body: Build with GitHub Action workflow token: ${{ secrets.TOKEN }} # build-and-release-android: # name: Build Android App @@ -96,5 +96,5 @@ jobs: # discussionCategory: autobuilded-releases # artifacts: "build/app/outputs/flutter-apk/*" # tag: Auto-${{ github.run_number }} - # body: Builded with GitHub Action workflow + # body: Build with GitHub Action workflow # token: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/lib/data_objects/cutoff_tetrio.dart b/lib/data_objects/cutoff_tetrio.dart index da51e00..83b3379 100644 --- a/lib/data_objects/cutoff_tetrio.dart +++ b/lib/data_objects/cutoff_tetrio.dart @@ -1,4 +1,4 @@ -// ignore_for_file: hash_and_equals +import 'package:tetra_stats/data_objects/nerd_stats.dart'; class CutoffTetrio { late int pos; @@ -8,6 +8,7 @@ class CutoffTetrio { late double? apm; late double? pps; late double? vs; + NerdStats? nerdStats; late int count; late double countPercentile; @@ -21,7 +22,9 @@ class CutoffTetrio { required this.vs, required this.count, required this.countPercentile - }); + }){ + if (apm != null && pps != null && vs != null) nerdStats = NerdStats(apm!, pps!, vs!); + } CutoffTetrio.fromJson(Map json, int total){ pos = json['pos']; @@ -33,6 +36,7 @@ class CutoffTetrio { vs = json['vs']?.toDouble(); count = json['count']; countPercentile = count / total; + if (apm != null && pps != null && vs != null) nerdStats = NerdStats(apm!, pps!, vs!); } } diff --git a/lib/utils/colors_functions.dart b/lib/utils/colors_functions.dart index 8ec5900..1e04665 100644 --- a/lib/utils/colors_functions.dart +++ b/lib/utils/colors_functions.dart @@ -9,6 +9,16 @@ Color getColorOfRank(int rank){ return Colors.grey; } +Color? getStatColor(num value, num? avgValue, bool higherIsBetter){ + if (avgValue == null) return null; + num percentile = (higherIsBetter ? value / avgValue : avgValue / value).abs(); + 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; + } + Color getDifferenceColor(num diff){ return diff.isNegative ? Colors.redAccent : Colors.greenAccent; } \ No newline at end of file diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 59c0293..24d8f53 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; import 'package:tetra_stats/data_objects/news.dart'; import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; import 'package:tetra_stats/data_objects/record_extras.dart'; @@ -44,10 +45,11 @@ class FetchResults{ List states; Summaries? summaries; Cutoffs? cutoffs; + CutoffsTetrio? averages; bool isTracked; Exception? exception; - FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.isTracked, this.exception); + FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.averages, this.isTracked, this.exception); } class RecordSummary extends StatelessWidget{ @@ -128,9 +130,10 @@ class RecordSummary extends StatelessWidget{ class LeagueCard extends StatelessWidget{ final TetraLeague league; + final CutoffTetrio? averages; final bool showSeasonNumber; - const LeagueCard({super.key, required this.league, this.showSeasonNumber = false}); + const LeagueCard({super.key, required this.league, this.averages, this.showSeasonNumber = false}); @override Widget build(BuildContext context) { @@ -157,7 +160,20 @@ class LeagueCard extends StatelessWidget{ const Divider(), TLRatingThingy(userID: "", tlData: league, showPositions: true), const Divider(), - Text("${league.apm != null ? f2.format(league.apm) : "-.--"} APM • ${league.pps != null ? f2.format(league.pps) : "-.--"} PPS • ${league.vs != null ? f2.format(league.vs) : "-.--"} VS • ${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP • ${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) + RichText(text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.grey), + children: [ + TextSpan(text: "${league.apm != null ? f2.format(league.apm) : "-.--"} APM", style: TextStyle(color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.pps != null ? f2.format(league.pps) : "-.--"} PPS", style: TextStyle(color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.vs != null ? f2.format(league.vs) : "-.--"} VS", style: TextStyle(color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP", style: TextStyle(color: league.nerdStats != null ? getStatColor(league.nerdStats!.app, averages?.nerdStats?.app, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: TextStyle(color: league.nerdStats != null ? getStatColor(league.nerdStats!.vsapm, averages?.nerdStats?.vsapm, true) : null)), + ] + )), ], ), ), @@ -181,7 +197,7 @@ class _DestinationHomeState extends State with SingleTickerProv bool? sprintBetterThanRankAverage; bool? blitzBetterThanRankAverage; - Widget getOverviewCard(Summaries summaries){ + Widget getOverviewCard(Summaries summaries, CutoffTetrio? averages){ return Column( children: [ const Card( @@ -198,7 +214,7 @@ class _DestinationHomeState extends State with SingleTickerProv ), ), ), - LeagueCard(league: summaries.league), + LeagueCard(league: summaries.league, averages: averages), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -373,7 +389,7 @@ class _DestinationHomeState extends State with SingleTickerProv ); } - Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, List states){ + Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, CutoffTetrio? averages, List states){ TetraLeague? toCompare = states.length >= 2 ? states.elementAtOrNull(states.length-2) : null; return Column( children: [ @@ -393,7 +409,7 @@ class _DestinationHomeState extends State with SingleTickerProv ), ), ), - TetraLeagueThingy(league: data, toCompare: toCompare, cutoffs: cutoffs), + TetraLeagueThingy(league: data, toCompare: toCompare, cutoffs: cutoffs, averages: averages), if (data.nerdStats != null) Card( //surfaceTintColor: rankColors[data.rank], child: Row( @@ -405,7 +421,7 @@ class _DestinationHomeState extends State with SingleTickerProv ], ), ), - if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats), + if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats, averages: averages), if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) ], ); @@ -1068,9 +1084,9 @@ class _DestinationHomeState extends State with SingleTickerProv child: SlideTransition( position: _offsetAnimation, child: switch (rightCard){ - Cards.overview => getOverviewCard(snapshot.data!.summaries!), + Cards.overview => getOverviewCard(snapshot.data!.summaries!, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : null), Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : null, snapshot.data!.states), CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), CardMod.records => getRecentTLrecords(widget.constraints), _ => const Center(child: Text("huh?")) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 5648170..e463641 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -11,6 +11,7 @@ import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/badge.dart'; import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; import 'package:tetra_stats/data_objects/distinguishment.dart'; import 'package:tetra_stats/data_objects/est_tr.dart'; import 'package:tetra_stats/data_objects/nerd_stats.dart'; @@ -63,24 +64,32 @@ Future getData(String searchFor) async { player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username } }on TetrioPlayerNotExist{ - return FetchResults(false, null, [], null, null, false, TetrioPlayerNotExist()); + return FetchResults(false, null, [], null, null, null, false, TetrioPlayerNotExist()); } late Summaries summaries; late Cutoffs cutoffs; - List requests = await Future.wait([ - teto.fetchSummaries(player.userId), - teto.fetchCutoffsBeanserver(), - ]); + late CutoffsTetrio averages; + try { + List requests = await Future.wait([ + teto.fetchSummaries(player.userId), + teto.fetchCutoffsBeanserver(), + teto.fetchCutoffsTetrio() + ]); + + summaries = requests[0]; + cutoffs = requests.elementAtOrNull(1); + averages = requests.elementAtOrNull(2); + } on Exception catch (e) { + return FetchResults(false, null, [], null, null, null, false, e); + } List states = await teto.getStates(player.userId, season: currentSeason); - summaries = requests[0]; - cutoffs = requests[1]; bool isTracking = await teto.isPlayerTracking(player.userId); if (isTracking){ // if tracked - save data to local DB await teto.storeState(summaries.league); } - return FetchResults(true, player, states, summaries, cutoffs, isTracking, null); + return FetchResults(true, player, states, summaries, cutoffs, averages, isTracking, null); } class MainView extends StatefulWidget { @@ -242,6 +251,7 @@ class _DestinationSettings extends State with SingleTickerP SettingsCardMod mod = SettingsCardMod.general; List> locales = >[]; String defaultNickname = "Checking..."; + String defaultID = ""; late bool oskKagariGimmick; late bool sheetbotRadarGraphs; late int ratingMode; @@ -251,7 +261,11 @@ class _DestinationSettings extends State with SingleTickerP late bool updateInBG; final TextEditingController _playertext = TextEditingController(); late AnimationController _defaultNicknameAnimController; - late Animation _defaultNicknameAnim; + late Animation _goodDefaultNicknameAnim; + late Animation _badDefaultNicknameAnim; + late Animation _defaultNicknameAnim = _goodDefaultNicknameAnim; + double helperTextOpacity = 0; + String helperText = "Press Enter to submit"; @override void initState() { @@ -260,17 +274,30 @@ class _DestinationSettings extends State with SingleTickerP // windowManager.setTitle("Tetra Stats: ${t.settings}"); // } _defaultNicknameAnimController = AnimationController( + value: 1.0, duration: Durations.extralong4, vsync: this, ); - _defaultNicknameAnim = new Tween( - begin: 0.0, - end: 1.0, + _goodDefaultNicknameAnim = new ColorTween( + begin: Colors.greenAccent, + end: Colors.grey, ).animate(new CurvedAnimation( parent: _defaultNicknameAnimController, - curve: Cubic(.15,-0.40,.86,-0.39), - reverseCurve: Cubic(0,.99,.99,1.01) - )); + curve: Easing.emphasizedAccelerate, + //reverseCurve: Cubic(0,.99,.99,1.01) + ))..addStatusListener((status) { + if (status.index == 3) setState((){helperText = "Press Enter to submit"; helperTextOpacity = 0;}); + }); + _badDefaultNicknameAnim = new ColorTween( + begin: Colors.redAccent, + end: Colors.grey, + ).animate(new CurvedAnimation( + parent: _defaultNicknameAnimController, + curve: Easing.emphasizedAccelerate, + //reverseCurve: Cubic(0,.99,.99,1.01) + ))..addStatusListener((status) { + if (status.index == 3) setState((){helperText = "Press Enter to submit"; helperTextOpacity = 0;}); + }); _getPreferences(); super.initState(); } @@ -289,36 +316,35 @@ class _DestinationSettings extends State with SingleTickerP sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")?? false; ratingMode = prefs.getInt("ratingMode") ?? 0; timestampMode = prefs.getInt("timestampMode") ?? 0; - _setDefaultNickname(prefs.getString("player")); + _setDefaultNickname(prefs.getString("player")??"").then((v){setState((){});}); + defaultID = prefs.getString("playerID")??""; } - Future _setDefaultNickname(String? n) async { - if (n != null) { + Future _setDefaultNickname(String n) async { + if (n.isNotEmpty) { try { - defaultNickname = await teto.getNicknameByID(n); + if (n.length > 16){ + defaultNickname = await teto.getNicknameByID(n); + await prefs.setString('playerID', n); + }else{ + TetrioPlayer player = await teto.fetchPlayer(n); + defaultNickname = player.username; + await prefs.setString('playerID', player.userId); + } + await prefs.setString('player', defaultNickname); return true; - } on TetrioPlayerNotExist { - defaultNickname = n; + } catch (e) { return false; } } else { - defaultNickname = "dan63047"; + defaultNickname = "dan63"; + await prefs.setString('player', "dan63"); + await prefs.setString('playerID', "6098518e3d5155e6ec429cdc"); return true; } //setState(() {}); } - Future _setPlayer(String player) async { - bool success = await _setDefaultNickname(player); - if (success) await prefs.setString('player', player); - return success; - } - - Future _removePlayer() async { - await prefs.remove('player'); - await _setDefaultNickname("6098518e3d5155e6ec429cdc"); - } - Widget getGeneralSettings(){ return Column( children: [ @@ -341,15 +367,30 @@ class _DestinationSettings extends State with SingleTickerP trailing: SizedBox(width: 150.0, child: AnimatedBuilder( animation: _defaultNicknameAnim, builder: (context, child) { - return TextField( - keyboardType: TextInputType.text, - decoration: InputDecoration( - hintText: defaultNickname, - helper: Text("Press Enter to submit", style: TextStyle(color: Colors.grey, height: 0.2)), - ), - onChanged: (value) { - _setPlayer(value).then((v) {}); + return Focus( + onFocusChange: (value) { + setState((){helperTextOpacity = ((value || helperText != "Press Enter to submit")) ? 1 : 0;}); }, + child: TextField( + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: defaultNickname, + helper: AnimatedOpacity( + opacity: helperTextOpacity, + duration: Durations.long1, + curve: Easing.standardDecelerate, + child: Text(helperText, style: TextStyle(color: _defaultNicknameAnim.value, height: 0.2)) + ), + ), + onSubmitted: (value) { + helperText = "Checking..."; + _setDefaultNickname(value).then((v) { + _defaultNicknameAnim = v ? _goodDefaultNicknameAnim : _badDefaultNicknameAnim; + _defaultNicknameAnimController.forward(from: 0); + setState((){ helperText = v ? "Done!" : "Fuck";}); + }); + }, + ), ); }, )), @@ -415,7 +456,7 @@ class _DestinationSettings extends State with SingleTickerP mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("Show leaderboard based stats", style: Theme.of(context).textTheme.displayLarge), + title: Text("Compare TL stats with rank averages", style: Theme.of(context).textTheme.displayLarge), trailing: Switch(value: showAverages, onChanged: (bool value){ prefs.setBool("showAverages", value); setState(() { @@ -426,7 +467,7 @@ class _DestinationSettings extends State with SingleTickerP Divider(), Padding( padding: descriptionPadding, - child: Text("If on, Tetra Stats gonnna provide additional metrics, which will allow you to compare yourself with average player on your rank. The way you'll see it — stats will be highlited with corresponding color, hover over them with cursor for more info."), + child: Text("If on, Tetra Stats will provide additional metrics, which allow you to compare yourself with average player on your rank. The way you'll see it — stats will be highlited with corresponding color, hover over them with cursor for more info."), ) ], ), @@ -1474,7 +1515,7 @@ class _SearchDrawerState extends State { final allPlayers = (snapshot.data != null) ? snapshot.data as Map : {}; - allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted + allPlayers.remove(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted List keys = allPlayers.keys.toList(); return NestedScrollView( headerSliverBuilder: (BuildContext context, bool value){ @@ -1502,7 +1543,7 @@ class _SearchDrawerState extends State { child: ListTile( title: Text(prefs.getString("player") ?? "dan63"), onTap: () { - widget.changePlayer("6098518e3d5155e6ec429cdc"); + widget.changePlayer(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); Navigator.of(context).pop(); }, ), @@ -1533,8 +1574,9 @@ class TetraLeagueThingy extends StatelessWidget{ final TetraLeague league; final TetraLeague? toCompare; final Cutoffs? cutoffs; + final CutoffTetrio? averages; - const TetraLeagueThingy({super.key, required this.league, this.toCompare, this.cutoffs}); + const TetraLeagueThingy({super.key, required this.league, this.toCompare, this.cutoffs, this.averages}); @override Widget build(BuildContext context) { @@ -1563,18 +1605,18 @@ class TetraLeagueThingy extends StatelessWidget{ defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ - Text(f2.format(league.apm??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" APM", style: TextStyle(fontSize: 21)), + Text(f2.format(league.apm??0.00), textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : null)), + Text(" APM", style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : null)), if (toCompare != null) Text(" (${comparef2.format(league.apm!-toCompare!.apm!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.apm!-toCompare!.apm!))) ]), TableRow(children: [ - Text(f2.format(league.pps??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" PPS", style: TextStyle(fontSize: 21)), + Text(f2.format(league.pps??0.00), textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : null)), + Text(" PPS", style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : null)), if (toCompare != null) Text(" (${comparef2.format(league.pps!-toCompare!.pps!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.pps!-toCompare!.pps!))) ]), TableRow(children: [ - Text(f2.format(league.vs??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" VS", style: TextStyle(fontSize: 21)), + Text(f2.format(league.vs??0.00), textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : null)), + Text(" VS", style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : null)), if (toCompare != null) Text(" (${comparef2.format(league.vs!-toCompare!.vs!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.vs!-toCompare!.vs!))) ]) ], @@ -1657,8 +1699,9 @@ class TetraLeagueThingy extends StatelessWidget{ class NerdStatsThingy extends StatelessWidget{ final NerdStats nerdStats; final NerdStats? oldNerdStats; + final CutoffTetrio? averages; - const NerdStatsThingy({super.key, required this.nerdStats, this.oldNerdStats}); + const NerdStatsThingy({super.key, required this.nerdStats, this.oldNerdStats, this.averages}); @override Widget build(BuildContext context) { @@ -1700,7 +1743,7 @@ class NerdStatsThingy extends StatelessWidget{ style: const TextStyle(fontFamily: "Eurostile Round"), children: [ const TextSpan(text: "APP\n"), - TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), + TextSpan(text: f3.format(nerdStats.app), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.app, averages?.nerdStats?.app, true))), if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.app - oldNerdStats!.app)}", style: TextStyle(color: getDifferenceColor(nerdStats.app - oldNerdStats!.app))), ] ))), @@ -1730,7 +1773,7 @@ class NerdStatsThingy extends StatelessWidget{ style: const TextStyle(fontFamily: "Eurostile Round"), children: [ const TextSpan(text: "VS/APM\n"), - TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), + TextSpan(text: f3.format(nerdStats.vsapm), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.vsapm, averages?.nerdStats?.vsapm, true))), if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.vsapm - oldNerdStats!.vsapm)}", style: TextStyle(color: getDifferenceColor(nerdStats.vsapm - oldNerdStats!.vsapm))), ] ))), @@ -1749,13 +1792,13 @@ class NerdStatsThingy extends StatelessWidget{ runSpacing: 10.0, runAlignment: WrapAlignment.start, children: [ - GaugetThingy(value: nerdStats.dss, oldValue: oldNerdStats?.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), - GaugetThingy(value: nerdStats.dsp, oldValue: oldNerdStats?.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), - GaugetThingy(value: nerdStats.appdsp, oldValue: oldNerdStats?.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), + GaugetThingy(value: nerdStats.dss, oldValue: oldNerdStats?.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dss), + GaugetThingy(value: nerdStats.dsp, oldValue: oldNerdStats?.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dsp), + GaugetThingy(value: nerdStats.appdsp, oldValue: oldNerdStats?.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.appdsp), GaugetThingy(value: nerdStats.cheese, oldValue: oldNerdStats?.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2, moreIsBetter: false), - GaugetThingy(value: nerdStats.gbe, oldValue: oldNerdStats?.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), - GaugetThingy(value: nerdStats.nyaapp, oldValue: oldNerdStats?.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true), - GaugetThingy(value: nerdStats.area, oldValue: oldNerdStats?.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1, moreIsBetter: true), + GaugetThingy(value: nerdStats.gbe, oldValue: oldNerdStats?.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.gbe), + GaugetThingy(value: nerdStats.nyaapp, oldValue: oldNerdStats?.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.nyaapp), + GaugetThingy(value: nerdStats.area, oldValue: oldNerdStats?.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1, moreIsBetter: true, avgValue: averages?.nerdStats?.area), ], ), ) @@ -1807,13 +1850,14 @@ class GaugetThingy extends StatelessWidget{ final double min; final double max; final double? oldValue; + final double? avgValue; final bool moreIsBetter; final double tickInterval; final String label; final double sideSize; final int fractionDigits; - GaugetThingy({super.key, required this.value, required this.min, required this.max, this.oldValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter}); + GaugetThingy({super.key, required this.value, required this.min, required this.max, this.oldValue, this.avgValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter}); @override Widget build(BuildContext context) { @@ -1839,7 +1883,7 @@ class GaugetThingy extends StatelessWidget{ ], annotations: [ GaugeAnnotation(widget: Container(child: - Text(f.format(value), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + Text(f.format(value), textAlign: TextAlign.center, style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: getStatColor(value, avgValue, moreIsBetter)))), angle: 90,positionFactor: 0.10 ), GaugeAnnotation(widget: Container(child: diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart index 03cceac..2fdbe05 100644 --- a/lib/views/user_view.dart +++ b/lib/views/user_view.dart @@ -61,7 +61,7 @@ class UserState extends State { Card( child: Column( children: [ - Text("Pornograph", style: TextStyle(),) + Text("oskagalove", style: TextStyle(),) ] ), ), From ae7d92fcacb520db8136306aefc59ab22ab4cc1a Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 29 Oct 2024 00:55:38 +0300 Subject: [PATCH 55/86] i'm feeling a bit sick --- lib/data_objects/summaries.dart | 4 +- lib/views/destination_cutoffs.dart | 6 +- lib/views/destination_graphs.dart | 78 +- lib/views/destination_home.dart | 28 +- lib/views/destination_settings.dart | 551 +++++++++++++ lib/views/main_view_tiles.dart | 724 ++++-------------- lib/views/rank_view.dart | 271 +++++++ pubspec.yaml | 1 + .../Снимок экрана_2023-11-06_01-00-50.png | Bin 0 -> 1545331 bytes 9 files changed, 1079 insertions(+), 584 deletions(-) create mode 100644 lib/views/destination_settings.dart create mode 100644 lib/views/rank_view.dart create mode 100644 res/images/Снимок экрана_2023-11-06_01-00-50.png diff --git a/lib/data_objects/summaries.dart b/lib/data_objects/summaries.dart index 31540b4..4c8f105 100644 --- a/lib/data_objects/summaries.dart +++ b/lib/data_objects/summaries.dart @@ -39,9 +39,9 @@ class Summaries { zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); if (json['zenithex']['best']['record'] != null) - zenithCareerBest = RecordSingle.fromJson( + zenithExCareerBest = RecordSingle.fromJson( json['zenithex']['best']['record'], - json['zenith']['best']['rank'], + json['zenithex']['best']['rank'], -1); achievements = [ for (var achievement in json['achievements']) diff --git a/lib/views/destination_cutoffs.dart b/lib/views/destination_cutoffs.dart index 3a8ea02..5e4ad0a 100644 --- a/lib/views/destination_cutoffs.dart +++ b/lib/views/destination_cutoffs.dart @@ -10,6 +10,7 @@ import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/rank_view.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:vector_math/vector_math_64.dart' hide Colors; @@ -273,7 +274,10 @@ class _DestinationCutoffsState extends State { Padding( padding: const EdgeInsets.only(right: 8.0), child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { - + Navigator.push(context, MaterialPageRoute( + builder: (context) => RankView(rank: rank, nextRankTR: rank == "x+" ? snapshot.data!.data["top1"]!.tr : snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr, nextRankPercentile: rank == "x+" ? 0.00 : snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.percentile, nextRankTargetTR: rank == "x+" ? 25000.00 : snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr, totalPlayers: snapshot.data!.total, cutoffTetrio: snapshot.data!.data[rank]!), + ), + ); },), ), ] diff --git a/lib/views/destination_graphs.dart b/lib/views/destination_graphs.dart index 422ca6d..c84b2dc 100644 --- a/lib/views/destination_graphs.dart +++ b/lib/views/destination_graphs.dart @@ -10,7 +10,6 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/destination_home.dart'; import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; @@ -45,6 +44,9 @@ class _DestinationGraphsState extends State { Stats _Ychart = Stats.tr; Stats _Xchart = Stats.tr; int _season = currentSeason-1; + List excludeRanks = []; + late Future> futureLeague = getTetraLeagueData(_Xchart, _Ychart); + String searchLeague = ""; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override @@ -169,18 +171,24 @@ class _DestinationGraphsState extends State { TetrioPlayersLeaderboard leaderboard = await teto.fetchTLLeaderboard(); List<_MyScatterSpot> _spots = [ for (TetrioPlayerFromLeaderboard entry in leaderboard.leaderboard) - _MyScatterSpot( + if (excludeRanks.indexOf(entry.rank) == -1) _MyScatterSpot( entry.getStatByEnum(x).toDouble(), entry.getStatByEnum(y).toDouble(), entry.userId, entry.username, entry.rank, - rankColors[entry.rank]??Colors.white + (rankColors[entry.rank]??Colors.white).withAlpha((searchLeague.isNotEmpty && entry.username.startsWith(searchLeague.toLowerCase())) ? 255 : 20) ) ]; return _spots; } + bool? getTotalFilterValue(){ + if (excludeRanks.isEmpty) return true; + if (excludeRanks.length == ranks.length) return false; + return null; + } + Widget getHistoryGraph(){ return FutureBuilder>>>( future: getHistoryData(fetchData), @@ -247,7 +255,7 @@ class _DestinationGraphsState extends State { Widget getLeagueState (){ return FutureBuilder>( - future: getTetraLeagueData(_Xchart, _Ychart), + future: futureLeague, builder: (context, snapshot) { switch (snapshot.connectionState){ case ConnectionState.none: @@ -340,6 +348,21 @@ class _DestinationGraphsState extends State { spacing: 20, crossAxisAlignment: WrapCrossAlignment.center, children: [ + if (_graph == Graph.leagueState) SizedBox( + width: 300, + child: TextField( + decoration: InputDecoration( + icon: Icon(Icons.search) + ), + onChanged: (v){ + searchLeague = v; + }, + onSubmitted: (v){ + searchLeague = v; + setState((){futureLeague = getTetraLeagueData(_Xchart, _Ychart);}); + }, + ) + ), if (_graph == Graph.history) Row( mainAxisSize: MainAxisSize.min, children: [ @@ -404,6 +427,53 @@ class _DestinationGraphsState extends State { Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) ], ), + if (_graph == Graph.leagueState) IconButton( + color: excludeRanks.isNotEmpty ? Theme.of(context).colorScheme.primary : null, + onPressed: (){ + showDialog(context: context, builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, StateSetter setAlertState) { + return AlertDialog( + title: Text("Filter ranks on graph", textAlign: TextAlign.center), + content: SingleChildScrollView( + child: Column( + children: [ + CheckboxListTile(value: getTotalFilterValue(), tristate: true, title: Text("All", style: TextStyle(fontFamily: "Eurostile Round Extended")), onChanged: (value){ + setAlertState( + (){ + if (excludeRanks.length*2 > ranks.length){ + excludeRanks.clear(); + }else{ + excludeRanks = List.of(ranks); + } + } + ); + }), + for(String rank in ranks.reversed) CheckboxListTile(value: excludeRanks.indexOf(rank) == -1, onChanged: (value){ + setAlertState( + (){ + if (excludeRanks.indexOf(rank) == -1){ + excludeRanks.add(rank); + }else{ + excludeRanks.remove(rank); + } + } + ); + }, title: Text(rank.toUpperCase()),) + ], + ), + ), + actions: [ + TextButton( + child: const Text("Apply"), + onPressed: () {Navigator.of(context).pop(); setState((){futureLeague = getTetraLeagueData(_Xchart, _Ychart);});} + ) + ] + ); + } + ); + }); + }, icon: Icon(Icons.filter_alt)), IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) ], ), diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 24d8f53..25bedf0 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -55,12 +55,13 @@ class FetchResults{ class RecordSummary extends StatelessWidget{ final RecordSingle? record; final bool hideRank; + final bool old; final bool? betterThanRankAverage; final MapEntry? closestAverage; final bool? betterThanClosestAverage; final String? rank; - const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, this.hideRank = false}); + const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.old = false, this.betterThanClosestAverage, this.rank, this.hideRank = false}); @override Widget build(BuildContext context) { @@ -85,13 +86,12 @@ class RecordSummary extends StatelessWidget{ "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", _ => record!.stats.score.toString() }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white, height: 0.9), + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: old ? Colors.grey : Colors.white, height: 0.9), ), ), RichText( textAlign: hideRank ? TextAlign.center : TextAlign.start, text: TextSpan( - text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ @@ -158,7 +158,7 @@ class LeagueCard extends StatelessWidget{ ) else Text("Tetra League", style: Theme.of(context).textTheme.titleSmall), const Divider(), - TLRatingThingy(userID: "", tlData: league, showPositions: true), + TLRatingThingy(userID: league.id, tlData: league, showPositions: true), const Divider(), RichText(text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.grey), @@ -229,7 +229,7 @@ class _DestinationHomeState extends State with SingleTickerProv const Divider(), RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), const Divider(), - Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) + Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "-.--"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "-.--"} KPP", style: const TextStyle(color: Colors.grey)) ], ), ), @@ -246,7 +246,7 @@ class _DestinationHomeState extends State with SingleTickerProv const Divider(), RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), const Divider(), - Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) + Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "-.--"} PPS", style: const TextStyle(color: Colors.grey)) ], ), ), @@ -266,7 +266,7 @@ class _DestinationHomeState extends State with SingleTickerProv children: [ Text("QP", style: Theme.of(context).textTheme.titleSmall), const Divider(), - RecordSummary(record: summaries.zenith, hideRank: true), + RecordSummary(record: summaries.zenith != null ? summaries.zenith : summaries.zenithCareerBest, hideRank: true, old: summaries.zenith == null), const Divider(), Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) ], @@ -283,7 +283,7 @@ class _DestinationHomeState extends State with SingleTickerProv children: [ Text("QP Expert", style: Theme.of(context).textTheme.titleSmall), const Divider(), - RecordSummary(record: summaries.zenithEx, hideRank: true,), + RecordSummary(record: summaries.zenithEx != null ? summaries.zenithEx : summaries.zenithExCareerBest, hideRank: true, old: summaries.zenith == null), const Divider(), Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) ], @@ -623,7 +623,7 @@ class _DestinationHomeState extends State with SingleTickerProv ); } - Widget getZenithCard(RecordSingle? record){ + Widget getZenithCard(RecordSingle? record, bool old){ return Column( children: [ Card( @@ -641,7 +641,7 @@ class _DestinationHomeState extends State with SingleTickerProv ), ), ), - ZenithThingy(zenith: record), + ZenithThingy(zenith: record, old: old), if (record != null) Row( children: [ Expanded( @@ -1084,17 +1084,17 @@ class _DestinationHomeState extends State with SingleTickerProv child: SlideTransition( position: _offsetAnimation, child: switch (rightCard){ - Cards.overview => getOverviewCard(snapshot.data!.summaries!, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : null), + Cards.overview => getOverviewCard(snapshot.data!.summaries!, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.percentileRank] : null), Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : null, snapshot.data!.states), + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.percentileRank] : null, snapshot.data!.states), CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), CardMod.records => getRecentTLrecords(widget.constraints), _ => const Center(child: Text("huh?")) }, Cards.quickPlay => switch (cardMod){ - CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), + CardMod.info => getZenithCard(snapshot.data?.summaries?.zenith != null ? snapshot.data!.summaries!.zenith : snapshot.data!.summaries?.zenithCareerBest, snapshot.data!.summaries?.zenith == null), CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), - CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), + CardMod.ex => getZenithCard(snapshot.data?.summaries?.zenithEx != null ? snapshot.data!.summaries!.zenithEx : snapshot.data!.summaries?.zenithExCareerBest, snapshot.data!.summaries?.zenithEx == null), CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), }, Cards.sprint => switch (cardMod){ diff --git a/lib/views/destination_settings.dart b/lib/views/destination_settings.dart new file mode 100644 index 0000000..8c2af2f --- /dev/null +++ b/lib/views/destination_settings.dart @@ -0,0 +1,551 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/utils/filesizes_converter.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; + +class DestinationSettings extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationSettings({super.key, required this.constraints}); + + @override + State createState() => _DestinationSettings(); +} + +enum SettingsCardMod{ + general("General"), + customization("Custonization"), + database("Local database"); + + const SettingsCardMod(this.title); + + final String title; +} + +const EdgeInsets descriptionPadding = EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 8.0); + +class _DestinationSettings extends State with SingleTickerProviderStateMixin { + SettingsCardMod mod = SettingsCardMod.general; + List> locales = >[]; + String defaultNickname = "Checking..."; + String defaultID = ""; + Color pickerColor = Colors.cyanAccent; + Color currentColor = Colors.cyanAccent; + late bool oskKagariGimmick; + late bool sheetbotRadarGraphs; + late int ratingMode; + late int timestampMode; + late bool showPositions; + late bool showAverages; + late bool updateInBG; + final TextEditingController _playertext = TextEditingController(); + late AnimationController _defaultNicknameAnimController; + late Animation _goodDefaultNicknameAnim; + late Animation _badDefaultNicknameAnim; + late Animation _defaultNicknameAnim = _goodDefaultNicknameAnim; + double helperTextOpacity = 0; + String helperText = "Press Enter to submit"; + + @override + void initState() { + // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ + // windowManager.getTitle().then((value) => oldWindowTitle = value); + // windowManager.setTitle("Tetra Stats: ${t.settings}"); + // } + _defaultNicknameAnimController = AnimationController( + value: 1.0, + duration: Durations.extralong4, + vsync: this, + ); + _goodDefaultNicknameAnim = new ColorTween( + begin: Colors.greenAccent, + end: Colors.grey, + ).animate(new CurvedAnimation( + parent: _defaultNicknameAnimController, + curve: Easing.emphasizedAccelerate, + //reverseCurve: Cubic(0,.99,.99,1.01) + ))..addStatusListener((status) { + if (status.index == 3) setState((){helperText = "Press Enter to submit"; helperTextOpacity = 0;}); + }); + _badDefaultNicknameAnim = new ColorTween( + begin: Colors.redAccent, + end: Colors.grey, + ).animate(new CurvedAnimation( + parent: _defaultNicknameAnimController, + curve: Easing.emphasizedAccelerate, + //reverseCurve: Cubic(0,.99,.99,1.01) + ))..addStatusListener((status) { + if (status.index == 3) setState((){helperText = "Press Enter to submit"; helperTextOpacity = 0;}); + }); + _getPreferences(); + super.initState(); + } + + @override + void dispose(){ + // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); + super.dispose(); + } + + void changeColor(Color color) { + setState(() => pickerColor = color); + } + + void _getPreferences() { + showPositions = prefs.getBool("showPositions") ?? false; + showAverages = prefs.getBool("showAverages") ?? true; + updateInBG = prefs.getBool("updateInBG") ?? false; + oskKagariGimmick = prefs.getBool("oskKagariGimmick") ?? true; + sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")?? false; + ratingMode = prefs.getInt("ratingMode") ?? 0; + timestampMode = prefs.getInt("timestampMode") ?? 0; + _setDefaultNickname(prefs.getString("player")??"").then((v){setState((){});}); + defaultID = prefs.getString("playerID")??""; + } + + Future _setDefaultNickname(String n) async { + if (n.isNotEmpty) { + try { + if (n.length > 16){ + defaultNickname = await teto.getNicknameByID(n); + await prefs.setString('playerID', n); + }else{ + TetrioPlayer player = await teto.fetchPlayer(n); + defaultNickname = player.username; + await prefs.setString('playerID', player.userId); + } + await prefs.setString('player', defaultNickname); + return true; + } catch (e) { + return false; + } + } else { + defaultNickname = "dan63"; + await prefs.setString('player', "dan63"); + await prefs.setString('playerID', "6098518e3d5155e6ec429cdc"); + return true; + } + //setState(() {}); + } + + Widget getGeneralSettings(){ + return Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text(SettingsCardMod.general.title, style: Theme.of(context).textTheme.titleLarge), + ], + ), + )), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Your account in TETR.IO", style: Theme.of(context).textTheme.displayLarge), + trailing: SizedBox(width: 150.0, child: AnimatedBuilder( + animation: _defaultNicknameAnim, + builder: (context, child) { + return Focus( + onFocusChange: (value) { + setState((){helperTextOpacity = ((value || helperText != "Press Enter to submit")) ? 1 : 0;}); + }, + child: TextField( + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: defaultNickname, + helper: AnimatedOpacity( + opacity: helperTextOpacity, + duration: Durations.long1, + curve: Easing.standardDecelerate, + child: Text(helperText, style: TextStyle(color: _defaultNicknameAnim.value, height: 0.2)) + ), + ), + onSubmitted: (value) { + helperText = "Checking..."; + _setDefaultNickname(value).then((v) { + _defaultNicknameAnim = v ? _goodDefaultNicknameAnim : _badDefaultNicknameAnim; + _defaultNicknameAnimController.forward(from: 0); + setState((){ helperText = v ? "Done!" : "Fuck";}); + }); + }, + ), + ); + }, + )), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("Stats of that player will be loaded initially right after launching this app. By default it loads my (dan63) stats. To change that, enter your nickname here."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Language", style: Theme.of(context).textTheme.displayLarge), + trailing: DropdownButton( + items: locales, + value: LocaleSettings.currentLocale, + onChanged: (value){ + LocaleSettings.setLocale(value!); + if(value.languageCode == Platform.localeName.substring(0, 2)){ + prefs.remove('locale'); + }else{ + prefs.setString('locale', value.languageCode); + } + }, + ), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("Tetra Stats was translated on ${locales.length} languages. By default, app will pick your system one or English, if locale of your system isn't avaliable."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Update data in the background", style: Theme.of(context).textTheme.displayLarge), + trailing: Switch(value: updateInBG, onChanged: (bool value){ + prefs.setBool("updateInBG", value); + setState(() { + updateInBG = value; + }); + }) + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("If on, Tetra Stats will attempt to retrieve new info once cache expires. Usually that happen every 5 minutes"), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Compare TL stats with rank averages", style: Theme.of(context).textTheme.displayLarge), + trailing: Switch(value: showAverages, onChanged: (bool value){ + prefs.setBool("showAverages", value); + setState(() { + showAverages = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("If on, Tetra Stats will provide additional metrics, which allow you to compare yourself with average player on your rank. The way you'll see it — stats will be highlited with corresponding color, hover over them with cursor for more info."), + ) + ], + ), + ), + Card( + surfaceTintColor: Colors.redAccent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Show position on leaderboard by stats", style: Theme.of(context).textTheme.displayLarge), + trailing: Switch(value: showPositions, onChanged: (bool value){ + prefs.setBool("showPositions", value); + setState(() { + showPositions = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("This can take some time (and traffic) to load, but will allow you to see your position on the leaderboard, sorted by a stat"), + ) + ], + ), + ) + ] + ); + } + + Widget getCustomizationSettings(){ + return Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text(SettingsCardMod.customization.title, style: Theme.of(context).textTheme.titleLarge), + ], + ), + )), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Accent color", style: Theme.of(context).textTheme.displayLarge), + trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25), + 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(() { + context.findAncestorStateOfType()?.setAccentColor(pickerColor); + prefs.setInt("accentColor", pickerColor.value); + }); + Navigator.of(context).pop(); + }, + ), + ])); + } + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("That color is seen across this app and usually highlites interactive UI elements."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text("Timestamps format", style: Theme.of(context).textTheme.displayLarge), + trailing: DropdownButton( + value: timestampMode, + items: [ + DropdownMenuItem(value: 0, child: Text(t.timestampsAbsoluteGMT)), + DropdownMenuItem(value: 1, child: Text(t.timestampsAbsoluteLocalTime)), + DropdownMenuItem(value: 2, child: Text(t.timestampsRelative)) + ], + onChanged: (dynamic value){ + prefs.setInt("timestampMode", value); + setState(() { + timestampMode = value; + }); + }, + ), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("You can choose, in which way timestamps shows time. By default, they show time in GMT timezone, formatted according to chosen locale, example: ${DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19))}."), + ), + Padding( + padding: descriptionPadding, + child: Text("There is also:\n• Locale formatted in your timezone: ${DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19).toLocal())}\n• Relative timestamp: ${relativeDateTime(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19))}"), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Sheetbot-like behavior for radar graphs", style: Theme.of(context).textTheme.displayLarge), + trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){ + prefs.setBool("sheetbotRadarGraphs", value); + setState(() { + sheetbotRadarGraphs = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("Altough it was considered by me, that the way graphs work in SheetBot is not very correct, some people were confused to see, that -0.5 stride dosen't look the way it looks on SheetBot graph. Hence, he we are: if this toggle is on, points on the graphs can appear on the opposite half of the graph if value is negative."), + ) + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Osk-Kagari gimmick", style: Theme.of(context).textTheme.displayLarge), + trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){ + prefs.setBool("oskKagariGimmick", value); + setState(() { + oskKagariGimmick = value; + }); + }), + ), + Divider(), + Padding( + padding: descriptionPadding, + child: Text("If on, instead of osk's rank, :kagari: will be rendered."), + ) + ], + ), + ) + ], + ); + } + + Widget getDatabaseSettings(){ + return Column( + children: [ + Card( + child: Center(child: Column( + children: [ + Text(SettingsCardMod.database.title, style: Theme.of(context).textTheme.titleLarge), + Divider(), + FutureBuilder<(int, int, int)>(future: teto.getDatabaseData(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return RichText( + text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white), + children: [ + TextSpan(text: "${bytesToSize(snapshot.data!.$1)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + TextSpan(text: "of data stored\n"), + TextSpan(text: "${intf.format(snapshot.data!.$2)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + TextSpan(text: "Tetra League records saved\n"), + TextSpan(text: "${intf.format(snapshot.data!.$3)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + TextSpan(text: "Tetra League playerstates saved"), + ] + ) + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + } + ), + Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: (){teto.removeDuplicatesFromTLMatches().then((_) => setState((){}));}, + icon: const Icon(Icons.build), + label: Text("Fix"), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0))))) + ) + ), + Expanded( + child: ElevatedButton.icon( + onPressed: (){teto.compressDB().then((_) => setState((){}));}, + icon: const Icon(Icons.compress), + label: Text("Compress"), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))) + ) + ) + ], + ) + ], + )), + ), + Card( + child: ListTile( + title: Text("Export Database", style: Theme.of(context).textTheme.displayLarge), + ), + ), + Card( + child: ListTile( + title: Text("Import Database", style: Theme.of(context).textTheme.displayLarge), + ), + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + if (locales.isEmpty) for (var v in AppLocale.values){ + locales.add(DropdownMenuItem( + value: v, child: Text(t.locales[v.languageTag]!))); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Settings", style: Theme.of(context).textTheme.headlineMedium), + Spacer() + ], + ), + ), + for (SettingsCardMod m in SettingsCardMod.values) Card( + child: ListTile( + title: Text(m.title), + trailing: Icon(Icons.arrow_right, color: mod == m ? Colors.white : Colors.grey), + onTap: () { + setState(() { + mod = m; + }); + }, + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: SingleChildScrollView( + child: switch (mod){ + SettingsCardMod.general => getGeneralSettings(), + SettingsCardMod.customization => getCustomizationSettings(), + SettingsCardMod.database => getDatabaseSettings(), + }, + ) + ) + ], + ); + } +} diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index e463641..a60e0e9 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -38,6 +38,7 @@ import 'package:tetra_stats/views/destination_graphs.dart'; import 'package:tetra_stats/views/destination_home.dart'; import 'package:tetra_stats/views/destination_leaderboards.dart'; import 'package:tetra_stats/views/destination_saved_data.dart'; +import 'package:tetra_stats/views/destination_settings.dart'; import 'package:tetra_stats/views/tl_match_view.dart'; import 'package:tetra_stats/views/compare_view_tiles.dart'; import 'package:tetra_stats/widgets/graphs.dart'; @@ -103,7 +104,6 @@ class MainView extends StatefulWidget { State createState() => _MainState(); } -enum Page {home, leaderboards, leagueAverages, calculator, settings} enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} enum CardMod {info, records, ex, exRecords} Map cardsTitles = { @@ -215,6 +215,7 @@ class _MainState extends State with TickerProviderStateMixin { 2 => DestinationLeaderboards(constraints: constraints), 3 => DestinationCutoffs(constraints: constraints), 4 => DestinationCalculator(constraints: constraints), + 5 => DestinationInfo(constraints: constraints), 6 => DestinationSavedData(constraints: constraints), 7 => DestinationSettings(constraints: constraints), _ => Text("Unknown destination $destination") @@ -226,509 +227,81 @@ class _MainState extends State with TickerProviderStateMixin { } } -class DestinationSettings extends StatefulWidget{ +class DestinationInfo extends StatefulWidget{ final BoxConstraints constraints; - const DestinationSettings({super.key, required this.constraints}); + const DestinationInfo({super.key, required this.constraints}); @override - State createState() => _DestinationSettings(); + State createState() => _DestinationInfo(); } -enum SettingsCardMod{ - general("General"), - customization("Custonization"), - database("Local database"); - - const SettingsCardMod(this.title); - +class InfoCard extends StatelessWidget { + final double height; + final String assetLink; final String title; -} + final String description; -const EdgeInsets descriptionPadding = EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 8.0); - -class _DestinationSettings extends State with SingleTickerProviderStateMixin { - SettingsCardMod mod = SettingsCardMod.general; - List> locales = >[]; - String defaultNickname = "Checking..."; - String defaultID = ""; - late bool oskKagariGimmick; - late bool sheetbotRadarGraphs; - late int ratingMode; - late int timestampMode; - late bool showPositions; - late bool showAverages; - late bool updateInBG; - final TextEditingController _playertext = TextEditingController(); - late AnimationController _defaultNicknameAnimController; - late Animation _goodDefaultNicknameAnim; - late Animation _badDefaultNicknameAnim; - late Animation _defaultNicknameAnim = _goodDefaultNicknameAnim; - double helperTextOpacity = 0; - String helperText = "Press Enter to submit"; - - @override - void initState() { - // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - // windowManager.getTitle().then((value) => oldWindowTitle = value); - // windowManager.setTitle("Tetra Stats: ${t.settings}"); - // } - _defaultNicknameAnimController = AnimationController( - value: 1.0, - duration: Durations.extralong4, - vsync: this, - ); - _goodDefaultNicknameAnim = new ColorTween( - begin: Colors.greenAccent, - end: Colors.grey, - ).animate(new CurvedAnimation( - parent: _defaultNicknameAnimController, - curve: Easing.emphasizedAccelerate, - //reverseCurve: Cubic(0,.99,.99,1.01) - ))..addStatusListener((status) { - if (status.index == 3) setState((){helperText = "Press Enter to submit"; helperTextOpacity = 0;}); - }); - _badDefaultNicknameAnim = new ColorTween( - begin: Colors.redAccent, - end: Colors.grey, - ).animate(new CurvedAnimation( - parent: _defaultNicknameAnimController, - curve: Easing.emphasizedAccelerate, - //reverseCurve: Cubic(0,.99,.99,1.01) - ))..addStatusListener((status) { - if (status.index == 3) setState((){helperText = "Press Enter to submit"; helperTextOpacity = 0;}); - }); - _getPreferences(); - super.initState(); - } - - @override - void dispose(){ - // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - void _getPreferences() { - showPositions = prefs.getBool("showPositions") ?? false; - showAverages = prefs.getBool("showAverages") ?? true; - updateInBG = prefs.getBool("updateInBG") ?? false; - oskKagariGimmick = prefs.getBool("oskKagariGimmick") ?? true; - sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")?? false; - ratingMode = prefs.getInt("ratingMode") ?? 0; - timestampMode = prefs.getInt("timestampMode") ?? 0; - _setDefaultNickname(prefs.getString("player")??"").then((v){setState((){});}); - defaultID = prefs.getString("playerID")??""; - } - - Future _setDefaultNickname(String n) async { - if (n.isNotEmpty) { - try { - if (n.length > 16){ - defaultNickname = await teto.getNicknameByID(n); - await prefs.setString('playerID', n); - }else{ - TetrioPlayer player = await teto.fetchPlayer(n); - defaultNickname = player.username; - await prefs.setString('playerID', player.userId); - } - await prefs.setString('player', defaultNickname); - return true; - } catch (e) { - return false; - } - } else { - defaultNickname = "dan63"; - await prefs.setString('player', "dan63"); - await prefs.setString('playerID', "6098518e3d5155e6ec429cdc"); - return true; - } - //setState(() {}); - } - - Widget getGeneralSettings(){ - return Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text(SettingsCardMod.general.title, style: Theme.of(context).textTheme.titleLarge), - ], - ), - )), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Your account in TETR.IO", style: Theme.of(context).textTheme.displayLarge), - trailing: SizedBox(width: 150.0, child: AnimatedBuilder( - animation: _defaultNicknameAnim, - builder: (context, child) { - return Focus( - onFocusChange: (value) { - setState((){helperTextOpacity = ((value || helperText != "Press Enter to submit")) ? 1 : 0;}); - }, - child: TextField( - keyboardType: TextInputType.text, - decoration: InputDecoration( - hintText: defaultNickname, - helper: AnimatedOpacity( - opacity: helperTextOpacity, - duration: Durations.long1, - curve: Easing.standardDecelerate, - child: Text(helperText, style: TextStyle(color: _defaultNicknameAnim.value, height: 0.2)) - ), - ), - onSubmitted: (value) { - helperText = "Checking..."; - _setDefaultNickname(value).then((v) { - _defaultNicknameAnim = v ? _goodDefaultNicknameAnim : _badDefaultNicknameAnim; - _defaultNicknameAnimController.forward(from: 0); - setState((){ helperText = v ? "Done!" : "Fuck";}); - }); - }, - ), - ); - }, - )), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("Stats of that player will be loaded initially right after launching this app. By default it loads my (dan63) stats. To change that, enter your nickname here."), - ) - ], - ), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Language", style: Theme.of(context).textTheme.displayLarge), - trailing: DropdownButton( - items: locales, - value: LocaleSettings.currentLocale, - onChanged: (value){ - LocaleSettings.setLocale(value!); - if(value.languageCode == Platform.localeName.substring(0, 2)){ - prefs.remove('locale'); - }else{ - prefs.setString('locale', value.languageCode); - } - }, - ), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("Tetra Stats was translated on ${locales.length} languages. By default, app will pick your system one or English, if locale of your system isn't avaliable."), - ) - ], - ), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Update data in the background", style: Theme.of(context).textTheme.displayLarge), - trailing: Switch(value: updateInBG, onChanged: (bool value){ - prefs.setBool("updateInBG", value); - setState(() { - updateInBG = value; - }); - }) - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("If on, Tetra Stats will attempt to retrieve new info once cache expires. Usually that happen every 5 minutes"), - ) - ], - ), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Compare TL stats with rank averages", style: Theme.of(context).textTheme.displayLarge), - trailing: Switch(value: showAverages, onChanged: (bool value){ - prefs.setBool("showAverages", value); - setState(() { - showAverages = value; - }); - }), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("If on, Tetra Stats will provide additional metrics, which allow you to compare yourself with average player on your rank. The way you'll see it — stats will be highlited with corresponding color, hover over them with cursor for more info."), - ) - ], - ), - ), - Card( - surfaceTintColor: Colors.redAccent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Show position on leaderboard by stats", style: Theme.of(context).textTheme.displayLarge), - trailing: Switch(value: showPositions, onChanged: (bool value){ - prefs.setBool("showPositions", value); - setState(() { - showPositions = value; - }); - }), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("This can take some time (and traffic) to load, but will allow you to see your position on the leaderboard, sorted by a stat"), - ) - ], - ), - ) - ] - ); - } - - Widget getCustomizationSettings(){ - return Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text(SettingsCardMod.customization.title, style: Theme.of(context).textTheme.titleLarge), - ], - ), - )), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Accent color", style: Theme.of(context).textTheme.displayLarge), - trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("That color is seen across this app and usually highlites interactive UI elements."), - ) - ], - ), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - title: Text("Timestamps format", style: Theme.of(context).textTheme.displayLarge), - trailing: DropdownButton( - value: timestampMode, - items: [ - DropdownMenuItem(value: 0, child: Text(t.timestampsAbsoluteGMT)), - DropdownMenuItem(value: 1, child: Text(t.timestampsAbsoluteLocalTime)), - DropdownMenuItem(value: 2, child: Text(t.timestampsRelative)) - ], - onChanged: (dynamic value){ - prefs.setInt("timestampMode", value); - setState(() { - timestampMode = value; - }); - }, - ), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("You can choose, in which way timestamps shows time. By default, they show time in GMT timezone, formatted according to chosen locale, example: ${DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19))}."), - ), - Padding( - padding: descriptionPadding, - child: Text("There is also:\n• Locale formatted in your timezone: ${DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19).toLocal())}\n• Relative timestamp: ${relativeDateTime(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19))}"), - ) - ], - ), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Sheetbot-like behavior for radar graphs", style: Theme.of(context).textTheme.displayLarge), - trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){ - prefs.setBool("sheetbotRadarGraphs", value); - setState(() { - sheetbotRadarGraphs = value; - }); - }), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("Altough it was considered by me, that the way graphs work in SheetBot is not very correct, some people were confused to see, that -0.5 stride dosen't look the way it looks on SheetBot graph. Hence, he we are: if this toggle is on, points on the graphs can appear on the opposite half of the graph if value is negative."), - ) - ], - ), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Osk-Kagari gimmick", style: Theme.of(context).textTheme.displayLarge), - trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){ - prefs.setBool("oskKagariGimmick", value); - setState(() { - oskKagariGimmick = value; - }); - }), - ), - Divider(), - Padding( - padding: descriptionPadding, - child: Text("If on, instead of osk's rank, :kagari: will be rendered."), - ) - ], - ), - ) - ], - ); - } - - Widget getDatabaseSettings(){ - return Column( - children: [ - Card( - child: Center(child: Column( - children: [ - Text(SettingsCardMod.database.title, style: Theme.of(context).textTheme.titleLarge), - Divider(), - FutureBuilder<(int, int, int)>(future: teto.getDatabaseData(), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.hasData){ - return RichText( - text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white), - children: [ - TextSpan(text: "${bytesToSize(snapshot.data!.$1)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - TextSpan(text: "of data stored\n"), - TextSpan(text: "${intf.format(snapshot.data!.$2)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - TextSpan(text: "Tetra League records saved\n"), - TextSpan(text: "${intf.format(snapshot.data!.$3)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - TextSpan(text: "Tetra League playerstates saved"), - ] - ) - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return Text("huh?"); - } - ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: (){teto.removeDuplicatesFromTLMatches().then((_) => setState((){}));}, - icon: const Icon(Icons.build), - label: Text("Fix"), - style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0))))) - ) - ), - Expanded( - child: ElevatedButton.icon( - onPressed: (){teto.compressDB().then((_) => setState((){}));}, - icon: const Icon(Icons.compress), - label: Text("Compress"), - style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))) - ) - ) - ], - ) - ], - )), - ), - Card( - child: ListTile( - title: Text("Export Database", style: Theme.of(context).textTheme.displayLarge), - ), - ), - Card( - child: ListTile( - title: Text("Import Database", style: Theme.of(context).textTheme.displayLarge), - ), - ) - ], - ); - } + const InfoCard({required this.height, required this.assetLink, required this.title, required this.description}); @override Widget build(BuildContext context) { - final t = Translations.of(context); - if (locales.isEmpty) for (var v in AppLocale.values){ - locales.add(DropdownMenuItem( - value: v, child: Text(t.locales[v.languageTag]!))); - } - return Row( - crossAxisAlignment: CrossAxisAlignment.start, + return Card( + clipBehavior: Clip.hardEdge, + child: SizedBox( + width: 450, + height: height, + child: Column( + children: [ + Image.asset("res/images/Снимок экрана_2023-11-06_01-00-50.png", fit: BoxFit.cover, height: 300.0), + Text(title, style: Theme.of(context).textTheme.titleLarge), + Padding( + padding: const EdgeInsets.all(12.0), + child: Text(description), + ), + Spacer() + ], + ), + ), + ); + } + +} + +class _DestinationInfo extends State { + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: 450, - child: Column( + Card( + child: Center(child: Text("Information Center", style: Theme.of(context).textTheme.titleLarge)), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( children: [ - Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Text("Settings", style: Theme.of(context).textTheme.headlineMedium), - Spacer() - ], - ), + InfoCard( + height: widget.constraints.maxHeight - 77, + assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", + title: "Shizuru!", + description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru " ), - for (SettingsCardMod m in SettingsCardMod.values) Card( - child: ListTile( - title: Text(m.title), - trailing: Icon(Icons.arrow_right, color: mod == m ? Colors.white : Colors.grey), - onTap: () { - setState(() { - mod = m; - }); - }, - ), - ) + InfoCard( + height: widget.constraints.maxHeight - 77, + assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", + title: "Shizuru!", + description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru " + ), + InfoCard( + height: widget.constraints.maxHeight - 77, + assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", + title: "Shizuru!", + description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru " + ), + Card() ], ), - ), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: SingleChildScrollView( - child: switch (mod){ - SettingsCardMod.general => getGeneralSettings(), - SettingsCardMod.customization => getCustomizationSettings(), - SettingsCardMod.database => getDatabaseSettings(), - }, - ) ) ], ); @@ -1377,24 +950,32 @@ class _NewUserThingyState extends State with SingleTickerProvider }), const TextSpan(text:"\n"), TextSpan(text: widget.player.gameTime.isNegative ? "-h --m" : playtime(widget.player.gameTime), style: TextStyle(color: widget.player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: widget.player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !widget.player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ + Duration accountAge = DateTime.timestamp().difference(widget.player.registrationTime); + Duration avgGametimeADay = Duration(microseconds: (widget.player.gameTime.inMicroseconds / accountAge.inDays).floor()); showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: Text(t.exactGametime, textAlign: TextAlign.center), content: SingleChildScrollView( - child: ListBody(children: [ - Text( - "${intf.format(widget.player.gameTime.inDays)}d ${nonsecs.format(widget.player.gameTime.inHours%24)}h ${nonsecs.format(widget.player.gameTime.inMinutes%60)}m ${nonsecs.format(widget.player.gameTime.inSeconds%60)}s ${nonsecs3.format(widget.player.gameTime.inMilliseconds%1000)}ms ${nonsecs3.format(widget.player.gameTime.inMicroseconds%1000)}μs", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24) - ), + child: Column( + children: [ + RichText(text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white, fontSize: 28), + children: [ + TextSpan(text: "${intf.format(widget.player.gameTime.inHours)}"), + TextSpan(text: ":${nonsecs.format(widget.player.gameTime.inMinutes%60)}:${nonsecs.format(widget.player.gameTime.inSeconds%60)}"), + TextSpan(text: ".${nonsecs3.format(widget.player.gameTime.inMicroseconds%1000000)}", style: TextStyle(fontSize: 14)) + ] + )), + Text("${playtime(avgGametimeADay)} a day in average"), Padding( padding: const EdgeInsets.only(top: 8.0), child: Text("It's ${f4.format(widget.player.gameTime.inSeconds/31536000)} years,"), ), - Text("${f4.format(widget.player.gameTime.inSeconds/2628000)} monts,"), - Text("${f4.format(widget.player.gameTime.inSeconds/3600)} hours,"), - Text("${f2.format(widget.player.gameTime.inMilliseconds/60000)} minutes,"), - Text("${intf.format(widget.player.gameTime.inSeconds)} seconds"), + Text("or ${f4.format(widget.player.gameTime.inSeconds/2628000)} months,"), + Text("or ${f4.format(widget.player.gameTime.inSeconds/86400)} days,"), + Text("or ${f2.format(widget.player.gameTime.inMilliseconds/60000)} minutes,"), + Text("or ${intf.format(widget.player.gameTime.inSeconds)} seconds"), ] ), ), @@ -1584,8 +1165,8 @@ class TetraLeagueThingy extends StatelessWidget{ //surfaceTintColor: rankColors[league.rank], child: Column( children: [ - TLRatingThingy(userID: "w", tlData: league, oldTl: toCompare, showPositions: true), - TLProgress( + TLRatingThingy(userID: league.id, tlData: league, oldTl: toCompare, showPositions: true), + if (league.gamesPlayed > 9) TLProgress( tlData: league, previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, nextRankTRcutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.tr[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, @@ -1605,62 +1186,25 @@ class TetraLeagueThingy extends StatelessWidget{ defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ - Text(f2.format(league.apm??0.00), textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : null)), - Text(" APM", style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : null)), + Text(league.apm != null ? f2.format(league.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)), + Text(" APM", style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)), if (toCompare != null) Text(" (${comparef2.format(league.apm!-toCompare!.apm!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.apm!-toCompare!.apm!))) ]), TableRow(children: [ - Text(f2.format(league.pps??0.00), textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : null)), - Text(" PPS", style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : null)), + Text(league.pps != null ? f2.format(league.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)), + Text(" PPS", style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)), if (toCompare != null) Text(" (${comparef2.format(league.pps!-toCompare!.pps!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.pps!-toCompare!.pps!))) ]), TableRow(children: [ - Text(f2.format(league.vs??0.00), textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : null)), - Text(" VS", style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : null)), + Text(league.vs != null ? f2.format(league.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)), + Text(" VS", style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)), if (toCompare != null) Text(" (${comparef2.format(league.vs!-toCompare!.vs!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.vs!-toCompare!.vs!))) ]) ], ), ), ), - SizedBox( - height: 128.0, - width: 128.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: SfRadialGauge( - backgroundColor: Colors.black, - axes: [ - RadialAxis( - minimum: 0.0, - maximum: 1.0, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.25, - minorTicksPerInterval: 0, - ranges:[ - GaugeRange(startValue: 0, endValue: league.winrate, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - Text(percentage.format(league.winrate), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), - angle: 90,positionFactor: 0.1 - ), - GaugeAnnotation(widget: Container(child: - Text(t.statCellNum.winrate, textAlign: TextAlign.center)), - angle: 270,positionFactor: 0.4 - ), - if (toCompare != null) GaugeAnnotation(widget: Container(child: - Text(comparef2.format((league.winrate-toCompare!.winrate)*100), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(league.winrate-toCompare!.winrate)))), - angle: 90,positionFactor: 0.45 - ) - ], - ) - ] - ), - ), - ), + GaugetThingy(value: league.winrate, min: 0, max: 1, tickInterval: 0.25, label: "Winrate", sideSize: 128, fractionDigits: 2, moreIsBetter: true, oldValue: toCompare?.winrate, percentileFormat: true), Expanded( child: Center( child: Table( @@ -1846,7 +1390,8 @@ class GraphsThingy extends StatelessWidget{ } class GaugetThingy extends StatelessWidget{ - final double value; + final double? value; + final String? subString; final double min; final double max; final double? oldValue; @@ -1855,9 +1400,10 @@ class GaugetThingy extends StatelessWidget{ final double tickInterval; final String label; final double sideSize; + final bool percentileFormat; final int fractionDigits; - GaugetThingy({super.key, required this.value, required this.min, required this.max, this.oldValue, this.avgValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter}); + const GaugetThingy({super.key, required this.value, this.subString, required this.min, required this.max, this.oldValue, this.avgValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter, this.percentileFormat = false}); @override Widget build(BuildContext context) { @@ -1877,21 +1423,25 @@ class GaugetThingy extends StatelessWidget{ showTicks: true, showLabels: false, interval: tickInterval, - //labelsPosition: ElementsPosition.outside, + minorTicksPerInterval: 0, ranges:[ - GaugeRange(startValue: 0, endValue: value, color: theme.colorScheme.primary) + GaugeRange(startValue: 0, endValue: (value != null && !value!.isNaN) ? value! : 0, color: theme.colorScheme.primary) ], annotations: [ GaugeAnnotation(widget: Container(child: - Text(f.format(value), textAlign: TextAlign.center, style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: getStatColor(value, avgValue, moreIsBetter)))), + Text((value != null && !value!.isNaN) ? percentileFormat ? percentage.format(value) : f.format(value) : "---", textAlign: TextAlign.center, style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: (value != null && !value!.isNaN) ? getStatColor(value!, avgValue, moreIsBetter) : Colors.grey))), angle: 90,positionFactor: 0.10 ), GaugeAnnotation(widget: Container(child: - Text(label, textAlign: TextAlign.center, style: const TextStyle(height: .9))), - angle: 270,positionFactor: 0.4 + Text(label, textAlign: TextAlign.center, style: TextStyle(height: .9, color: (value != null && !value!.isNaN) ? null : Colors.grey))), + angle: 270,positionFactor: 0.3, verticalAlignment: GaugeAlignment.far, ), - if (oldValue != null) GaugeAnnotation(widget: Container(child: - Text(comparef2.format(value-oldValue!), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(moreIsBetter ? value-oldValue! : oldValue!-value)))), + if (oldValue != null && (value != null && !value!.isNaN)) GaugeAnnotation(widget: Container(child: + Text(comparef2.format(percentileFormat ? (value!-oldValue!) * 100 : value!-oldValue!), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(moreIsBetter ? value!-oldValue! : oldValue!-value!)))), + angle: 90,positionFactor: 0.45 + ), + if (subString != null) GaugeAnnotation(widget: Container(child: + Text(subString!, textAlign: TextAlign.center, style: TextStyle(color: (value != null && !value!.isNaN) ? null : Colors.grey))), angle: 90,positionFactor: 0.45 ) ], @@ -1905,8 +1455,9 @@ class GaugetThingy extends StatelessWidget{ class ZenithThingy extends StatelessWidget{ final RecordSingle? zenith; + final bool old; - const ZenithThingy({super.key, required this.zenith}); + const ZenithThingy({super.key, required this.zenith, this.old = false}); @override Widget build(BuildContext context) { @@ -1924,7 +1475,7 @@ class ZenithThingy extends StatelessWidget{ RichText( text: TextSpan( text: zenith != null ? "${f2.format(zenith!.stats.zenith!.altitude)} m" : "--- m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: zenith != null ? Colors.white : Colors.grey), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: (zenith != null && !old) ? Colors.white : Colors.grey), ), ), if (zenith != null) RichText( @@ -1954,21 +1505,22 @@ class ZenithThingy extends StatelessWidget{ defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ - const Text("APM: ", style: TextStyle(fontSize: 21)), Text(f2.format(zenith!.aggregateStats.apm), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" APM", style: TextStyle(fontSize: 21)), ]), TableRow(children: [ - const Text("PPS: ", style: TextStyle(fontSize: 21)), Text(f2.format(zenith!.aggregateStats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" PPS", style: TextStyle(fontSize: 21)), ]), TableRow(children: [ - const Text("VS: ", style: TextStyle(fontSize: 21)), Text(f2.format(zenith!.aggregateStats.vs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" VS", style: TextStyle(fontSize: 21)), ]) ], ), ), ), + GaugetThingy(value: zenith!.stats.cps, min: 0, max: 12, tickInterval: 3, label: "Climb\nSpeed", subString: "Peak: ${f2.format(zenith!.stats.zenith!.peakrank)}", sideSize: 128, fractionDigits: 2, moreIsBetter: true), Expanded( child: Center( child: Table( @@ -1979,17 +1531,63 @@ class ZenithThingy extends StatelessWidget{ const Text(" KO's", style: TextStyle(fontSize: 21)) ]), TableRow(children: [ - Text(f2.format(zenith!.stats.cps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" CPS", style: TextStyle(fontSize: 21)) + Text(zenith!.stats.topBtB.toString(), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" B2B", style: TextStyle(fontSize: 21)) ]), TableRow(children: [ - Text(f2.format(zenith!.stats.zenith!.peakrank), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Peak CPS", style: TextStyle(fontSize: 21)) + Text(zenith!.stats.zenith!.floor.toString(), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Floor", style: TextStyle(fontSize: 21)) ]) ], ), ), - ), + ) + ], + ) else Row( + children: [ + Expanded( + child: Center( + child: Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: [ + const TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" APM", style: TextStyle(fontSize: 21, color: Colors.grey)), + ]), + const TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" PPS", style: TextStyle(fontSize: 21, color: Colors.grey)), + ]), + const TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" VS", style: TextStyle(fontSize: 21, color: Colors.grey)), + ]) + ], + ), + ), + ), + GaugetThingy(value: null, min: 0, max: 12, tickInterval: 3, label: "Climb\nSpeed", subString: "Peak: ---", sideSize: 128, fractionDigits: 0, moreIsBetter: true), + Expanded( + child: Center( + child: Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: [ + const TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" KO's", style: TextStyle(fontSize: 21, color: Colors.grey)) + ]), + const TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" B2B", style: TextStyle(fontSize: 21, color: Colors.grey)) + ]), + const TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" Floor", style: TextStyle(fontSize: 21, color: Colors.grey)) + ]) + ], + ), + ), + ) ], ) ] @@ -2247,7 +1845,7 @@ class ErrorThingy extends StatelessWidget{ final FetchResults? data; final String? eText; - ErrorThingy({this.data, this.eText}); + const ErrorThingy({this.data, this.eText}); @override Widget build(BuildContext context) { diff --git a/lib/views/rank_view.dart b/lib/views/rank_view.dart new file mode 100644 index 0000000..b129acc --- /dev/null +++ b/lib/views/rank_view.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; + +class RankView extends StatefulWidget { + final String rank; + final double nextRankTR; + final double nextRankPercentile; + final double nextRankTargetTR; + final int totalPlayers; + final CutoffTetrio cutoffTetrio; + + const RankView({super.key, required this.rank, required this.nextRankTR, required this.nextRankPercentile, required this.nextRankTargetTR, required this.totalPlayers, required this.cutoffTetrio}); + + @override + State createState() => _RankState(); +} + +enum CardMod{ + graph, + minimums, + maximums +} + +class _RankState extends State { + CardMod cardMod = CardMod.graph; + + @override + Widget build(BuildContext context) { + double percentileGap = widget.cutoffTetrio.percentile - widget.nextRankPercentile; + int supposedToBePlayers = (widget.totalPlayers * percentileGap).floor(); + return Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + floatingActionButton: Padding( + padding: const EdgeInsets.fromLTRB(8.0, 4.0, 0.0, 0.0), + child: FloatingActionButton( + onPressed: () => Navigator.pop(context), + tooltip: 'Fuck go back', + child: const Icon(Icons.arrow_back), + ), + ), + body: SafeArea( + child: LayoutBuilder(builder: (context, constraints) { + return Row( + children: [ + SizedBox( + width: 350.0, + height: constraints.maxHeight, + child: SingleChildScrollView( + child: Column( + children: [ + Card(child: Center(child: Padding( + padding: const EdgeInsets.fromLTRB(0.0, 8.0, 5.0, 10.0), + child: Text("${widget.rank.toUpperCase()} rank data", style: TextStyle(fontSize: 28)), + ))), + Card( + child: Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset("res/tetrio_tl_alpha_ranks/${widget.rank}.png",fit: BoxFit.fitHeight,height: 128), + Text("${intf.format(widget.cutoffTetrio.count)} players", style: Theme.of(context).textTheme.titleSmall,), + ], + ), + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("TR range", style: Theme.of(context).textTheme.displayLarge), + Text("${f2.format(widget.cutoffTetrio.tr)} — ${f2.format(widget.nextRankTR)}", style: Theme.of(context).textTheme.displayLarge) + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Spacer(), + Text("(${f2.format(widget.nextRankTR - widget.cutoffTetrio.tr)} TR gap)", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14)) + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Supposed to be", style: Theme.of(context).textTheme.displayLarge), + Text("${intf.format(widget.cutoffTetrio.targetTr)} — ${intf.format(widget.nextRankTargetTR)}", style: Theme.of(context).textTheme.displayLarge) + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Spacer(), + Text("(${intf.format(widget.nextRankTargetTR - widget.cutoffTetrio.targetTr)} TR gap)", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14)) + ], + ), + ), + if (widget.nextRankTargetTR < widget.nextRankTR) Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Inflation gap", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.redAccent)), + Text("${f2.format(widget.nextRankTR - widget.nextRankTargetTR)} TR", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.redAccent)) + ], + ), + if (widget.cutoffTetrio.tr < widget.cutoffTetrio.targetTr) Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Deflation gap", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.greenAccent)), + Text("${f2.format(widget.cutoffTetrio.targetTr - widget.cutoffTetrio.tr)} TR", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.greenAccent)) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("LB pos range", style: Theme.of(context).textTheme.displayLarge), + Text("${percentage.format(widget.cutoffTetrio.percentile)} — ${percentage.format(widget.nextRankPercentile)}", style: Theme.of(context).textTheme.displayLarge) + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Spacer(), + Text("(${percentage.format(percentileGap)} gap)", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14)) + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Supposed to be", style: Theme.of(context).textTheme.displayLarge), + Text("${intf.format(supposedToBePlayers)} players", style: Theme.of(context).textTheme.displayLarge) + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Spacer(), + if (widget.cutoffTetrio.count > supposedToBePlayers) Text("(overpopulated by a ${intf.format(widget.cutoffTetrio.count - supposedToBePlayers)} players)", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14)) + else if (widget.cutoffTetrio.count < supposedToBePlayers) Text("(underpopulated by a ${intf.format(supposedToBePlayers - widget.cutoffTetrio.count)} players)", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14)) + else Text("(cute)", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14)) + ], + ), + ), + Divider(), + Text("Average Stats", style: Theme.of(context).textTheme.displayLarge), + Text("${f2.format(widget.cutoffTetrio.apm)} APM • ${f2.format(widget.cutoffTetrio.pps)} PPS • ${f2.format(widget.cutoffTetrio.vs)} VS", style: Theme.of(context).textTheme.displayLarge), + Divider(), + Center(child: Text("Average Nerd Stats", style: Theme.of(context).textTheme.displayLarge)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Attack Per Piece", style: Theme.of(context).textTheme.displayLarge), + Text(f3.format(widget.cutoffTetrio.nerdStats?.app), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("VS / APM", style: Theme.of(context).textTheme.displayLarge), + Text(f3.format(widget.cutoffTetrio.nerdStats?.vsapm), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Downstack Per Second", style: Theme.of(context).textTheme.displayLarge), + Text(f3.format(widget.cutoffTetrio.nerdStats?.dss), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Downstack Per Piece", style: Theme.of(context).textTheme.displayLarge), + Text(f3.format(widget.cutoffTetrio.nerdStats?.dsp), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("APP + DSP", style: Theme.of(context).textTheme.displayLarge), + Text(f3.format(widget.cutoffTetrio.nerdStats?.appdsp), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Cheese Index", style: Theme.of(context).textTheme.displayLarge), + Text(f2.format(widget.cutoffTetrio.nerdStats?.cheese), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Garbage Efficiency", style: Theme.of(context).textTheme.displayLarge), + Text(f3.format(widget.cutoffTetrio.nerdStats?.gbe), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Weighted APP", style: Theme.of(context).textTheme.displayLarge), + Text(f3.format(widget.cutoffTetrio.nerdStats?.nyaapp), style: Theme.of(context).textTheme.displayLarge) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Area", style: Theme.of(context).textTheme.displayLarge), + Text(f1.format(widget.cutoffTetrio.nerdStats?.area), style: Theme.of(context).textTheme.displayLarge) + ], + ), + + ], + ), + ), + ) + ], + ), + ), + ), + SizedBox( + width: constraints.maxWidth - 350, + height: constraints.maxHeight, + child: Column( + children: [ + SegmentedButton( + showSelectedIcon: false, + selected: {cardMod}, + segments: >[ + ButtonSegment( + value: CardMod.graph, + label: Text("Graph"), + ), + ButtonSegment( + value: CardMod.graph, + label: Text("Minimums"), + ), + ButtonSegment( + value: CardMod.graph, + label: Text("Maximums"), + ) + ], + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + //_transition.; + }); + }, + ) + ], + ), + ) + ], + ); + },), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 6bd0b83..21e0118 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ flutter: - res/icons/ - res/tetrio_tl_alpha_ranks/ - res/tetrio_badges/ + - res/images/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/res/images/Снимок экрана_2023-11-06_01-00-50.png b/res/images/Снимок экрана_2023-11-06_01-00-50.png new file mode 100644 index 0000000000000000000000000000000000000000..294b4f8a034bf83a0a2cb89d2abe3540b94cceff GIT binary patch literal 1545331 zcmZsB1yCGY*DVRbArL$`f#B{kKyY`LKyVq{og_E}x4_^qxCFPsf(F;YCBWeB&YSyQ zy{hm2-@mK6X1Z&7y8E2H)?Rz{iBwaO#X=`RM?gTpl9!XxKtMo(e}8?BiVQ!JD7=>s z|9ImjA+LoB4}VmP2m}Nw1bHcOE$__3cAsWTX`hpq(gtYu*!<4rGS=m?ve&fI=mb^L zAQjV~w7U6a|G)u0fie$mBC)i2zHWPDH?@rU4;g8)_fnsv-dG^p`Z{K0+xSw9LvD;E zpPs$Tp~ikW0j=Y8adg2#WPijR{65aK$gAVvrotQY>*~JSAD6bzVALGdz1a!|BfIk z{Bs}Uhm7If-CcyPmvxk9qnv>21tns!KrJ+_a&2E<-@SvA;_9HU&);4lha)rUGTnBl zu0ODYAOHK$-m>HRuhFLsbGmGV93b*2Y}91If29o8fT^is zw;07rV-Uq-e5Wabt^Ze?o^Q^Z$G*a%_yhzVlzixum-vWj3j`LZF(eMdP+fh_Ltj@` zI`qm*;7hZYuK#a$LLnry_-({6PBvJfMPja5WlZ1+b3>r0vopsPtdvOWojs7sYKT@& z4K_Rb_-?WLzv3^r`)9;&NfD9yriOO|?}8;I4_6zKRy&OI86B8(V!FX;F8 z4|^w+U-py#us1OTIbU91aS90ZCjUt?c2ti=XAgT!Q3%am6{S;9S=YWq*r+TUT)C$#oT_|KWaW+6pu}sg=p{(N$n~lQ9W*4cI@ZsZ}EOK<>2Lx8~}3} zeOT=);{~=HWc&Fnfz}4Mu+evX@dbcXo-*F?ipL)YPPz+6Gm9H62Q-Du!-_2MI-Vt zlHT=H*(u+kGB%Q--uqzI>C3&A58}o81kPOGqi=`bW}LBS{^<{Jv1>n^f2IJ94r|@I zooB3sBUwEjlusDvcmBR7`50q6_v3-tq#e&#qIWew6Ai)V`H7{ufq#CfA`G(-J?|Ei zWxPTeeIfFCjta%t=Ju$y7Z&rpcVApu(yMbMPwUuEv2ErB&eeQbHS#=M?8~@15Ip!X z&3yyqb~?^1I7jtBSi2E;$*jABRUiN5O*48#aYFnz2Lx4kAH4XoBh0UG!HO55G!xTERS;#G+0;K*hyY}Bba zadT%J@ypW1(j7GDSQ*(T?yI$e(?`JzCBR-|6CJ!WC(iD4eHM7e8##FyTySi zgdT`J_#g-^F5M2Iy!dOK$AlOwT6q&V4LECH-JRz`lyR%FqQ-@xWX2WE~+Cr5Ln{aPMqhrOgn>zNG z=u|TWDnuIrH)Lz@)LA%R<`WVY_C5j=xt%eOS2Oq63d?fnkj>sETkdaY*E+f}2#+o{Lm1Pc5Br zp_SE1mZSGqEJA^=$jOf?^qaEl#ssk9JsSgdRK`RvE_ohpmIhC9jJuV^+S6D3uDLAF zX;=McQ_|a7RdRw{ue4=Z)@;s2yi39T)Cw@D^41^d3slbQW ztmX4zVg1g(`v26o90AUB_T#`qw6*8Fu|Yk{J-*)K?4 z3)Y$as*W8gDdls{8)(sQ4W0g-b?o@#6`!?bAy1RP?T^}?G5DRI8YBos>pThOg8mLK zd@4{x7x9ggSiAV+;Z@_mqe4e6(6uk(=Y3o=U=wi2Y4DYd;|6lU(9p2yvr4SM!*zYU zq{@cMK)A&^BxBXY};bH7Yt4JXj~|C=AadM&xIMy`=Vif>3>UJ1fG5cQquVP6{PTiTCt4Phnkb};Z7!GJw?#K5KC6PuE;k}9 zCM`@K(VI@@|EvYv=j`smp>Z`^2Htp{5aB+zud5i$UDEv=G=-bc^z8Z*O$VfTSv<8HcL=lP|zOZ zckD-n)n>IK6j|ro{qwTkbdyf>1zhdCu^|mPyGome6Io}_LmR>fpvn`qWOk=y*&T|i_x8$6^soQ3DbxjH5c1sR@14hkK4~E&G$phQ_YCM!ONZl)y?M(Y2djcRgeM@h`~&zzWbjt z=@hpI*CEyW!My)!&NwOYyUsR?y!TVX+z-Zfb-Ykk){m`m!m+NzwQw#lH!>uB6943d zKZ)kv(Bw9PM*qvnpTDw_)g1TtKgIm=>Po#z%X@z>rNXE^Y?!?be~oD4%#Z)+XY947 z=v7ltmg6SiXeQ~dP4xv&tOdqb^)MLz2jTxmY2i+#+Oi!+k(LZljYv{-2h{bh6 zj!#;y(!9>ZUbd7y&YuLg_Bjx=1-4t!0^NBowbKY(vzq` zi*d~%CVcyGdGkk6<;!9+qyKfzONzsim-FGJZE}?Kmrfd++M}pW&V7ZFR&XY8%n08u)kEe?KmhCA0JNO;zW0hEO_d ztjx7~+p|yhz(!K4^LZP>Ub$MA(_DJ2kXRh-P z{qtGSg{sLTuGew^cAAm*D<{7T(pPY04CkM^)7!hlg{GV%VXvBi9X5CoAI;R~_Oa^e z=gImQO;~HS;=3VWIxzP6P|P{L{AGZk*JUIu27~}7CRTbD1HH6Pji@>mv7-_T%4q+d z&3=S89b#rb{YB$6q8OqOBJv~PY3F*ZKdh1HTo2!a3#HR2%i_5#5WmNe1>p#2c!(Dm z-X};ed5@3@FA{Jb9q4#M={LHr*%gd6_dHn{hNXQTf@>Czll!`+v%YcvM3>F}v&xge zAT8TB;$2fosujaQ{WSx>oYtOu`VaQ@z*;OtQVr5anZK{T+6!m%nj`m_eM{$#EdXno z9o<`xv|CSIpbg~z!!pir2|j!E@k}6#WBWXY&t&Z;oyX;MXY++F;Bg-B5*rh97|7y! zg^lUF7L0v1Z5Y&j6LhH=*lC7@AnbXp)MMoO7YRNtUyF4r6sn)@S04nf3i0-i%hqC- zSD^~3PA9+o|K0Z)V^)~dMXCBfBiBRs*62lp5D(@mgj~1M-dzx7cU=W}C}%ClSQEw) zn)`0b!9}KZ$M&rK?jfz-GtYE(Q1)>n-+da3=@O*nM-G2@?8uVwK-9SLi%XGGwv>|- zrC7@yA6(%E{sV_p7GtTPATd*7*R>Oht#2rl6cmCN--Gc8S-1Lg9y8%`7OrvKlyZbZ zOOxoigO15`s`M4Tbcy_Kjy8LdoocTe`M!TSWH)`)PisPBV4$$|{VQVl+ZbXicyD4; zxp#>3^cE8nS9&tSFa?LiUs|&-odzGE*>v`EU9cZl$4VJ_{S;MfBuyFdUWDRAnGiLCJ zV5kIgl`-=U-l8AI_PFI$s|Q?eh>kn0B{-IoJCOdRZ^DU)FeBxI9wFWL2{e| zZXW#ifUXmJ%L{eQkF|>Wp7&Fnf37ZvX8q5?s<}-4`RlC4a`hN+ND|u&u%?~4V}(=i z+;&b6)>a4L*<|q)X1&lDfC9(JY|pt|eSdp@?|%yPzu>NW-tyZmB1gHPp#4Q`aHrT^ zDCzA?1EpDazL?7RZ<&R^yOtDq%tguhZEc_=_V%CJN%S?oa})@zMM_&Cc&hnoEM(N7 zJq3C!Q;Sm)Kt&8n?{Q%t#|O`jv!j=76VV&&Jq(jVIfP;!AeObSUpTnYrBC7DRJ!;rEwWv|n zwrU1p7*64G_Rc@GnOvCco|u^URVxWc)jz%ELJY|J325@Ut?7#>T;Xz*6GaTK?xp_LyGj(BaIep!6O}S?E@-6rgj<0l%4p5RRnd2Z_ z@*l*a|0iuaJA2ohMvlJ#@1LfFSl=uaSG9-1ffjc8to(8{nx+VOR}(loq$Pv1P{*aH zpZWtofH>AJ&Y3#L`hH|{!hto}?UiPn$Td<@NY9%Kz^d<+_Tg$bh%EQ?}||BPu6}#IHDGmK|YUvis91LYQK_C=S^U|udF_NMy#4| zQ+B4sIJ!nVZ(+auV6LG#YuAcG!i$0z^_m|${j=e;v>92PT5vV!RQ+Bax;I^P4$o7M zIh61)y_Z9o?XhfaFA@2~OE4KBvrc8677Lf{MhKD7bzbG+b&9u&C59=XwcJlY){YtnTg?D$&3`_+4x*5VHQA4tmNX z%G?I6ogLFWle3b@|CULwyO9kp2o!OVHAKnEB6s)jnD7ay%#n@OR5AJu5v;qrb7;bo ze&^J(uh0l+ih=j4%8e)U^2A|>D@{2l6oDZo%Xj6i&G1q**lNwk!K0-j(h46oX;G8) zQUYfET5r=j7Fnc+q9x(uj&N&!2bDE00O1|US(K%<^L2Xh#b(ahkXN8H-csX9ZgEo_ zK{@oabUff2A`15TvsZhPf5$c7T6r}j(_!3cC#*UsDXYy>vr=c_`qA?rITFWk0ITi@ z!6M~9d!l?whPy}Chbse}&rAFzT1=4ZS)#c=+Yh2IckQGL^_I+Vf9aq8&@a$WEba4o z+2xNOB8}^weOLu|I>6<_+U<2voy!HGukSNQ?%kMh#H{2fiAd)s=c0H25b>Tw+&%R54iRj7wy{KU}OISw3@?*3xF&T;3+_8v1 zjP&K(3FQj(c5=(_R1JIWu3H(dJ3a^R**n!^C`qFEYv6P;Vrx~Tp^A7m%C|)tfQmgg!=dRI1Upqn0 z&u5Z6>RsD+$eng_I?tW_Z>wM4em$;YTYa*2+Qi`ZZz%OA>OA*%Y};f@&GzT|ycw3$ za|<`AxNSGysOWh=s;8{|XO-*z3;P|{aX}e$T2I7c;Hq_3_K#zJpMebtQc8fEvm{}F zb~_|f;=0SNBsfcm$CEZ%o`SzZGd{Q7a0u{7$@-Dv^~b#gZwVVf)b0{#P2pZfGBZJI zTR-nd6_OsOwVEW3sGkCXfTJUag0XUq(qC}#>A$w~({?&6t1qo@Vgg6l^QfqxQ%KHs0Q2rI+>g&TMPmA*a^akkRO6NFaS%RB`W3biZ zK)=b8^ZQk2OaRI^>*=W2%wmm7v@hMNO_W~d!+?Fol$0j+;m7~a;7aY$o!JL%dgxckY%Pqk!pI2Cnnj`w6 zi8LAT_f8HryPgBQHDA0N5rhRG#Dg$b+2!XWXZPmalpQyCS>XisW5PXdUaaX=zWd6{ zf7>>1W#c&Cg~DG*z#R@w4uP)Y&%$yW#4ttxYtprL8Kb|PyCG?YZ~irH zC@8`3lNA^j7Z+`SK=boLu~qKZ|KhIyC)WGJ(yl9M{>O#?*?rONdKdb?9DkFw*(%_; zeb^ek*5;t*h@1y z%?^_UCK4y2x_>x-`1Z$0Jkcf*)!!uVwLE|9M`c`j#Rs%(6#6WV1oNVXM4N~Yquijk z93ACwbD#Ba$>gsW_Hh>?;9-o&q&5<7l38Kucz3X1k_?VnT-)u@gAD>}<-BT>i-R@$ zydAaoVxP{@q#chUN6@>tVY1IZ&?n_GrGVcNAdu8^TTPX6cU8Q8G^U*O2MT$DM5#-o z%I~qfXa-v{1%R&Tvw%>Y#GGQKOeAXX+nJzgV`+FmSnkYa0Ex=Pwrrmu5=`w%pb-BE`Rxb_d zm6sy>*t=M-s9K;cAxg<%d2UgbC&8%9R$Z!7_?gHMH(#!P`vE5`z8BZZ2G!rt7%cBD zkzX0eJ2dSrN?Wr#I3C&YTMbWoJt0?VWh=U zhPIwC*AGAjK%Lbq4q#2gYiS5`m7bnOW@XKIHr}hN3A2+f2+uWu_7(-~oM&_;J(Ks5 zqy(3|7LS%;RGwlFQ1el?+tC-Y+Rrcf()APlAPNmMWrQk6;IM+KaUB#2Gn$mCmx z+HSg3uDyA+bundVUj0v z8JfD`@H-CeRhNrrfB#@=|BH9xy>Ncz=-Bc!Zyn=P6I;if=9R5{ZUq<@RFP8FM*|8Y zWVU9r0skQ5qtV9eGUg64NR*-WO|j5AqQhinrV5Tj%dY?!4F9w-TewV4S;Ob$jeOi8#Kuq5O=xS*z<=&VzFnVY>9?K1k=3>!jrUvxAgWuQ6}pRD?$Jvr5e0_r@n0J7v__6jj*Z{b7wc z>u}wkcbpP$DtUD>D)37~O7(}A>+se`p4>YQX77RA0lY(N734f+_s(j{9kbv(nO#FD z1tvGkD9AUfQfagNzNm=Hh5JZr;r@F6dy&NMNQx<;1T%?YIM=4kigQ8?7!)*av4AgR zSUGxchbIzauaL3)ho|)2WUjj~%kIdD=^ga5O_6MduHCig6Alkxfll7iu;xgbg!_<( z$2oKa`>BhD@)P5UIs-%0xBPg`EfG`zYVrsSvKX(;|Lm*qd%;65b1)}8YCT=FgjCaI zFuYPqw{WN&_0@f&82?}}yx4UuJU&SHkxTZ6IgQDOgIrquC4lY!UkmW3zM%j0Vq=a< zG9dXRGIQ2A>}|!?g0M-<)RRqR38whEqHP&LUH^=Rm%<-k_P5>a?vo-X9*3V_+#pl~ zy`e!ec@A{K6fdt4Q5uta^Dm6l-_hrqD|Zw!BwNH9y~p`XC@empp=+u?bc~`_$%Kq; z_nR8kH8FMvcuG~q^qLagL8I~fYmtz}qV8(_bnlF^*}fQ6R>Agpep*QxnyUgdwg?yh zx2(-4Sl3P0c*0l>(-R3hN_H`^K%r(J?O^06CVD*kA79!k1p}8t>c&QmA)Gf;9HGOL zzuoY=unVb&TQ%h$^`PuoN%)e%A0+c~QKK?Sl$hLFP=421T?1dfEf57{3WV4=M`Wq` zhfduh{SbRb??F>P2vI%^uO*AxlT7Jwu7;k>CXQDQwtK z*&G$kh~!)|D%!peViL}VeCtM3Exy`>6&{ycPOAh=XxkEHvtzA|GrE4Ozz+VT&LaQ( z6wU*qW=PHje_oWs7x%#lLl*a;pf@`EOvmQSoL#-U;>!HM|Mcd&r)%F^9iLdvzRIQ zGS%{X_cGI|(+ZK1ALfk;&sQc+h%pLy7(Pa;<62NxZfVk_25_+Z)~2wt|QzLs`t;-bgv!d8lA;5^bveh`xrBdZkUH-3mwGLEj6DikUn#?Y7P|0m%-Tm~-`>+1%@Ag6@!-wpBs&cIP zucwv_yDuy^3Y4uQMYvcct3WXK${;3ghCDQT6e-X*`9VVPCuUBzyHD>zDdaJS3JJpL zlr0BmrINVcWS$ea0{qAij%C_ofrd1MB4*MD8BF@4b=MQAIl| z0&puU^R!SQ9Ob!xsHJg`Lc-;HD;*vvFIP|X%Rw)K3e025QqTsKP_e}lH(rd54|g@m zI}2cS_+Tzj*_{#@S1%kIq8Vm^MN^`~OA%#6ogkzgB|!L^`vfDB^+{**Rf@QV#PWr^ zf%OnxOTHW@aFGmo*z)@FkPeFWjk+iZwjD~}LNfClJXf>C8t ziLUP=EMVZd!@s2ZiD_Yd$ zg9B46kGzKI&}Hc5m8dN7HSU(8w{fi0@FjPEoOznQm8(r>abf!B+C78jWVc+tJQX4o zCiAwVH^xhuab9uIrj zXfkI5u4TeZ##jpX!5Z5#3f@qHss-F~8OVMGSKT>JlK3tv+TSDM_Z*0)qa1jItiRh=w9W{mQC*U|oJ$AUH`C3% zWq%mV1HJotC6OLSiH5>0A`G4_x4+ zS^|?SI&hEq_coS>uQ}C6E;x!B4cPsRQJk)EK$JQ5jk&kqUF17TSd`Utgc7P48y7#jZfb8L)usHI%IDQ*;74h2rStzg6t@ zX%NssRy4BXy2rzVY1ftr7O(Q1(HYyJ6&iipo*!^ zLC>MjXf=wh2$0#nK4UAHu2W z)h#hAaVDxKP!k+o?2RTHSM=ZF`igd+&HkqFs|(ggv35QgB3R|z1pRc~FhoU{e(Ma+{bX7dsZA?c2w?kKm^Um`Hf154()PW2Z=B$LH`m=sSOT;$l1 zL{<9P@S~LeneleY_}b~7({Iin`$(L3=&#Q!ad~y}nO<$pExkq68l-d94Vz*S;=?3f zQ(QFay}PKPue+TN5MrvVPl8{&DU^pp?nZle~_f=&qdO4LT9*Dy(U_%nzdO0#-sNOEtL`l?Th(p zq;c=Z>uBjp0g-6c@ko5yXzUK!#phpt-vE=o(bgbF=tt<4gLY>kvYj`)>vaHUTNH`4 z)G+|gH$yyp2Q%;p9rfwY&^pbMWHo8TdLt*)q1A zwHl6-mTDzF21PM+){Ei`@wH(Bjln-YdL-%nNTMW0B(?*9=Ywp%lAib`lDA>2GkCxG zh6B!1k>lbfUW&jFoQFIiG>QJov~D`tfN1Wx^|jDn91P644FszuT?7D-Qlg5~PBp&^ zMs;ga3?(s2k`@|w;9_9i7hEF|z?irYFstfbRwe3*i9&D8z~{Cw-` zQUW|8VQl+rEDsn-5|OleA163YR4eO~K}GN2SyG|S=^ZFRCbK`Sqp7y{M6P~2hPF1)L2zUp2(oo_8Zz3_Rz`ytYhX`whO z!%UnTKT#AmO@c1CsZP>e)o%9qoh;d)uo`CAi>Im!rn~qC=#Igxfx}666t7+?yUK=@ zB4%+~Y6!=p=t@R=&k1TS7+JWiu8n$NBz~`&M-`%f639?*gKSPsDcs+q76)(`hIuS@ zAfrfEXS0%KC@^rP#vFF|OyGcu3m511g7IMKDCmJ7ROyC`L=m~=NS&;|_VT=@WVp>> zn9&;gAY1hXb$@UXfjXm-zfof&KtG0{W9p4`uP>>@;>{;!Y92&Mt29Zfk?HBCnCw;M z`i7KKks4n462_aJ{3Q0x`nt9LJVemx6Rr!Hsm-V_=q+#lZ=>mks*LBcF#6I3@2qD$ za^|e3&)6%@D=H&kZr)zb!n}5!x=>q$y4%L!2ydL?{*OuaG5AO#xPMczOxQKrjf&v! zHR`QBXaX2O=?;E{o@>yS_H5-jwl{}ZZG%JTiIvQtx9MC~ef1dGR#H+zgynzFXcaA| zw5^QNy2_|mTAMxah_%ypopH6SpZUL$JZtioXVsUruG?L8zsgl{smEB$?tl@Yb5`Gu6oF*@J)))hzGKDStzy-{+lKkSd$|Ue`J{ zr_TD-p{CbQZIMD=JAAc&NUJ|MC?^c^F2b_(?z1d;Ub&Q`Yt> zP1mw*s^#ahbTo*Pp()+3p7RJtUJ}y~;zbV=`Yi;iFD8bjO$o$of10bZo;xf>DRv|d zA88jNo!7fH^-R88h$Sy3RZg^+Rhvui?p|ze<)kSH?{8IVzxMw~;VPrz063}VYQ#NN z>OL}?pSChCD$NQ0d0Xmuv@AUf8m}NSH81$(@mZ|H(lgHTOIkHag5Z%XpRMM2w!ZEg zh3z(ZGgM`{ot%Y6N)SXdu2%}}+y356cCm^Og6PI1I6*M3(Ky<3Ajg5Bh$VXld}UE_ ze|PDb=jp-slwd20?-gUX6KVk!edp%9Hy&Z$O-q~pS#Z*XOT_t? z&6N`>lZ@Vpyaw-A_zF=}*iR^yqYKcyQ$z;r2t@HJ26U!jD|vrS#@Sc;)dU7qYeLzY zt7kyZ>PI$aM}K;CR6#JRt5DV&{*pL-)Gwv`!bKrF@9YXmz}o4%4i_@c*7FOATkWCR z{^=U`5&vjvHaxW!gRFbel24u?(%RJ373T6CZ^S205yCKI%&3!+al^Bu6a&1ADs5PS z;MnvXbu#{#X9qFbD!Yv`E?E4~Ak1RShz+sEMUUgkgkl2oS|U$d-JU2g*ZRf^x^_;_ za+lOL*rb9xkYDk6CNZ{FSeEqu#}U8nnriM0FGxSlKh65&8D}Z`mg`PU?zcCNJLcyL zcC-7^G)Z?sNff?Xw>vMJy$r=FJ*felYX|GcWoFwf=hsPuodhrf@BMym#v6Mg4c;)? zWdEnkU7<1wFq0)~WAqquSZ^CPc{OQ(naio_<_^!i;4;Z-59_2P|5RMWUf%SGbUoSan7LiOKS`T+KdM12p)P zS?6|IC?ko7i1ImJ`H!!7=TNnr-r_}Ax%Z8k7`7dirI*P(JXC0meBwd zPvd-Qv3c7dIKzjm0I<+%|3@j={gT`ddH%^iw8TtK;X+Rqs1I4XKlNw8?gKpcI^trX z_%UgP)dK?5h)=*Fsi|acJF(_>Q2|nUDlw^ChMo;i#Uq32oygmm7&D@Lfg4sVK67CD9CewvIiix$EQQ6uR5voYrI4DnCrU(`|QU+{uI> zm{42f&Z^&X!G-Qp#LZ_OXe7eh!3t*m;0awH75&hQg!_TJ>I+ z?N!H2q_bj(Ecv9&~(k>+JH4<-v<-~ zf%&E3O6+Atuuw02L2xLfzB-RfB8sHoFG_};Gz8U6aPVQBd9({*%PnKVUJ-#Ib&CQ` ziooY}%CMwKiKWH_dT_hU-|Y81k}*5LjoNvnO5WMBtztaM*Gy`(v_$3qXa-dd4nF=SJ;nI-&fJurGfGl1MUDzx z9pWduq#t?189O{B?~kT^0_c8>70t3#-4)k$9!!mHIdv@6(4{b5;2jEpr^N9r@|iaC zhZN8k?c4usWfg(#8nvdhJ!oSwn}t#o^W>%?x%aljc=z_qy1YZIhb#q2+J4#2D5koP zvbpYWIWT;c4K~(J640Oj6H*0Rn4y-l9!g^*G8XC17}GrTr6Gl`mMgC5=miv|-;JI2 zC+UCTB`kA1TgJ)dR2JU0o>o1`r)#tCSb`52o?G^4sq z`e0Lc>ha`I*ju@JWjX|EpM==S{?NH86b6NJ6{}pW_>4f43{QwJwH?6bbF>YUPTkr*$6OduAt8rhV9FC&sXF$-aI5?s>z}k) zP1dDWS`xOIn2$x(3^Z!wwn>b^g9#0JR#^$Q)gJ0x<_3~nJwrbmK9(wQ-QZ>9$_HRG zb1T~`1&h==wW&CImC_6SFIg>M&FIewEUZS%cqRp$l2}@C5VEp^5rQ6xB zYKqtJ-Ws`FiWyMZcfm}a28^~m&9pc%kE(3{lhK9eQrf3=M;+=E5cv`*Xc{)YB3Z2{;ac z#<8v%3W2hd;zKBNQz8c%-%|mTpR@<_@+%RganTlb?ogY)K+$bB-M8LvFur^0Q$M1X zHA;Eh5*Oj}Qti_17nDV_b(CKg2y13}OMRzf+oQc1uWqAlHJ7x|;Sj5G5u@VqVJ|*{ zztEvAup^~vfr#Vad*{yGfQrRZu*t{66}FJJL_YkmusV(bM_VLVpt71;uXboGuZUk> z!}P!f%_D%!+f%SD=N_4R?4h|rQj?8-nsA>kKfS~J{_E5X1cSHNMd@X=VPl0@a)y5H ziA+g!&+fI!;x$C^8GCc*v&uopX6U0L58lq_?n0z$S-7Wof8!`Y|4!a47K{P(**>p* zW+pT>Q=fGEq)DFaumO9e+!vf#%|K|G1@!vV=n)Z%;+yAmB7^M5UwilGrO0E$MGM8N zSXPa|(C%svwBnc&=#nA+Ew+!_+2&Ux7^3!FL;X+iH){RzZA4-LJz%wh_>wpA{1bPh z==poA>?<^Pdml2H5WfD&MJ&Hj$4SSqcM-oA88rDLr>5jZLNF`1zoMVfaUeE}P*$ea zsGzV6e`hvH7qwh&YyZ@O7_Q{rtGG4+H!F;PKI$vIM1OQor& z4i19X$E0QlP-Fjb(Ew<}Ld*L~mb3UFI|_QcLqb#TZQ>8m3y&B9o;qq_1lwt5fAe)F z0^3}sc=Z8aOuL+!Hg3*vD~2SQMA4@thrV00-F!L7Q3Z47RAf9qT5SYhmiQKQ%T41X z61pp=S=C>kj1JjePD&REkWj#-GjJPjXKsG@%c}(9i7jW>ANsDJ6k*zmAa0Y-c3hxNI(XGAP>_CF<%V$>hVpVH0=ajAa)0bCAw7rh69r9>KgHl|%70+%_oS4z z=(LN8Eu^N6%mn3)G^1+npDb~ zh&e&BU#knJTyf))%U=E{Hq}Qq8V|SosfJa4`=;LATjI5jWxR57 z(F`4OhrzczQELAOKc2p~${hq|Zn1I=71YprPdWX3IZc)*6%p3Lf*pDIVu?Ln`HHhd zsY3<1ePjxtATuZGh^D?U`osBO7z}gxQUKYc0%Whtf%w^YD2v~5KVe~;g7`Zpr}Zx; zTBpzicJ()bW)VQ;AMgDEwixpHj9avi6S*_s>Jd#VEK_X?94YEIk@L~vCod!^c5wRcqO5flbG3WbilhnfcCQ^DFeQm3gn9Fuf~J)oLdv)ro@@(+EOyCPP8T8?>tREKxCM*zq4v#} z*;R8sjf*71jB_N?r25}Rk7wq>%QVNFUv?LruspKmdE2~yj)bk0#b7~oiYz|&%s>-8 zGe%_c?ahbXqudQSVrIVxOI7md`{@EPMoHTpQEiqq>gX!=3#>Y3+?R=)bTWJRtL$42 zU}F*40gDCu9Z+dCkNaU%apiE&BlAa`srxC9NNAeaY(mW3#F)0suX@Zfwg;Z?Rn?8H zETEY1MNFlI4`68?@}XF#nvZ*2Zm;}wzL<@HH9uKwk9cAj8{D))WgRHxc9zp0xu1zk zmwR$;g^FynX;6+-1ek*s!`w$VRe`++w2>w)`-72mCLq;0r2GL_iY0HxE6=u=l4(l8lMryl57sV9u zbVFd)LaA)!p-}48x16MljYK^tzx;77jkb!oI>Wepu_KSGfJ5wXxi~E5 zj5^e(PjVa(Cc_gy6jnSF*;UOg153lIoP<&#IUz^c{%N+HwDZAKGc=-s1~G4m+h z12{6eu|K0PQQW;Q4|=5EYTHhbLS$6c;McLuI*y1kCqC@6Z<>9BRB~x2r!WF|3C3^R zp6E0ebkJ0(S_ltoOut5xUa1PLf6@besYc;w_9JK)rhJ|2@ko}Ks^gn&B44$Ylnvu2$|zForEa~pIzl6I(TVhoY5!Nmjw%>gAoA~D>4A*`Vc$e2RR_E^303#$(uhD)ojZcb*a_bYQNe_QLagfR1fvVV zHTDNO231IcC1F#kq4tE;eFFH_^(9PFbu*RWMFisqifc>10`Ag&`adq&?qygC;a{0Y zm0bohUHofRV5-Dosg}=}*MQV)q*c)zHS_z2iXlaeONlv#iUeau| zM6Z&rJ|)V?XHvsTIxVVo{ujNM_`aWeG+5%w6e zh~bv(guE{mYz??{#R;#)n$`3~NDvUgVDc3jN;Ar(CBOTTChS~#2g$bSn*(%6i)#eT>#7p z5-pYUXh~(Ti96A4EtrZ0;~n73TOMdOPJMz1f?zzPKo9Ra8t7#PF>ROHK>0>zJqG!K zDNdGNtSPU>-9k|m3t&W1fSAkgf_6J|4K!WfxI+zOFo~5FJf(hrh*gE?Fctr|i{B zH#(<(m^pW`^N<;MJOo)j8cnLC96UR&+6xo!ffmqq-4cb zYDdW~C6JwwfORz2Z=+(tjKRduXmfN|$oI~dqo;+~eg*m|JW8ip=9x;(_ML!=RniNR z7MI!W&sZ*%8MUrp?YxZg$yCIB%jtzTB@6vlTj@tPJr+Y&1z|f`o=G&?&;%02q-(3{ zIfn;fqY0(YyfhNwSF>~}^Lhc{wi0_O_gH6GN0xfKbe$eY1?d?XS0VPXF@>JKjg_S! zcVIHw&)5WH55wXxYh&479$$&rd2AVlI{7tXE5Z@6huq=C$C)IiQKOC@i*HL?<1S@C z3lCMbSNPLiRK&;~0nP8Re0VN&$cpM{@Vlx|?6?fN?HbLzGkN6h+p035v0%ZOtzq2c z#?K3Bj$ie2tioODkW(%Wv`3dzK;h>@71M{U<>JRu;DG_~Ie5t-Z;YF_4p9gzv*R`d zIbfGW|7T2rGBQcJZt)4Z{1&D!JE$pv_2XAwT0SME#cq5npc~!EqJM#BQC)x(5>^0tD;fCuv@iQcLh5}q(E`cnmfUXu*)GB zFZgZmSzaIHO|QKe#rzX3N=`uH*xC9H0W8pB$)0M}gF*6>4sZyYIl~_V-+($cWk#3w z0A&p&g<&(d4Qz|0am;(I~^36vF>T!XwE7? zlO?^8ZMd=h`d7k0{K`Lwu+ViC1AuC#gqd zxwj9J633nSzhn(*+H^XVZZ>c<*d4nYwQ<5D9+*A&`3{a<{B~xa^)dSQJ;#&(`AU?E z*?RBa5{EJ4=Uir-u`tKp*p8}eeKk=OJ7#d)nb*-V`4l$&_G9$z>7(=5)0zM3@3P^p zkFjsfkI~3luh1gfix2IV7hu|*jfs|{sv_gOQ68x2KaQdAkU8tJP57rclr`yKBn~Z} z2T6q*xtxgHQ6%1iU0u@l`+HRCSW+I49kHGV9TR6T=bUTlI(8ZPmTC0wew6CK76uQl zX77eyv2Ww8%sAu2=(;BenL03fuGvvz^b_jrRx>mw3hnGn*IkfrX8WnN>YkrD?*eLP z?Wx*R5^Ln+*uGpMlLz$+X@yRWy{8ZoC9&y9###$8=9z<9JXU_S#n&(HP<%~8z!^&&KGfSQrD3TU2YcM2`VfaU9U5E)`rE6mF@G{v0H z>n>cx*6jy)YVGp`=1|oHF*wRJjM`Hojfapj;x(Gk+1|>O@#Ex8vOYYw>kzl!zmoNv zck#OOmT=Lsh2(QSm8x{O?R)ldsK1{e@E91XGHX&N=bUsr_pjN)Q^9lSSYGxr)Elzr z;C=@CL*f_?>^s0vIV7x>DHMvdwN8{RzJcu{(!o71A;K74TW{uxU2H~O--9rbt+3YP z6)o*3F;5@@Qy+08TXD6f_J$=N6W8jV^kCd5G6uS0i0xeMFj^Ahey*UU%@Ew$oM({8 z6NuJUAwLEOVDG^`JWmmL(9-6!dFKuuUAvlnJ^ST}T8q5cBCusdw(Z7M!4@fsIHO0A@GxQ!kH-1jgDn&h<$oj%t`;0Hztd{ib6B72!%Av=c&EL+-{3a zG&wCV8cGoNcuRQOhbTIcKHfckCgUg0FpHqt>{VwNfULJh-3mb0U(XI1wrsUIv8s!j zw){M0dl& z%8q-nsk9(|m5@y zS)FH9)7T!A3Dj6;M?&*@sb!k=B5MrVNc7poU|O$7$Uo~*X0kIXuYU>9NNxAgh20u;m_Z z!L!1sVS1~}(>%b*K6I(A3>%VaB34~;s`0`IA~nPv{-gP&k#vuJfR^bu!;>4KvX78w zv*^QwF^(Aa6xe3OVAD3%*#DZymfCSuA2v^-5I?#dR1mO&RM=v zHD9;Jx?A8T(KY}s|4hka}BGu(UM(DQ@_8uLH^Br%W_NpR2b&V5-GBiP=g~{vJ{)7I75U$5gecb?^Q`Pkx@y#7$okxxOIn;De!0vsYL?EeSI8`3cz;8Kw58-Mjh!iTe}X4I^C8T)PeniNrj$I0#Cpm#>f2B2S(f5&Fo1@Z2(u|;x+8wbVZ^^1tbleQ zxS+`ZYBUfD0p$R?4`S@3{eJrhD%) zj7ccFk7A!208p1wnfk|XG*U5Gea^Y|d$A&Wze@cqf?DPo5nwYRB{|*uo4_j7y3Vys zgApOifa&-pocYdg;_y5FCWiZtVS4dRuy!1Ipw{3rc_1Ln1;{N2;vX<8#N>H4mI1&X zIQ_&Qv){oqayMFE~SlM?gilM=5^Hq%Ycm;MH4&VOc zIC$G9bsQ!-koLFLfykawu3e2=fA}Axo&m2u_-kMf9KQ8uvFGp&*t35V<2Bj8PA*9s zk5+-{rtHH)a2m1#0vjuP`?^-}nAN{#Ag-v446ZrrN)9IST&r)SEo}~%;AYIqo^C!B zvQG{fqh#f|q6F3s9>T%v??Y2p*gp3N%Ju88dgQ%0w(?)%ntNWyWb*|y^$eEsZqw>= z6`DToaXeZ=NWXu2*K*V2-XhW}$E`pL%Ds2$?4OkJs1h?cjAb?TwV4X|0EQ*P9%z+L zA4Rt^0d@0bjR+H`(mJx_RIoNWd9Brrx#bA%q==~re#m@KSk&ZAk846ldM{`&Sv`>n zl3fcd0$F^h?b$0G^VAcYxNu>r{ufvm1Xk(Z4&*X-xb~(2_8(fs6Hi>k_NEhH5v`CG zovQ^&y)v$@J+TMd+YR1$^L6B^}6X%$e4tO!pIr#;$nfgwoR${ZFDM_~4{pawQ4 z#ChkSF!;c|*W%Rq3wY(#9aw81J_}`0X_^MZfyI$SYrxYEfRVwMtiZv{mQ`ZTrw)0Z z!^0uZ4c^?ii1V8}xccZCzWCs?__M!w8bxmKH-GZOxZ|dyLK#d@3GGnOG{hq|D8Fa9$O3D_6<6_7C!S*~hwpILpzb5w}g z1M{YVA)qt{Ou`;@%-GqHysVV~BV#XN+*jAdH-edwL{M_t&(`B#f9*>cSb~AYd?x)~ zmcVo>3vFHs;Dt&UfW?3?$c3F|NPIbffvt;j+Tj{tyd?sby(n(M%)?r(Z!N$lz!4C& z86ztMl;lRFk*irx+$_pLiJi$59n4S>-|OpbFk*X0$NC)NUm5q2(nj>^<%gqGP00dogKQ5dFUp_k_+qHGbdz=>Sb zlOtv{b1^sf6$Bh5);a>!%%jMpTy+8%*J{>W%^1#e47_9@AYk1fM$PhGg5P-c%V@0( zz{tAR#S8`*u5mQmAeEg}?NEDzYp#DE*7qKVcP?%!;%08idJyGgR6IF8mu}1g@UXDq zO43U~!Ze$(Y3chv>!&4YP`pP#>kQfutRZB%L8cnE^9<|6XQU^Ja?Genjx`9vH~K&d ztV9!@*WZHT@AcrwF{4qSzpkAyeftZqw%u+3M_>7mo5o%`-2aelZkDxC`Xcw9N&77P z4Pyjo*XaJ|@O^2YptD>qmNk8r`X73aaJbcOSa!VvI|*nv&{tAmlU?6#by@hEFnKy- zJ}owf-f9;IHg%7>!3e9QnXD8`7>$wa)uG+0uT zURl#(ar;yRs`_$tnLu6gTRJ9}%KCJ12rp3g<;d1Tulu<3SWLWAjNVI~_mee5zV0Fo zmkf!pUZ?enhk%^aFDb{uS`rJGJZjoBjZsr#U4M;j2N-)X^^2ATh7aWEVV?QSO$G4GAkMImQkzKkY|aGOu>pSDRX!yBo=H4+0HgLp8LN+ zbQY8YlWk?LWg2mJ680$ym=eUgDpU7FEI{30Re-a}mPm+l)CNEqcmgg+pguCvUQH$X zU10<~tAe%D5Xz>MfwOZ#t>8T25<`JP*+Vt9_r1fZ>`3UDd%z1ZhH2jB7Y zxbVWC;nM35;;MW87Ro)xB~E~jV`BiU}*CY5iZdikJ8jP%}@>$E!n7 zE;vaOK*O;P00a~=0C{2aoIN+e8>h$OC^M6Z#u`{4Ec~p;NX(3zZr_JH?>G#yz=f>} zPhZ@G;e^@3s-Q7+)9c|I#_DJY02ohZ(%w#hO79TOe!T{lphyXq&aZ2ZKH{L}vLOmJ zNGa0F#%vT^C`yAj&Yj0MpPIsDGvqnJK$&fU8R!}R_N(84&8>ilkO9}Y)-RhV#>(1q zjJs|>f#b&x;Gyq6hjW{kz}{dytMK(lUcu)ccoIc!@i#ts9}eytVLbK7G7}de=YWl^ zO&G(dYmsiRo;io#`S)MJOQ+t%yz*j3T8BRN3a9`^moHb#Z?^E_gQD@`Z_qI^D*#Q? zXq#xhflVD2>AF@Hm*v)PD~T_HMpu26qruM z=cM>?k!Qn7Ut~a*drYRn&@FQjf|s_W4Tw}es;Pjl1*~HQ3yqcME3fA)syPxEl*@Q^ zbAU>Y?aTyJRf7i~`U^C*I51)FvG3qM%y|oLl3_A#aQ?zZTzK=0QO`xNXQh}AyP8p! zQqH*sB9aJK;-k|;OyXUmr+gXEKuw^PUM^9b8S|;080vUN!|*l7ji2{&>QfYQ!e?^@ zUcD%%gHoOe!B4DF@H?27tKY=AmwH%bqy`O+i~yh}h?NG&twiB+>US{DBtKV4TjqIC zZe{)%1ruE(W5U-C@3`YfaN{i>#%yv4#b7nc{yxAJ%tBMml+enl&PE650DMswPZWWm zh!VAmP<*(XO|pODjuH}?P2s+Y`YVhvu!fLlgfbJb5F{YBbBx9WN7GX904f@TmIZ{JP7zq3hx zkNcT)Z@47w&QD310w|UC*KTD3xBa;kYzk!v4Zl#3_F?Wy9NrtV!x%o zb)E@j?)&5HYhz)hl|5I;K>DyZ@D{G+c?TeC*DR9F&#syGp1s^7-G`|2=WO84lFCbW8TOnF&>E?1c4&s^cvg*n@=2e)ffe6cTc zFL(92YT=^pNAAh0l(pPGNZrM;^L{Vsjx6cDvV5BBpSL!ym)2pI#_cRK`b)6f>c7|d z-YJ*-N6~Ujz6;Au{7p-c4{Di(D$_j6K_6Ij+$Pk~B=Rfy#}Fo=51Z@&7*YZp0xeT4 zsYhx>k*?`+&zk(}_d{BS=vJrShP`z)q!ILyD0h1KrC01e)4fTt&E#_GOWCoI0vc`O z;43|?qe;Oa)y0?zCL^4A?z4FID}Nv3jVELer2hSaU&T-X1AsHhJw(r)j@5k=0c#n_ zVSTOuR@us-rjmUYC`38d{;;waxO84DjPepF2ZW}9_!Q6NT~@FxL@zFefNuz{VPSw- zHH%I!We%h9WWgez&n_qc^Jiz;2RR3g9SjIaPZboVEvl= zaOTO+VEgq)k*V7zDhdo%D&&QLxvmC|-TwVpz3Rhw?o0m!=brs6j=k%XaiA?xUl!z3 z_#Eq^SW6b}vCg9gDAuonu_ewv^?BqPgr=F-c>Z($1>AfCCd+Z`?qAY46QmZR9xs36 zH?VT>P8_}c$FP0&6kho9Uq_amg~0&(uOVD|{f{vn18eJqVhF74A&fVLXfvB~8wK41 zXh;Hg8=&$s4hACuX=}$o2qxL6Y4|+Ev(R`NPAt>_*(>`&m|!ygswx7*+t;-EQ8U(!x)0iIYvb) zhk(rqTNk`cbV3s3qZM@4pasnc;`lW!pBLbY^wbERW#T9Z0I1JBBhh%d(FnF`@*E_! zXU1?qSY0s~Z+rMk%FwuErsxX6EP^RpC%k;_0?ux2XIyOJ6m5Oho{^_u>P@hy)Sed%)?<4Lo=1H0JXP)({+f{PEWw z!q#RD0^{au55ii)pFQ|EKJ`QIlR*vjySH88#Zzwp%s8-T4?g_v>u_N2emwf@3z${n zix99F4p)$u129*Z&u7@)nu6G)D02XW$!rEf4k90YGcvS~gDRtJL9faL6Dx~S?pyU+ zm=UXETaj*oQR>u5uWQ=+2Y&|6G4hfG;A+}FXhIMZmNDDZ4CfoX=jI!6%aed zm`*EXnP?=&5S(*ZAFbhrV@L7GQ!imwSMU=?Iml295lvE!(Ro!7=9!lhQzzh7tv)p} z0|iC`yI0zysu(LnV119l#VwEd++$@$d^RuyD5*7oMiSUJF0xf6J`4-Xm~1iZKum0H zCLrH@)+&Augd48C9+TM=Pdxn?vMk4uLx=G5pZd@6@RJYWk*EF~-+uDT@QuORN+wLk zO#DNZE20R7Kq*ZlOvb!qSYpg8ZU35O;j3kSWQ9!7VAAUWkY(gU0X_!=HOmm+jae@9 z&W8xxiJaQT8P)gDXZoZ)G1tSewHEFt21x76sZv8pEJm6qmrD zw#=A?044#T*Sd#(Y=L`i12Q=E+P87$^fS2gt{=yZw|oRHu8zrK&~E3V5}7)&Zf~l* zZ6)o(rZLDtN`FABU@x#!LYiqpEHXbb773A=NB|5OWC{@GnZck?Mj?QP2|fdyZB5P% z2zhRVwVo*3;ZPv>C`o}x*7AwW5QVxifNey(cY4apYPQ44`jlSj=3(Iq?Zms>*D@?T zllDWhap-hHCk<|ICCWc8SGMpfb^E%1FWyKtP!X3&Z-^Ad zNq$TQZd~g_8oLa`D8GAk=m4y3B|3qbLZBAgYY!j)YumvCYPF+*3PQQHY@K()n$4|l zz{K!|U=`^L>!V4YNp%UaHV{3Wfk-?7y0SHH$W>qPlswmK+UWKL-9GE|ot7cU@`N7~ zpN(wAqzsAc{*DjC@YsTNgC=OH9SfA+(t21Hy@@osi|C=U*9+^NMcW^dHOa!D+qv)} zUjD|fW484&Y%xMToq)XnK0YBGg(%2?25!*boRS#W1Cvelzo!_&WJ^|YAzKLEu(D3b za$vRt%qDV}UKVn2pXFj96#&vkt(jb-b@pQNJ6ac@PlBNk8j>;zP{=X10ayT?o8S9i zVRZObymJ3P#N^EfP&F-)qEHJY;}kUV>hepOtxjd&g!`#3 z8(a;r_n2-xiqZZ9SU*03F;lPys%?jQz5&+&`>y{e-1Iyq=PC^M-T*^{mmd5Fc>Zg@ zft!Eu*Fp4S7_J||>fw)L^VRwF| z5MiwyK03fX_a4CNT8^(jdJ5-WsX$7343tlbJ6dVW?>>HBOaq-PdgTx}bhN~olXI91 zSlLtH!i5T`b#c?3(}m3`Y6q01jY_j2slR7#H+Rb3b0IP{B@v~#jhGBTBPPo`!DmOF zg?3{wd#smxQ4S1V-M#>Lq5Nz2NOagR4i!j|&?X#DGXjm4j4;FAcyj{heRS|?3!ri} zm>ETGaA9i)zyGm zfd?LY4wt44%5o%Q!VEDO4zay8#mdSW4jdFYyhC7S1`0LZH zqym5vS+;HCMo*tW3b?BOL7TAz^ib2*CMY$mz@G{r?-^6|BavA-^=WDW*lRsCutczA z@XcqR#>VC)R8@^TuDcG?+0TZsd;kC-07*naR17b@aTX4NvFNaGbq_L|;YZ(lH#Wyx z`0h*3%Ep%&;~fPsS%9><5zvebV`T*x8et(?Jx!ZWIR^QuYXK8yb6_x+aUX)abig8N z&kqKr`Z}zEy@wd37xQLIjC#tTQpRAWk;{A|>_VFZWaqf)rmOM(cYg$*|H|j_mydh{ zd-jaLW{!`%?}K>$(3{J1Wy#1g@wq4qgEB<5Uzr3jBInx$fP}T?te7mCw*Px(3`_&EH2lMvas?M10E%NaXFDF`W>~!5)<52-n^C zK^(jKUGS>K_VvlICPd{kD)n8Taq$GZr~P&aVBGn$d~*|Ic=`asdPJ?i*F<}^k@Zf< ztuP4(g+-AIBhadt7n5zk=Y+-rfVgkSjQIVGzI?()LURv}&qGwd&=fLu!d#Wb*Lfpj z-*NN-%iMvWqO!lNo#Pr9+B7XoufbFlbvL}J!gblJ_dvhRHUCz=zvYzTj~!E4wEY@} zQ5;Ux``){vG8w+dI?Efom9Nc5b(bWOAR8la^B;I!xw+d$AT00yx06#Sb6WN=e$xRE z^~Ge}Z^6X3$<}+@rR;rQLKl6@&5#HM-)p?iH|l=w&V_cAMN5--fA@BHTV0h14oW^> zF7YmlE1iJ7LM$s{^83OgSID~~?Z1a{kT$ETkxOM%I6Uq%;`$Mz`{~kd#p6~^cb>;v zYqD@}QEntp_m!Iz5lRz(3;o+E+(LmU>GxzI?e#|*w4js!V#&Mtr61EQsGCK{)LS16 zaw=VyI#uhX$IQny?qW zkUgBb_VGmOhfmhqGysXyHP`?x^Ua_o(QvExL?$7LjuB0klST;pv(!IfTqBnZ+BjUO zhvn)!!NMe%5POz#hTwNWKk4>Ccis>XEhXZ3@Q5bPF%5OyVo0qgpuQWKS_Oe2hZ~_f z`to3O81Me^e+f{5dbWkFGml{F^#}0AV(@D4DEPrOP=0Xl|* zg(O3W5wAV?tGMO^e;32`{a8Ev3#jTGC%^gISlcUkR5Jk`>ku~}G!O}^4*;wsG;?KU zs!v8%cW%uL5r}n=_TXCf30c{;m8&UQa*^(F=5z6#xaW|Oz%Z*Mu6HW&iDw8XhBaRJ z(%%r0QUh2AtQ>zoZu_Z!kM$$_v3}%Tc=J2IhhbCWrXT(d*kSj3uZ` zJ^%(Qz?`7I;yId(y9(gBTAt-fFn}-*2Se@u82}^u%4RP0HUnU=CKJA%iz_K#wJ!I< zM4e3-wL6M;e&V0w*sVXQYoJ?b)Bf5R=6!5y60q4?pdcV8LaxCuhr8GqF9!+(pr)gb zNSJofAB$HMUs+A;&`Kng{sa*kVscAK=w&rw19zOF24NQ}Dz?-DUb#lteOv(-#}%$8 z;P9aV^3tH5I>5{1Z6i;~=DRhgkqm+eW6#KPgR8DCaqj#KO~a^W9@Y{_Um#K+0pSFQ zC0*~Ou>*Dbw%hh&eNTaBPhG&)#YUW$LM3=y@U`VK4|arRE1ArOF`s(uJ+X@X>H_O~ z3a}-dJ$o9I8=Zh21j5F~6c7I8>o|01h&&gsdLcqC^u!_9oVzA~fYK<&Xw+mD_=};G zWrye=P3oc*J7|f$IGYA!61`9SNm9k2yLgnDw*3(wN_1`NlgBH63m84 z*;mYPl>jQF3;+48F`Al@|e*1AASatxq)|#yvVV+vxV)Qu>_9+!&)mRIyzRZQJS{~ zEitw=Ffs(dilA{`Owqj+Uwb0hWT1Em(8rKd0;A14pUb$Ep&=%w?Db@aq>l{A_rONX z3Px@XUO4pzo_+NV42lBx-F`dvtgPVSr=G)?9(@eVjLq!{zVOhaSRD+oHXPy1#S573 zNc#_0WN~#6u#RVdbA-$?MyqnKuBEJ5223YX;4BkRaI(`1^_{WDWGtq;fB=ZzDT9`E zei&iuiL+uaPJo9xf4~^+hrTaNxu#*50WjNCK*uo7ZM=zndxp5`s-vJsfsG4i@jIXS zE$rX72Ip&#H{wjT5~_GP09IGkKV0SEm7QBLqZ|nk*XW6ks|2W=Y$?E#aWuQOMizjg zWMsMYqX;DRV>pnLziLMW`9RGC8its(E7(WMYP2NdrJ4$G2olqFnb*?3xdkW#NKLk_ z0(*@UljXrs6r1gBi7st5A~qm_Z35FV&$&HmH!>%Kj0s9085ZB5gAtA&xeF(*{~)fu z?mckM!H0;)VIhdCPTT)oSsEpM`#If>==u@lX8_-j?~ql!Ku zGlaqt@{%yf)NDAg3-mPHst+J*C@T255x=A9!Ca#!xZ$K8x{)v@0ndQAS;|!jFLQ&c z5$3UD4+=_e2Mo~iM|3rGqQBurYUO;FRdoc{JVp_0kAy;?j$g zDlN&oSB);V6Lw)s+N8U8r-jGT7t-O<0SGBsnwOV;@mj)OOv`_Tmyxz?-{0h0D@FTK z%ASfqfd0n2v`4>{ieIW4&ZqU;bWuBc0#W`;IuaT7WR-6Bwdb%7A)fHuQFo zhqU~;x6&Mo^Q2RQ&aW}OB_f~?_V?x5@(vDP`~EHa72DzrkeVY0S_41tvRUf}B}NC+My*LNo}jX1I(| zj>HtXaX>wjppRjdvJf>`0;s5NZ(Gx4cmWNAKR^qTfQ$_zUz5C=8gO6?*Tt0-6GvB8 zgEdBdLEGLE2J1UG|KzV?`;GTwxbGd9b z_>2rpP!6QnTW7>lY-O(m#u<#-J`aB0>xO}; z0<#9{i$2U>g0bS;f3PC^Y$Cwqz#!B9_P}fls2jq3Ci2)8vNtx$fCWpP<}*E{Fi=Y| zU~3cDbM!cl-1I5Q)4M23l*`o7F-RwL^lNGYDM|K4IRP1o8S)^Lf5qDY3R(YWPaPpj zkj-w8Zy}mz+QAG&>Io@~89<@PHY5?yth=ue9HZ4ZIQl>^5P?@$40421C#RUq&SGyl z06a%~?jR|%Ap=83y+bVxT?-cg8Sfb{qMU9`}dFV z;I~eKZ18^;CLF^O;rSQOmCu!Ngz0Yp-v^vk}IZvP9W1iX6a9`pzpAogK%ZG3=$mTrHzGL~0HJX6B6m zGM*ib31U-%bqvb^uD z&f(uY@BjvR4q}6*Zt(7#Z@{o9@Qufx#>J|^AAb3(06dD^3QJD?AUH3=jsTQ}nBD>+ zN-874MfR)%c_DyyW`N05#=9+meFq8KTLMm%C9rjY<0&L^j#0>i=x7?C4BbwW zPXO2$jM-Qz{yEfaH^=iYy^P=b^uNMnI!0zP@GCvh zB4P{%2?(!kZmkAOQPYjtl(ADYsyVB$urLhd1Lf|O=Y?7);CrntuwX1rJI8Ic&JNQ zUsS=5%2$$R`|6U!NE#%PWYT_0KJ>EhKGUg4?+SgCYA(Ax7V0DQO9l~oWoF%g^xjFb zi?3cL%U%JNJ-L{IjDPMt&wb=z7bZ|k8KIcg+q-!2TW>D#!c*ZUcjwpc$?lq~%(LY8 zCG+@7N%FE6uUv%P3x%L5Tqd2g0=o=~{!dpb&)XVXyXCcH-tewpmL;&{;wwBt-^;kW zY*vz2QYiwW$(Z-5Tap~unI-RE>FU07Wyv+yytSJ=45&%LE5 zeOAg&oo0$MhoHMusf(D-$!9%xRbT8tu!aa}rA#~qgS?tAM_Ns|uZpQb3(~0`N6=s* zUiAmn!9tYPxLk!LHULOLx8OxF6o2RCvXszbE|*(>Ba1Nk8$m*4-Ni%BB<+4bEE)Cj zL#osIq%Sj)Xvd9@y{9gf)KnOVY|n65kY|KJ31m%!lMnwU>Y2lPKK|d~9UuOC_@n>v z!#IEPaoC($e*)K3!UQCa z{=%(61PjBs45mR025Er9W(Uztm@T9%p*_H@zAA(ghPA=JwlLsKDMSO;h_A&=Ee$3+ zjGe6(WGhy5{CEkvnP)&=5=QGlv-1Src=QR(XTJ$!Gi1a4xbX-6d6zwyih~5)7l0mN znso+N;czxkDgg2$IPYcjRqrW8U}J;eXq1PJ9U3Yh>X?QB?0}4q3rtt*y)|*RZSS9 zk?ikWE#u8*z;v52-V&U^<|VwFgUqm_2X?HATCv~GUs5+;Iiav>J*Dr2Lj|CV>800l z{4kMhGB<)0Y$dt0vq&c*Y6E|0CkxB4O;60P+DgT&!ooy|k%HwFJ@K}(G51O-uV;ie zUz_7~H%FmpIy0+}3&w$C28R!?;JK%!*xqaqf|3A;^kq2U;)NOyJ#rR1mul=^8{mWY z9mf6lzln{rHSCH|asxZZT49*^z%~Q}MnKAqb2D`?DzStq9K(cNmXmkcDq~D1Y;Z^^ z&l9$`=D4&y$Kk^(7>{cK&3p(>B3n(Prn;Nka{;dmz`?vx&7r+Iw+jMY+FijD9oPUX zyDFGCGMv|lM&3vB$N>6%^YN3YYM>lR;hhRFb_zP^fb=?W*l{#VC$KJB1WP^!fnf%k zroq8I>-f=+-ib-QjR(H{49=XHA+vpLh@YjFj!+7rGtN4@qAHw^g$4UVj!G;X?q0!f<}M0@x*~+bSw`6elj&34H0^S|3c`P zD@$ND_b|k;F!~%}R@Io-Te#}LK3v)wa(;j8Lh)Yfs_`>NBqtn!L#Y zVn(?FjMn7jq#TG~7N@Ba6}XxLTmhpsF$uOAqZk4gH#{DH>S^TGAj>i_HS+|Mi>A=1 zzYy^gDZsSTpfzMf8K`E6rqjNLnu2naYgXBiu3?lzMp+Q%jwPsx$}ehUA!mkw_fFEY zrHtdIk@21lT4jac!RjcPSeUg|*<+-Dm#+oj%QK*!%DZM&&B&Dr7#Ob8nY3AvGxDs} z{Jd(1-izslHR|NK(Yi7KuYMTS_vUOe2hjjkJ;Cv---&nK`&R`3(QzLa-{cBqUI25_ z>7tAg4nViA8R;e}JWibtT}3*4=zu_~VTa9)jcW$M7(z}03>Jk!S%}kN8x}{E`9YSa z%Dv**adVI?UotoK)H*QLJtLYXrT&44TY%OoYaurVdCq7&!K+tt1}7VbxX^VKFSK>- zHVQh8xGTOD9@94C;l+^4ltKS zWnQ}D<;&bk+NocsB=t0F1fO_I>yUo$P(zYJ?8XvWLQ`s|2KT=A%Ad}kUeVgNqGY~x zTO(!vcdD_poXe{bhVE{g&BZZ8mmN>>sdQJpdt$rimkO1I2JFfj0PRNqGG)SBxSu{4 z!I)GArgQKL>F-+IWy5euzg+IsC9hoeS~gEwu&Kx1Wh4=s^?ge6Tt4X*3y5$mK%M6( z`P^Yrq{~t|^>(SlTWWzs4inJvgBQ(NV`LOLRcPnkZfgjl#EAC;>x#o*4rT4B0cmob{U&wqMY+rE&9olf_My0MBgj50%&k~2 zEvWD8mfLcq^$+u$J;UoEBWry^nL<#IP(=cwi;?tjF|gR6_X9{C(jcFt+a1CYOzwcn z`apD9g`^MkF{5cKk08pDu%~p~vI@h&e9R)~8sXib*Y)}){R`6l4gp@>hZzMSGY$ZQ zj)_bTrK^fjE3*)i4YG_dEX41&aT%=1FsOW*~iJIf>O!!?DbPHAY5&CP4OCmI&1txZoJ`iGYSp&BzM^k(fSnqrjH| zijnjqGl30(*<j0bh-Tz#2W~@A4#cWA z)uB=%MUb3|MHwsUcC?3^@R!;mcuAYGh*P)EO%?4j#$y?pqI_ zv?ZRLPv9Fd_4Qr=kGzxyucyGqnK>?;pNao~+G8?pz=pB9&*R|nAugUFoPBeOqM+yx zhtw%V@TVxwMM;BKKu;N1+d=B+oX23*A}fLMc8w5uS(<>O(n{p9J*i*~Ff20(f}j(M zJz&&BDTCt#^{ghn!5^Zwn|D4gVxAMO-6Ej{lu z>D$bTkekl{uEl&5%*Yhbbyiq{P;*LGN!s492y}=5b5(=eZ#;r`-gFrA`Y;}Q<`ho* zsrYIjLc?Qx_M!W6!&OJ{^Pl(#{+}=Y1=iOMPMjFv;?6}hehn)_i3IIDV|{giPk!`W z7#0N@$H+3m=D5a-C*Q#Sy=&ON?=ULg02#wIPJ94F- zH{FD547K1u?X5>~F!p)rHc=Zm* zM{&hJZ5Ze2yhyFWB8`^)#p4g-vS1Onoz3%HHDbG2R|G0 zhEXaoT8x18{o-@Vt8-W;z~qGCY5{U0BTO!4@J&UiCcxwpqxLmc_ZS#!uyd(~F$SYm zW%YUnv%|bhyFwcRNHEEk!&B#ib3r?A7)+#uuCwQm;&SebEXVUH~D~v%QbHB)eGH2A41UhoH z_}Fo^2$@U+7+EWMR&$XlnFiExHG@ceC%)s(kK?A>K8i=a`LAJZf#GOxEAJCg9od9I zY)9?B73CcTGXWvedqMoijhrgSA>DpDosE5}e5gnbz6haaNChN{S5aCFazbfa1%-4y z32B5iFqA2j!}#vWG@weIDlZ0Y9RO8ed2!zAy;kWNgFn&?=lD2l*d1 zD<(-VjRd)jn6k9pl53e?zVN6|K8H=jZb@~%hmA)suZ8l&=P8z>Uxr=ZNz?2OvhHW@ zHb}a;OAEfRAx?e^n}f6tVS}~+C@!F6%M$OEJ6Z4L_hg31`nPkh5dW6u0!5tHa^z;fbIth{=QzRb>XqOIkkLE;H?p|+1#;Ml5`Wc9A#nAHRhXH>xie!JW6;cXKxR@N zlTk#C1B@|IXaE2p07*naRMRL>Zz-%a%pl`%@bCb4yz>Yie`*7p7wSks23jBx0LT(n zM+LGB*t0%>Ke-8GtN?bE!`*isgUx_PzVin1jNqBT&d0?&5sP5XKl`JT>hZ zEr-Qi1u#e=#m;9QHdD3~0nry>13;Nu-U;YuNI<%L&GX0$l7JKz*xGS;=!rK_nK5oZaSKp*oW3x}zx?d|$cXSWKlp=K zUl|EN%W?v=GAJV}<@r+^_|jiKhRy9QtgP+Bq^<>Y@=|tJ2k@W|Zp*lJ!GB``piz)E zoFeNp!Zro!n&YZYHi?o)%D~k0ILsAof1_6xoxtDzi66xmAAS_~Kk^+IL%8P1VN^WB#^x5P8Nsev zI7LD7vR(K+GpZRd7h3`3mQc@Y6a`^0kmnjbO&OFj*IX^{xEe4SBiHBVGian&so-c` zPGE{$0;g>60Jh-B5H5$(W(^DYJy=)s*ozXBBhR2LL17yfd4Z;JxOAa`H5u-D$DKI4 z^$IR-oyV;=z5}nkdEPMwGy?A6f6(Zj~UZT zQnzv_<_KUB*3hxc-bdddMShCD=8KTMuoVrm^%6X#zbY_M5 z$q-X$&th6!R|4FYnH&hrW-9T72;P ze;McAd=Bgzz}W64F4+Oa%_F0;CZ$(kjj0G(xLRf{Vt|g6anQ>$&aVzwL&8!qM6iaU z&zvGNC<}u@PNJk{trDD`Vg_9$D8MTEpoH%3MkpzFYCe^gE0#MgyS7!BTZSYMQEp}Y zupu}k0FXd$zm-qgs)K=rziC@0Oq7_8>emFd81dibb}E$7t}2;&~2YI zJ2^9JBUJaZOu@3S@#SU5(@U;kC)7`7e}%JIOB;cA$a2{fl=0u(aW*mml>WT~>_#GrOhT@8-+(>au%FWk+b@PI-eM zOuR!<>t`=x`})u{fV_ds8fXYe($1gpG4W+dn|4D_^Q5} zSqUWASr1%yoeX)kpj-Q0Tp%q}3jusw=JFzn!mw6zvgZV=NpJ!l0$n*w5UZsG`HuPo zwLo4Vy+nWMKbGIC3M^)GO)1hF``dI23q!C z=ze9yIo~n~J(9Vnc|@=>tS|k6l8%cc|I`;GTSSxW0GPrh^;JE&U?68@O99axi!l-` zVLab~C1A9A09)rz2{SR%NW=tGQ#ssj8fow}5^@L#X{-iPCBkrBe5$h-kdTe+X?&yQ z(*0SC$ZUilW0Lj{bqy9l&^kF~1Z1SHxr~55%ni;w{=cGJKMtE;4Kg`k4!|`VC`Y2R z*OmDGYv$_v5C8?T69~b?Ir#T;wG1fZC1P`o`WwfZ41~O-XzEHv7;5m!lw^i?QlWAP zv2fJ2vs@M63JT!X9w@bas*2D|p?id6et4*RGvMqq{}!{Y58#fE|2qu#-XLun#?$52 zq*R86`J9SfYOwLr|BIFVcVf@6TTl%5fh_RyU;Ott_tI~}RTp7%z`Grk>aHrVWoI+M zG}6Yyk&sTn0){q~5W>jH9&A1ZW;0<|vJ#_+1T-yHl`xa*eD;KTCgaK}z!>&$Hj}Yg z?daH6@VRknhR*;Eq%C*O2*GW%PRMiddq`?k$P8R{%lmNeC;l#;e&Ex1_8Wf$7$Eb) zRwOHdPil^Wr>iC=IFEZYC2oY|?!L5HYeL zke3FxUAG^vo}6GX$Z+<|6ty^Dh{BwVmku(r0Qv^8J^*65>)PSq(G2gt=OFI9^C(tU zN<8uSIaG7sf(9H;uNY13aro#EcinjmWl1=*F~w`AF2Ne3z?{RaHy^;UqpNuSmGjXD zgt$6rdp1>Sf&&j`VUigGiJ%-nTn^M#1rvh+%{`hL5)oHZHBiAy!i{mYOhlu7=(X%mz|o%I`6S>eHBO6 z58#ofUk2Dl(yUxZ$e!#%vlwtst3qqDW*KUE6Em7Ngq_V}Q{LU|GPkrtv83 z5Wo0yzlbyE&fx61v$*fQ@5j%5@+U!*;l-0L;wL}$v$*@tcjM%#SFyRZjj|~4sUQ0p z{FU$j5Vp3bc=oxcVaTfKv?!bG6xh`H5`4R`*Z$Ty(bPiCFG~Pk%4t=R1PhC>o@?1N zHBonR5=ZLukyV59>O;|}|G+#K_U~YTXx>X|2TQq@uMsw(Nfe)I?oiha^1`T6s6I!n z_KnrWrUs@{z!;A_2Tr~6AWpvYbsRnZE?jfn`(97X%RxemW94tlHRi7_qwsYmNY}Es4u*9nap}qXV+`lz1@XhwW`6qg& zwo_+dEvB5{0SmX6misN%f46HXzPd1pLh__zCH1Jdd;T#AUY^gD`s914&aQZI>f4j?1-#i$dhdN~s zAD7LoM2HYAf`pv}`m&jzj6L~{m((F`dqTG{(mvRAoNF6}WG**;D5--bIp{K0oZV+e z)?TY?u(3D6~kcei$t&ZNZOBslM07If-&G*T>fyEbDf7}9i5nT1V%hNQ!pns)lg_~*c&%oY` z48r8SFs@>vNn3~UU`+dSFkM}$_vqiw7}xxaRsU@kF@kEI1BIq#jQB;{yZF}6_&WCI0Mt$>E* z8o@UNR}rWX{2?$h=kr!RuxG3vxgBdq?!oxn3ov$wgExKxqy5*zRa>z62&;$R2axH? z=yqBxd=B52{bqBEv(JA9&wk}UhpTJU(+jwG@+$x{s_~}8L;&L6$khV+O~(SFu|^24 zrn<+n)Xr&^abj|cJ_4($Vm~^E@{+K!E*5I87I`!s1CwnbWrXn`g6rgkfPmvmViFwW z2{%$kW5fb%Iu;Y+Y6i^4vUePjITHSHWVMtp)6Ibkp$j7q{F2TCIZ6RZ24bWdZ9%-8oE zuRHzp=U@N&Zj3^m{m~4IbfH&R$+v@p(JuJRtbrF0K|c!43l*3NlOy<@@aj;33?c9# z*^vNx6yp7~)L}r9$`s(KYY_r-*T6I5{J9#ZPfsKe0||N?CTLYL=8aB12f#{s>M3xk zX>iI-z!l+{$1g$E)QIF$Sda|BHLH25tX>pHGEh|>hmH>Lfe#+X`dR_+ft_)Ky76!t zN?ZIa$myV0zWP1yK92Q01{XIQoP6yP42cPq*~W=$_Q1fRYHC=U2or`xK1EQh z7|cG=OQf8Z7<1cohx*)m{r)`f@#ydgdHV2SG5)KYfJHIj0&PL3uD zypjk@Ko*4QWP+xdATLYUtN4ti1^@?g@VOogBg|#&Ya&wHGK(<^H>`W@Y5gt0RH&? zZ{YOC7OWu*${gn}ZQ+6MK81U3y%|RjuHqLz@iF|y@BSekdg56$uExhd^Z|VA`)cxHS|GOtyz>~3$5VVpb>>N@P;P|0OGy0b)!+Y zgK7F`Oc;WfBnJln*4h;)q9RjYJWb6~W>7_CV^RWo5YW>!x*#|rRefKgkLO3mt{d4D5|hO2>r zmjEW2`j=qCVlJS5RX_s70^-Ml_28wh#pFB7XBH7n}tm%-enT_=)F zO0rvT75QY>r7|=*ur z*U@~i_e}8H7uL#9&SkA1$39muB;+Wpz{-fc?{*5JN zqsw;O;*}&d&$B0!P&rt^5whi3;L)13=5N7>mP#gt*z`w+NN%|x}n z-T>aU>s|^hbe4>8Fa66pH=z2Dj+tPZE9r3CP6Y9fnbaxXV<8AINm|D1{xA%P#PUIa zP|?SY0ym`32DTiL)Gf}L&%;4-wg&14_&(x3lRG;@c+>VCZIr~UxWdp!Q)mr)D= z%7y7x7HxZ0jiv=@w~mjwwBNjdQPJAc0X8StLfe~wOnWobg|+W&0kF}an@q3+0GU`P zu>q)T)0oe6&N54&#u6Y0>^b%!9KQ8uasI_G;K=R&DQ@}Ue*ut%cXO}{RL@?yLtS)i zrvoke^$z`(=uphS@pt_loPG9p@!VH`1%pv0J{{HXWHXk2u>&0wUQLH<0iJ{J%tnE= zvH{99zE9d+amz)Y_5SL za~W&i1JfO4XNI}NVr@4Vwr1{4B`)uVBab`-#@kB1G6F2t6>#vX>#^tH37k3kB*vQ? z@E+KF^Cet3^)Rlv^FNmHtaCfFK$%e_LXg$HlJ1uV1&~A9EOnU_WwC1eX;M1W5IAatCQ zu-2Eh9bP(l2`68kfZ>s6qMmrCY!@S&q^c42Pe$rn5CB(zn=)R$xGj$IS&?W`#MZP6 zFf+qz_@hi2M&66y05M7n-22|c*uS^HOzqsa$1{v44c695+;#UMJpAYzn9ZC5EFO2= zaTJFR4)NG`FW~W~&SCSC1oZH_3Xm~)@uf{n#tu#808 zr!-vSl~Wri2ST@=%>}ry8KG%fe}HA)1{M&;smIV}7!C5!67AU4g{`U?4}Ryns9lBC zwRP0q>{%n@<{FGbL1yta*q)GE65(Tv znh!&OF@{F4YGjZwtc-WV8ZoSArtcXR5{x0s4bE4?WlADQf+7hL009yN0n7jx%m5h70JG10 z+gtAKKKbLE)7|%dZve*+HTd4UOE0HSpFaKTvs}E4-}v0;@#UxXV*jq406VO#tfBHA z-Vwg`)N|OgZ5Pg6x&TKW?bIyj1dxg$;0NCI5dO;j_u(5)e-D59wJ)KrBsomK7O-zk zvxSXTfXTY1bN2$eNgTqM7ORCNSzw!q0BHT8x(`QSsV|GT^TGqo1fX@{?TJA!8{zRKCGyzwo=W0c<+0E4DWjHNAd4I^N;cBD^DY~wZyjQX-4EJW3mRc%?LN%_CehDuAjs= zzw!@p7b|5-KGrd=K|4N^dW;5)Z z`^m-ig0IUtc%=`cD3=Pl7Yt^KZwKg_0~qqW`e@_Nd17o{JFj$o{-+qvq`hz+N6Jqb zK()b2=s)r^3#`p;TER5)9xIEOZF6VsF#TR1a+n=E>I{)#d^}Sx7hm3P4N5#!)CJzy z&hov~T`pwZOa(N1p=cNt4|K>!;?+V<`f2N;v(0^+IUXXwk_2?*$?S!h&xSI}xiN<^ z%S%aqGO`t!mzck`hR^W?)ZQh1*EW>F0~}?K(rV%Z=z(a*f-XvEncx6w6)e<&pmHJ> zvLT6D?$V3}HWhz63x)3fBguFN^$~^a|Im< zX@5!%DFh6Vt@ONE@@Ri-4^fWetlaJ#uoi2fWh`YBdK71R&dw*Jm1CsX=~fpNZA|)6 zIcJPM+6MwsCSp$l5++X!KtWcaL(uk_Wn%4sZ+-E1@$IkuI=lxipCdG5>1d+6fh}I% zACDMKLvk2CK(YYr4+xVrF?<*d#m#elMTu)kP*jJt7=&J96Gpw}o#>5YTOYLDtbI-% zjDUC|1t5kg0ARYdidUcbZJa;;1#r7A304R>0gKLS;v7R!bS@DLPGgJ5D9KqZ1XZOv zmc`F*&7GR1Y+s9!%5=(V@hkJqz(XPhq()}gBKvIb3C)PX)Wc*vh4X|h`|rVc`6xoO zg8t%uY`*$IY`^aNG1z=9d|e^7fUgFE5r)h#39zTkBer&R7_XFVc0Dqh^%e9Mci{Gq z{1LwQr$2(xCB-e4vwa^BLYi|Ug<5_E%xHhm@oa!9eyd%dZ z1q)C4o&b6!P##<1z1rtFXkJLZMv6h%8vv6vVEH`2l>pOqUwKGD$LRzqKfqw9c!7GG z9lZ4(yz>LUh_C#+U&HIiUVtOuLqGAWxaWg^4X+rmIWuf58J!eV&MRWxy5Sii2|epWD-S2vVwyGRgx*0&`g0h-yCByX;JsAOr#SR zsu|Ugu>Yz>Y~RvDI}MWb15&2AN@|L6T@-B-D+>YZe2`Ao1NkQcTBzkbz#do~Pq4T+ zz;IZj_Q0_dmvQXaCER)Y4%~O|4xBzW#>GqPfM;B@e>3*(UBX{}?NuB(avqaui>h*} zXvt`DV6+Y6-1!m6GrZ56I-8wT37yD^g(54hTu+D|g3!v6!Z08iN}(>7L1^H;%@FzH zNdv7$v3`m{tf~3~kEqwp%sVDDGA8=Rkbx-yYNX8}s9@CZZ4fWu zJujxYUSm%H5?2pWSE^4JdGE!tD==d^4VX?M>Ryh!BQE%+Fp2gMl^-Gpyz8bru{0dw z&5P#|;V^BoYc(TV;+ZgPTj9T^jo7zq2L@FS7gts>8c(n==wW>_#^PXrgL`*l-_9M_ zw6KUb&t1f^Q*YqaH_r+vq5Qi(YVg8quVTGjL0>^6=)7z5@f_;3jb6JY(4TA9}3N;?>c zVli10;Ltf>y2c0-VZN%;Q(w(I6@v&|Uq@`-L0DN^#n}tzu-M;<=``TzYe#YN%@a6( z@e+3J-ie*tw&3Y!zJ!gCs@LqJa9%X$Fx28`*5v9MX(q^EikNs-kxSOkcG@)1=f z*S#(jzUHJz42B9IdW}yqmG|nt$~;|IrTnZsFf)2R0gyLs62Nh+R-dd&Lo*RtgCi<8 z#K%AO*Rg;9F1+^IF`RkxFhEt>fX5&SOqc=Z7`?i}w(a}T>n~y7Rd-@xVLLox22N&n zNJ)Hmc2Ze^%#yu)DgQQ$sk9H6*@RK|wXRmI1(*Wvh(Jy@DzyiCwMWmpblPht^0tXF zbNbREu-rh+Yk%5vsLU#@W2kt*6!Wxsw+&HIPK@1WA}CpBCm@B9G#~C%1l@SbOSU3gW0>X$XtK%eTK~Pui%F?&IQe9 zpTE*4Jwr49Pyl}klz5ARplvO!31{e(u9S>2TV}~W2TxS&YU zn*2xtzA0(qnHSKVpffyD&NFip>>!dejVj$5ulO-AvUsi>-;#F5d&Y;U4!li0CH;`C zZ{Fh34YG<#CQsU(soq0N^ zFKLOJ9r)rsN{lii%Vy+(fiK>R>!J6AO8*j>Or_2mB4DESz!^V@47PEYm49=`(P2X; zGr!40wpN&&PwtqGP*;S0&tb9eG3*iQN|NFAy#~Se5+B&Ye|V>TR9)H3VsQcG|KtKS$+81DiD`XYVysLHBHE{loWBBZE{udy(fcqZ$&oCX2aPjP$ z;!!j+U(F4fSrbsGWgbTXc+B+{EC`zw9%4N3eeX^;gQM#t|s_O_EM9P(jLM)-V+u! z$=?WI(ugG=Pmw&gSogIl1fCkhm{p)oZQ*V8PMeJ6ccn>Sh@^eQ!h3BaZLe#fHzY6< z#%sXR_MQ0YzxC_bweNPk_}xFp`#Sml-r7Po14(Lzs-D>_zCOiZ0ob!|0U-jn-n0WJPF}>G-Aj1KeOKcvUwZ|oPp?UqClK03 z0ZfW$&0H3wflOIrC~$V=1?*@{m^X3;tRf_Ni05PyFYT0j0_5p;LIKLjO5>^`jvg~Y z2)OsIgShqPefZ47PhhgD) zR6Xfe6&y~y`364o@Wa@#eG}lvsB0&ue#DqILhs2KSYIPa217Cq1~7Xs3w3`W*{&S{ zv|9zdXS5pE5MJE;C#yo|KtMGR*V^?}fTL#1j)ZEF;URA99H9Yi3BDHftU?bj9D4!J zA9)#lUxTSduj&H~tgc?f7r*{F#8AOkgsKwI5+n)-#R&F<&s=7aN@m3QWfcgt27G>J1^L?+grv znsvH@#`)B_%?^wP4}bCh#Z^~dk0VDOMvQ%gDX_3flFWc56M54B&Q&;j`Z@gN!^g2{ z^B!#8vvDxyh4dHc(NShV1rJ1 zd=ogu=m2(QduDZpLR@e7%+^^x&l?}l<7@pW$YlNOe2gW@fC{@-XuL%{6&crZV>-v$ z7T8#iK9HW#aoaFwuGFt|uUoLS7mWFqPgg7`Q6$&O#n`+Z7+7|8-o2ts(t47M_na5A zt8IF-)6=n#^0wrDEekSlz8=3fIru{_f#$SwO`LXpwTmy4sT#`dliDT{nXKoI% zH`|re`ZOOX&5gwAoavI*g;`n{Zx!!#A1y%k)NgdIr8Px?y~+X33(#j9WT*2XXq%nS z?Ms1-6&uQNi=uwPO<`1Q_UDGwdtn@1d0qaWQ{+bJ^J?~iEFby5j z`h)J1*x6wirF}$dPsA$rmF702WsJfSobAZy^g#Do3DTzZ&cH&__88r?9*%(TE#RYn z^&epO)wkn&kNqK5E}lY9EwI`NQU+-N2~6{qx=t!;QK>~&tDxXy1WZO^G*i_`)`?V! zF3b$ngC$UJh-UpPxR?9eM)g{Yx(CqfJxlK4WsbFz96HM^54{827J$4aIrQQ}-A<5V zxElzQbW+wmf|oYnHCJqLmiRa{`Q6rz4 z;Hm}ezyCkSw(EWn*L%Qo#L4geG1f0%#B~q-UF^Q`Ls)+ODZKLNe~ZD+hp^}NzajI# zkU>dy&CWZW1C}hqz`Ydm#@+?sD#DIyKZ;kL{QJ0Y;w$JaEWl#JJ160o0@NxlN?-jz zLDi|0?G$TWhd*!w>dIm3)*gP~`)ozOe$?h!M<%GA*ne)v8fvm01|tpn%n7K%Qw^0R|A&y<5j7 z1c_VVYfI`7g$@c#GBbw#8h75ZA8%Z^fVH(Cf(^yWZvi>jl$NNIEhYe#m9R0kIIw>Y z-hI!F_@lr029CY{rdq8zaMTR0wJ2B#uvSU)rN_R7eY>_`$JWiV7!}&y<<&6`AA225 z16;fJD%^eJ)wu4e-RRdf-g)cwc>mouq4FNzc=l-Gt$we@w3$jU+(t6nTE^N*U`%6! z`)|4tAAQ%I2n;N*uj1*Kjv}%gS0?L1sOdLBlP^Sk>&55M^FzeeVcNF1{ou9OzIihq zKm4prTBVgKgq4%n2ONR(>ub1X&kp?jCq9BhS8u`Bo<4@%+qUAtJ8wl*ImA}nl-nTS zVC5XPZQg`K2d>5G^XG8#+$nf3?VD;~-Kxjv4P}hXbTfD?s zP{6Y4u?ib;pG6PoElNTgK&(xpF38R?CK}+i?sFP9hAiBpWqFTu3|kNM1`&P$tgk`b za#+B$=~+g-1W;cDAQ)&MZoY$o0BCR?Ed{ip)r1$SppF2bPIDx(Jl~Fh>1q@pZb9bA zv;hW76273S0oMxu5+u_y4b-%q)KwwMJC=+kPG!OOeSj$n%{<`xjGhzuIMwXoNQ zX%jOh6In<5walH0bP@85;UZ(aEu$zQ2Wq^}nN+~`^q`V=>GRuDF)r){^ZK0Z`^CTOW%Au|$ZjvdqsU4IRu0jlS zVtd&_ZPo?00kRI546$$m_Fj6oc*gpkl=5PsO&I}!Bon;1#e>0chzMFgfrC0T74@N{ zlToHn_BX7m1#O8;o)piiq6@pPgi0Hcv_Ub#39!#mHh!KY_OKkAWUDA^D}WSoVgs0g zxBl>)XRh>nrsxfy(i)>IzvRu1VoUknY@NlKnm`3J4V~9U#_Tg)Is-_Db@S@n*zk>Y zmE%17cw*;bK3SUxw#gJi5Y%;CLzbl9bNY7W4`tB3XomqkR~oOkE5AcaF8Vi*{>3vQ zcBlv;Fo%eYZo>#L7~*X$D6i1gD}EA4t3MT3vu03nGxJ*ho91)zy)!^_zEe@aYB(%tI`Rieyw}oXP^M=lt86Y#@VE#acX4LgC6L7HS|_zhk6?ilpjP zmRX&4J;hy`Gf4|}$$c-I3FQM@^ES{mQ_=P-2G&Wu=^Qtg4aL#;evT*sQ(~P&;FdG^ zqh(eV;Na3YgjPems8BE4VxUj9CX}|DwVAE6Hhx5$to^(ffUsAIB}(mbf`c9*Cj(<7 z1j}4x(^+6qSB#K&ge-?kVlG{P`dJ_lOawdk#z6_hPg<#@fY;uz21kW0ty7&z(jsV+UfTRza*0n^wy6&O$CJSV;gh z`6rVmY-|a>&!`vGFv5U4N9wgF17}GGV}yXzq2L2#5Z7Yf0Z3qCa<^3ywbNNl!=2pF%*9;@z4KUxFpDgXnGRXG(aKIau|4&v@h$kv20s<8-wwPo4&p(lIJ*veR# zmy*RXY&pAsGfUZ%v4Vnj5Cn)NYGDM|YK%?q<$*X=Sw2x9TZgJQ04|>kc=F4?f$MJh zC%F3HZD<VI)Wb6BT|*is;Q0pHDd|7Fr^y>tGVftt{N$ z3j>9~h^|mJHA#T)%lqP*XW5o*GaLc0n>xtiT{$hQRplgF96(6sBUNC6njw=a0nEa5 zVG69R1zcL5-~;bD2mm;G{36Da20OMbV#oF&z`&sIanD`5aL4Vt(6%wfFX`0|Ax8Y! zBa+afQsB=l2@?ovC$dpaGh{-VtE1A1H0E9-AYlUE6aZv>rwu@(7MTWUFvOCPBZI4m z&wceNoV~aXp4yOE>?KYy`ot|=z^}k*YMq!j)R>6gi=m3jo80; z7p}Q_2fp^?bLjQeGEA-enlMEOEh-1BjvHKFTSHw{*tcs5Pd#@67cZ}(SJyan;2QL6 zkF84rfbHD8h}r`$pSpnamsSx%z-;!^aOUzUabs5V_tCmo?KKld&oSP6?>*SNb0>c5Pd^J%&6FHtw906D zz+z7qq*lPcs*<^0_hpQy6A^TSA*Y3_GS_wzLGBz-_hi1b6QN7hXZR`LMk=Uu@sov5 zw8{_w*8toXkkVX%1xR94VEBqKov58kRQLb1Ymm9ys{n4YkYW-=&$Gm&WIIJvt+ziA zfIrP^CovVBp47!4hZ$Zi9S57_up~N}^D)Xo>b#7n@Qlo*x+k(LMqr?E4*LU9QrcGJ z&_qun+X(bYtOJ>Wk($-VDPv(!qpmAVM&dr*Hb5H!CX)$b9H6R);JAjS0UmhZC$Mw( z7POjOjn$GDkUE;+C1k;rF`dc*jEeg%xKaXTh0-WIV~db!qj?ihigtV7l$&HJpkrza znbl8TVzJbf#3f8htJcdT%1eZtI$CkIH;BkeiqkeQEBI^*IP%FnP;8KjYi31g8>2QB zr;L<0Wg!5e&}@?e?FcS%0_vRSr2LheoYV#CzMlXS1DeX~F8{VQ&y-fPm``mWB$qSN z@k*No==#p2LFh%E2TOpZ^>Rl;%!dI*Ydd3KuKlxM19czoZsKRlDmOogPQ^9qmRBmV z<)fWe+CAP_cbQ!P15gk(}`0VC& z|M-uAr{#E*_c_Pam(@z2QC0dUEk8dm$(r!#Uh?Hb=qb>k{cO8V#nEj z#pH$pvy#misGYh+SCnF$RjRX?HHy=joQHzEn(N%n>ynHv~wHjwJ!1 zmN$3puzo1r80Y|X*{!`8AAP1X_%@bgfY_Wc0mXAAxNNUl|GBtUR+V%NyT@wOqTdM+ zt66P#)V5Ty5Xahv5EP8H15GW%Lz9Q-zs}(#7koTNh>;zT+WPGa(XL~ayfI4JYrkF5 zK0;pSV$r_TWa?Tf!F6d?&eiD{c>2+QEm>g~fMy)=zK{M7*tYi${MP^eVT91;zB1sr zC-iQnAOKBZi5su|?++z01pvv0t9m{A?0P%gvLAbIyBF8o_cJ*9$Ug&Zma==A-~yb% z#=4TR2?2;>?T43si@R^3GgeZ5m;k*6V7OD>ZO6b^>1%sKXTxE#kJTmB#(|OZ3;0Yw zuWrj0>dP$eBsT!1Fli}dRxk6Xs{x(>b$=VGdVu4P{~uUBeH4fP^pluA^eb>~1AYMc! z2V1qQn~pSgqZj>~2yi{wU&g6`aYok=0C>XUqJV2-v+z@S?R0E0(Kz(A@Xeqv^#%nM zLkmo%a)1L!Zi*N=Wul&}0_)Sf*R{jSy00F0x^HQ9&1@#>mh3rUUku>ufA-IC$Gbm; z8}E95%Di13`a0zeB1oBFxSBBN5r&#++`BaG#K5PNpZeEak<%eWaj^3L3n(NnlqBP7 zsB&V$!$AP3f^mfSTqu*A1gO0WC=Te22)b}P2q@!h^Tm0AAejVU*}R)TV^Y&_bt{&! zU@;lz!pg}IUeXh<0L^~HfNqMw8VgK800~klg57xmyCkC~i?lZXd+Tg1j02*qxHyMn z$1Y(!4Im<%KDUOiKk*taTpj_QQTHA8@7{#lZ`^}DI~LKl(zjvX+3Sc5Jo1+(aQxLv z=+`3YW#U?lM-lf-(1anvh#_*aFf5(}shBia@i_QhHcnn;CIlW;X~d36mJUS0-yCqRK(rtwtT zd-BvpFd{s835*;WbyefXKl}k)chz=0b>sy81AF&Kyhxp2P21we0|#*X^@s4( z;TQ45_YUKUXO5t*9Qu7iuV;crfV1(Gv9JKFtVDeETTkN5#mfL#-I5uD1+k8;d&Uxs zx{nl>aCKRLDeHGti4ctpRKAF#5j93fOb+rOVoWFUebXkOZ3&G6*DV1qa5}w=L6V?M zgUrLK(gZ9M>A0DAL*ryRAO$6E)up~xe)6>l@@6a&lvQpJ1P~<0Xr>}}OyM!52>MLa zg9EXOlmOkX77M}9=7nA?b~GG;0`y=`2`mVD8s=C}6B!Llq9tfQVgMG0QvZdsx{+W2 ztymLJ))^BO0n}r(VS;Vj_TZflei&bU$$eXKR2Gq0P*9Q0m-9R$FD}k8aLnk`c9jBIU8Pj9(@?XEzyYOxlTm2o_$p0|F*Z-W`!G45M06#3D?C z<9rCE?M&_}M4-s3yOS2d9q_9J#R_tyJx$&q=Kh-EZ+=7|MrUf)xl8=9`qmB;0?9|FuiDD5Yh(pt|2V-a#6%X((X z)@j3>2Rcfumd$U493j`8Ebu6Ayiz^0uUp-vMm_ss%x@!gFO_cJB~Xx2?aqEIS8HE% z(@5oYnyI^(qn-AZZ7g|kroL2M;tk66&-8zm%yG_*^6l5pFzH)+536W6~3K4O7c~ugMEa!kb0AW2D%z&Qp@VruT`Qx5FPR zTfxjIbwu3@8FB28sEp}br#+SSRi~^0L22!Ei3F*=rgwqzVHrW%9UV}EL{D8Fevu9wX85@fuOR9gDw-Ts zb)*ZHbZ8?70+IFo!m_Xgeu{A*3B=MH6TT$6L(R~K=u`-R- z6TfMBKt^!WJdKk}r>(jXP{)ez_=RrL2<=?@UZCAl@gAm%Z zS8pc3zz^}j$NmmpeEfHD=G7Mg5^zxxnkw%*iAfh4PHwusQVYz9>`{1nT(c?n(YC7B z1@YvGn*5?tojep)a}lz)t%e$kMlBDc$~33ggfnmw(uQ^l^p~J9*a7p@1c~88GnE99 z$&HwjJ%a*Mq60h4yqos0I@T7uQDlhDeY>gNmP7g}aG;JqGRS=>Pd z=vo^`wHUO-uUl_C9SgR{jJpbG|_#WXK z=T3ql7Eq2H+G)hmS1;nt)9d)ad#=MZdl%8PzzfGN;`oV6c-OsG;n^3@;<*>kN(N`M z`eOy|(iBBfk|P5yL;*+Htc>I)K_&pmHtP&inhiuH)!Mw{6i`$~U*3;SVkdePNVW;6 zynrRq$>w)5i5QJqY}qoz`|iIPRaN1$4?meKT%b(N40S#6GU=F=4;bVej-NP#s$y)~ z+{a`xhIbB4)8OFMdvW7|y*P8J!KWX76sON#L{)iAn=!s{_B$B%YXBaV^EiC$7%s0Z z>7egx{Lj;-TB_!9%y-gJ)hkj;5L7 z#Obr}BqzFJjmjx8i`is`5ODRL-T0e7`Uz}WsMLbcCE;OHWpVM|B<%nIAOJ~3K~ylr z_r3F8cn92i{WbXaj}GzFb1z6%TNPj&WzmWeXr_b_3&{KVM;}L3c?^d&Sg(!NWPBS8*JG! zz&*Ep2;Y193uq@7u+#&_DkE(1_H5uT&||>E!4Y9HiD(0(31(XeUF<1sV$3ST=}pp2 zh;6)OQ&Vh6NWuvO=z$G)v!2=~DIFvVjw1n#gO`=YPFMr$Vs4u%eLvWur-dbj%8PQ4 z6!I)SJ78LAVG{K;G-|miX_JUaS9;I|7G%ECe-gM{pRL3aqza4>9b8Ljo$!PuG>bZy@}`%& z8wG2d(K7+TSyArxMAs9&Pnj?UZE_Y>;gcDVat>{%{99CA40HDvb??tpbUWRk{9K~> z%rmJ5n(gsSMa4}j2bGj|lw(7tI#B%PqCZH{N=!radl?$7U|3aokW z%<|0aoZ=ZeM{<~}>qvH+HNC)G)j8`L<#<~g z3ILrzV!z%>AxG>H14$^ihfQ!2XP#svtHE*rOK*oXly+&3&Qh*-ow%Jk);V4^!-0}p za)fw8uoIn(2xuVsH>&|b0#vQg(u#FnduG~1*2lcWCaXsS{R|*3L(8C$1;!(G-$Ow& z8;%*;Fy}peEb6dMdspEjKl_hy>-|59(dtEd4GwZ$r`ZW;ZIgFMSKT$&z=k*{W zSbRXjbi@duhaCrhB*kP0sZMNwyRBCRzn7m5@NHj>Hv+I__kFncbhud-H<*#GB)_`hY z*hj*1Rxj$imF$bh>i~HfLt@FE-R}X-SoQWGfVFqxb?LnFu*x_h0e&s>viBli>RQ>o zK=%p2+T}Hzct7F z707AlnMqA^)fy+Yp>S)BqL4UP(0E2@ISJ9Agd##pvE4+m0BT#^XjXtE9}x>eM95O) zfvOk5#A?bbAfcpF&+%1kVY66yW|fp<^kpKNg=K7_X6bRFNLfq;@3DNb!CyRb6t~>C z1LrP`@al^gO#E{LQS~6*S&0543R6C)B zwpBLYmJyYcU}27`VLUq{?=&M$)_wp31V;gZTR0XeLk{RsB|@mL451*WCGwa|)w0krCP88| zFag6wdA_bhxKC()im^eAYfl;7;r$nAdC0gr{D{i~lwuNQFY#B(_F+HvgOu@5)h za4TvDpr?yZ%!`JZxC82TvbELb4&^yz^T^gN z+dNnq$sIs>JVeH{kx&s$Gz(0P0j0YzMg|G39P58l0yHq90I}K-U9nwisH{`T&9ZYH z#r9YJ$YW|1PX+`^f#e94a}uLU>-Pk;F0?+2B}f}{Tq|$Z^=dg8bOS1zkgkT&0q{D1 ziyuXkyNJqsZH0?_3Zry3Vx0f;;+0f%1+5Cqw!>$MmRIUaRuj?PLF=E{ zp{2L{*{L}3RPp`F7IgEA0q4W-4l|Tj=8TO@Cmo1cgs;%9dFGvYpDQWjo#`>>ce*~q z!{u1dt*e}8vo)p(RQxRX`K`#CpP`~L7RwYH_^f=-ECT`C=qmu{ij3=k$Vw-kI+d)3 zXhTfN1Uvmy+LZiGID{aUv-&5rH58A0)0z}pBQD?l^bPNEN*`C3L>{I?g_Cia5 z7$_wYtNaZ3Y}SlgubVkI=**dtAvUZRnHFhD>^~q|tFsiCgrUd@!01b`584TVTuX z8}ZJoZ^X0#dW$>on>IDU=Y77=P zr=%k`=3yf9x6&{^48UoElv?)-=iWSpCw~8jvGV2%=nb+_lLJ8N85n~CbV1^p2URTV z8lWm47<(HJJ$&UBV0%tu4+225E`wOBTXZuLtG3=yp7lL7n$ocq>qMFV&LdgmT4*(} zeD_}B>D%0IT7X&skx=OML}CS3Ez0lM=s1so)iZjyP>Z(}dFfj-0w!w>jz0X~;ndcv z0OxV=oxgys*W90CP9~n9g7xOD7sWDDg89z(QT4Zi!x+nNeiNZ>QT2shu@PFvM#KE5 zrCto`nyj>ewl_>p;LT_M3CKHa+H()~-}gxz|JLte_0k3O762E-a?@>+aqtAJFC!U* zR8@_t-h|8NFJQP?j7+LNFm41ybd~Z_Tg;yz-fnFp`(Wb>hK3-ENQMpOrW^xM4;Qg@ z_in7NT*jHxC$aCwcVlgg6hpC-5K-5YQkEv`ToSN)(lUaV@%P2Nt18G|*aJP66I7-7 zTa@q61rldJbAiIN;j$$^`E_aOWYbIClZg>5?hSL=Gzc^01wBR+@X`}YfRM80dSzf8 zh|*Qh+%s7-ka8nvIn-j#Vh4s+PVSt>5NxMP53jDdR!+!*94wo08(^_|BiZZ*soU#& zA?*~E$(VCsTaJ1fAOxgJPl{|2VAc~Eu|5fnT4POGJoTMZN>_;)GFodihGW!)QH5_n z`Wy$TMIBoqk?=xZ)VO1-Rar^ZL@%gVli$|Y&26fM4JZ*8+?j88A{@Ta!+|k$Y+n;?HL@p@?ecuCG zNo27+YVgI!AHyvNZ^Tl6F-&bEDmab zfxBaP1oY+E3cr9V`<}9;(BltP^|6&b;Vd)1KNtQv?SAEG?h3Fu3{{0 z6G7Tj7yQsl9KE2f`_3`yo@@l#Am-zq7~JT*)%oH*BwKk+gwQP9?KA&%dr=C?l58Wh74beC4*&8vocss^8h5iJ_cmlL-ix-a`!Rx1v-@V4zj)+?!QGxqKn5@ls9}=PJWz z6n%Xm*ro#c`IHgO07RY35|@W%ZO{#|W&bm6spHl}ww7cis{9yf5*X`p!Ul#a6=fgv z9j670+e3~|SSv_q;j+AgS{5RO_8cdvKGq9n)_l+tp2E-N=$bNPDy-%R83LtGA)eorU+WpfA}KqTEr*I3mj zb)zooD4Sv5&2mFxkC}c2z~zKK01{Ba639t(7~S}(dZX`iwwx*GvVk5+ahRdIb%kax z%prnV9wZuezt08B<}v?lJ)Otr71AwkUh#JDob|trEX5@q=8dTh+KlWfpE244xxXdO zSXySJYG&%3S9Bqky5(J|)7Hmyc%iqrcxaXf%Yoi72MdBI5j(@9jiRFQC8+I+S4rt0 znUJ$ZUy8Q}N%j;WRxee-JZ&gCUQ8!f62S^AXeXB_xobJCpMg%?&Qyf!(%n(_pr+Kr z6VPRCN~k&|84Lov(VD`oUy)1yG>f~CbIO|)SJ47E=}=owkAuX!!Y zTn#(x=!`71Ep^%>^8E3z@=N1aia@J}Q5v3Kc+u7#AOV!7!px43^{WJ-%YWtpg@EMx zY&;gHX}gVm*WHKdc!UEte-MjX2RQxuSu~@u=ztb?uLh1i^~Z2MV6a&Lx6lG;wLJrA zTN^ZiMUeZFWSlTrj~HEi0lmI?ILUsQA>oxm1B^#P9ZQsI2{Gi*Uw|RoE2Ip`9s(3b zX=AI0@#F<;j6to{98g0*tI0YLddfF3%bBSDuO8%36yyaQVixe5S-hq|65;>> z;t`_r%Yo4}5~tn3Oo_ctSidNVAGubB&^}F8b>9KT>F2)y2C(z`cc2|z+^|mC#GRcZ z9o>!@p&g;_i)m(zVwCXOOMi&NU;W!KU41YRt385x=aB-1ZP{)yYVoyzq?`8Kg-u)T z!098uhyD@(M690vHZGld0n_Oii`(U01Yo)@va23S``Q4kE=&0fi#2}smwywlzx*T~ z{`5b<@}&v34G8^3kyXADu(cWINow<}*05Hg$%iK9ZR4Rh6)Rs%f!p8zGkD)m{v&v1 z3>G)x(sHI5LwF^sUg4aCOfswJB)%2DGMN)YM4MJFnWe26b9l*$T+7eSKFAIs%wln_ ztizRDkD~!^oD~_9Qo1M_NM?&tsZa*C_)8+G#nfENGPhSdDUU&z+IbC72?d^X;j}=% zY4I}a8;@iv5ko-bou;5yGii$HgfK?NWZdGx`)u2!!N56-AZ@vMGg8_DI*@|y|_b|Tl_|x!?6m)X<)JHym-P?xv-0P>%G%af1 zlYWy}MG;F3I!BBHd#=H-?&nx%s-QtP`Y|#V2YuXi@EUC2v>96#4+}`)JSH0Gc6ntD zqh^GvN6Gra=@$cJ7{Yf<9#1`Y442kc(d!Y`*P6BTega|ZqtasY9_$qP?Dh3Lz#b;+@lQ= zH?VDm26ZjGJ`ENFgu#^ra1DBDS`UF{647Y3skRkB&sPkJtZDBVm;$kqdE+BMYTX#^ z0AdP|W+m=d;y%EQxPUXsK>-Gtm#1q29`**JOmdV((N}=q0>p+fT9<2ePe5Z1GVg+( z9QFIc_tP=-xtLBCCG#4CTCo$bwDy~pB$#chU}~$+*OkO<^p#}!0y9P% zW?{&1EdP1~q5E_yTG!Ve{}XWVSm@Un^aVf^89Bh7-FvZh33%o8WjuT2Nu0lM8h76Q zAg8xIG#F88%|&}IS0!)4?X;;9bwobEG1WovIgM-V51}g zI-3Yn^ppH6+QYiuu)e?%>A=b2j93g^+ZC6I0LW!mQEP)O8MdYo3JVbf@{~@baz7F& zSeVyJ(s#%S4Mg$k#s-tJDO(&WP;E^Cq(7d7514`0JG4O*yWoY_o4{}(%84jtdQt8f zX4#C2`)Qa>5u&tYBH!Zl0(TlXLq6Lq&Sp@F_?^C zl;zsPn0fduK6A#i^I94-b#;BaqGrmGHmd&hRxg&!SzM^VDpGypYaY%PX2x%A2~b=HsB&xXs~)di^hK4X z78NDBIx}!78li`*_IvxbUPnvu}_J3l>m*? zkyH>PFj{6bV+DvRq2XYYOqiw-{h1@s+w5`heg89Vdgxcs>(>G%axqT|S;0V#b!p>F zKs#B*(Qp18EN;00JFov?EN#0P92mO~eG13F{r_O)^ic$oq>lXo;I;wns*b06xKRWK zoAzS3eLpUreNwU%kAMTW|4m$b|Ig!#uY4P87mi@KS?UkdH8~(BwMc9LZh^InK(($0 zDFm#q%9yRMt;0LuXMgE`!Ceo01fTwmU%|=OzK+2Hp{Gf3VOD{fSS7NAi6R2}OFOV~ z{-Uf)3Q(I%ymP>0O^T@dz#A_=f%UaDY}v5~qX~+^&tx;q#S)<~3%>20MhMazTRRRcrj|%!okVr%9t!D`sGo>Pum{!)%$sG|JJ56NCN^F}>C*Xsb;aLXd zN)vU&NMJ;SMjJ~43NRSGq0|>S;)X-paQAI{@s+Q>j#DRBaJpa7JG3*T;KJ?&q*uHIuKYjQJPM=wWca$vMOo&7v zS>ze08C$4@l+q}~6l05slQo}#VdD171ygde5TYQ~eP%Wrm3@iS#ZJp)R<}A>j7GxQ zi);9UFFuJEj-5t`Q+)o>C$Tmfq1QK1uV@P+Sz0nAe&hONg5UqMZ{YOVix>_Z7%e{k z)yMJ7k(W_Y)J2bsre?84H`F{}(R^xG@;WLU-|!2t5yA^A?GnH<_p@I59?R%9lM^U>9mm zi#3A8IGmXKaUXC4XkrpZ)d#Qwv??p=UOv^cOy(dxF+>Engi1FEbwy&XuW?4InkC+0 zB{39zEv}J>0FOnPuNSfvvI+YBg79L?K�#Yo_i8nG!18&N2}Uy$x9a?@+eN>nuyxfn2Z=> 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 z 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ຶ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±#E@ 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%0D(CeLe9 zo77Qq|MVoxsn`J>iLOpwfWN*4VjzrmL6T~0OHsbgi6=m$NfJ1{qf#&eqy!0bL3NTX z)skyeBSNDjjBduD5eb73^4xp|aX-~^i8WLXBPD{U*W}{#9D^!?uYl?H3?Ke?KZ|xY z$7m#tU*7<}{MUbq5E!+_eZTgW*WiEtrBC9?hwjE_{``0F=D+iP+;H2Q@cgsK@yQSW z3f}v@{}j9T9>QY}d<63;aQW51Y?}m->vT*Nq_e~P**4JaD0u1xQ0>(tvK|THXnP7d zsG4OKq@PH?%(n$FZ6@Mb6ZF~2KqCbVg93<2-HQU)I4|mj09B0HR1(CH#*jBw9V>80 zqQ0eh)y{xsLjk8sL(g{rDLDWn;(+eO3a<98=wv~?5iAT^gv=wV zx0(Zy^1Ge(FKM~e zNM6{tnK)oVAw;pET&H#Rjhj)|0@8L`ABnLK zaNrbA(T^mSXs7+^YZd)M=1a++ys}PbbGdJ0FA3w;Gv+;`nET{{DVgaD-D6FAS|Ri8_^9^0{nBoqJy z#ORW77n5*~995^}q&PcGWD=u-VYThmvZ-}Z%qfqUGAc<^<^HsR6Cw0=n$C$~FmGiQ zbv>y|3sBdbCN=9S$&3PkohjM^VpLvSgD;-%;HgTKRp$WHFiVVpq+mr2TA0JdYPnJc|$mZn*KAP&X5tIrAbOeBd6u`ZaIC6<1t^ zt*r~#yZ16wbp!MPCr>^PfN{m4D{$fbIq7c#m}3^|YDW@raxZh5e57(fKac2U5zcu@ zK2tO1+vhN!ZlkI_JTv;PM?0Nj*MSL`Bc4C@xU2~W01o4^Lzrub0MboafqhQ|@)CBy zf(=JKbcii7lb7UlO2rhyTv)9ZE7MI>_AoSSI>UQXkdCvW3&1;vUV+kTB$hmd29}ItD%87Hl!4K1Q8+>?fUxN4vUUh^nmzWmDL`E+XKnNP!QR}OK9IK*Hf(m9pr9kg6 zrK+^@WR8=@t6~h@YvsK%pIAGX?7{RGr?QhKTo_d4d!(>o*(FAlSqNqoXgQq01ONj@ z1_tJ%)hh|^qCGskKCp^KYg#lz2Yd1Gl2xm?mH}BVkE`uLTI>(~zPQ{>KLEWI{8x>VT}wO)AjH@+*sKHai> z->N6g&39p1FT6VM$9b;P;!PQcAxf@|tKh`47gG|C!8;cV6+^;%uX-Mqt}nL9dxw%$ zMT0e($KN|jZe5qGB#;j8mT$^s+V`9ksr)tHRZb2uV1_bo)rSXzTQ1OHotRawv8@y9 zUoJ#Qxqi4nGYI8~rMGZI--*tf+EwVbWo<$T$<5p9LzGLizoYKI_OP)~Z7eL4aSVAO z%GA<>my`vZlsQjHG}t>CJOp>hbOs5u9-@F{q%@flxU}K0@H=*pHQ0+(zcsgd`HhhQ z%Dl&wW0*xjcBxNg=E93c>(dMjLEsgOw)ip}$KntlOIsF(!h2W}74mW*6XL2zOh@Q( z_T-BQu}3%Gg7eu5Byw=IC`~c=3`Uw#w^XD8ijLrzv3>p|c3pN2b~exB_kR0V@DKlw zU&g)z`vvq_(TpsYR*H*(j)kIMtj6{|^=CQ9bo*G^8Hy!cic9B!b7xNDgTM8Uao?vu zfvORYqGfNkKwcWtHHntj}CZOA5gqBgSlN^pm0bXJV zm`wx5YocpLb=^f2eb}`0^a$) zAIJ5#zX8|Y@Gd;@_+1$767bU3<`OM{Yr7+!Q!xNhnS<3t&g?>`?OX%u39xw{IDZ~! zYG72(6%SAVYRwEh+mw16)035<$jJO?A|xhW-^%q$i5Xs#oH($$tdeF`Hj)UE>#MOA zN8=hdzxK_T_D|sDmtRbodZU7su|sx!_dqovAVOp4W58Gi>wHmQ%h&o{*_;&QokDid zupYMWGK4Kx2^iN0nLCnkS2FB+b!(2TmJ5Y4&|3BE$O{93AGZ#Qq}(G>wSf4!tGDBEBLBt-5YHmrvcQ+T+%nZo$*fJcm9^;h^zv2*_^7tb2G&2~Xfi6_Tjg zij+MDaa|6YZPz(5SYwRIJxd8PaAb-ZCm_oMIqMR2eX~B{}OuGfK1S zC}qpvnGuQ{mqx+3NXjC#r;SmW-X;Hf0H_$~g~1eK6rj^U(mA1>3rOoKMl%Mu%G!f4 zHT5OL5;!%o#N`MA;vk4Hs8n#yJ4V+LTwv4&RI#uTp>^$gVJ$j6@vBD_`nk@T2+XD} zZn)ugy#4L(!_M|LE?#&U-dik;h^}q1zH0;TdiVSA%rlSU$)~=G$=XB~d59QIYHaMj z3|+_IiZSPaUAqq8)*ElY*PnU>JF^QQB7Et~U&LhGU}N3MBJXAXS6(Rinok7Ph|vXc zp_O)8LI_4rNK9M_tg36vPHU7ESJiS7hZ(`|k@$E95{6dCmI7jAjK>oIjP337xZ;W< zICJ-iVLso%o8R&t9653wLf_-ip`$o-#UZ?K>KMNGg}VVn_)1R-VVjF) zgx=V|JKp(T-2FG7#m?+yICAJaV6rj6)z@5y=Z`&!9z6~mID{u2dOFPmmq6Csd*#$(w%+bI$ok9DklZ5gj* zEJFu06(q^>k0lBH|o3|%Fh$z}PI+>V_YHyYQgFmRps*$8C0V+e31X_g1bj0H99hHO8S4=)&O zxgOb~Fq0@hGc2aK&kRP}KC-1aQ0h4~Ae$XYd9sbAc;C>t;bbIR&59D1s<>2R&Seh_ zobu5n(5`qc_4!i4#ovq9a}5VzrWI+L5mUa8vay`nSoSA!>m3nDQkChiE7(&>9#Hx9 zlAi$+YH;)LN#|%OF~pKFS>9V1$gAt1UIdp4;*xIDxt3z+E!U7x&K*lsa>+aM&tx)) z5(c%RnoHxc@*e%z!A3IBdBsgSCoROtB|pWp!KelLdkNZD>}Vo~`8=Wz0m*e!4Lkq} zTG?7-^g+sG{a&VVP8d3?w~Oheg^XclVezRsXyO8FM;BgzWBwYwMsN`05t?4{OzSw^ZK=Vxf)o)XZs zf0+SNucMS~W+*BbUK_MOhHT)4a~c40U`9ZW>9-2V8ni8khFE5HQo7aL$9)=r6?Fwr z9>hp-3wuCPYzy{aQKw;%GA88QFtO3%r&udFUg^?J>AwPcxKy92@;H6+6sFrd2z`qf zdI4HfxlWmdlC;ECn%Jc877oh>4VO65R3Uad*gkt4Yx|Dkh3AjsLm&7z_>uqRXC(%5 zC|h52g^e!_ep-@r1OJac(AN*xJ{qS9Q&0F7zBiFqe z=g&NeCm#Gu(e-;^x}~~hrJ<2pfP7}h5)rDA+FwBcysCkChZOFNOqv}v0>i~;=*Uc4^XQ@|PX$Dmi;LJSQnz5{T15qriYV09#$c_J7({2Y zWD@mAY^!<#Jb3S4;PD4O2aY}7^zQG(Blmv_58nG1hzxA(8iPX*4`Bb^Z$b=r!IQ+2 z=O76u2^!C@BFr}>B-Ln_vK$$bxRS+Iqbjq;nh~HTdUDehcVQAHoRjwZ095M$YR1LW z((TSGK>`@KP#eL(*8=iZW2E?az7oPo?1AkT10brK0asmrJ7znZc=_}xy#F8lI-Y#& zZY3Ky9iR#o5TfJ@D4Zz*FX)y z074WOQ?E9oA4wd%&_M}SvgM4%Bp@xxJnUMPuZ_+{W&cS6NlJhysm)*sUb!W4zaJ@+ zwbFU5ZqLjzcZrb0IY`tYz`%B_F8}E@iKQbX7vjWoum!dt4@~87HIla?E29;p zV6}pcPBAQJb{{E-to=XVVKfst#0{NR&M~T50K{%8a>p<%L?AMu_K+BiinVYM{Ub!V zr_v19h?19}?-`@9WY-25^+;l!)+0@9QYpI=z{Up9PX*L&CNoUdQs}yBO=n>JYV%V!&-Kg@YTtwT7yQZ=B z%q=rfWWk6~07;eWsGGg`piA-wbI}rC`pj5kRslf_4pD;bvu?-~6Pqc5vp>Gi8SCuS z&}l3@W~frdg;5Ml=N)dn@;bcnb+5;%Q!ik1Ga|Ao85L`m9DTspHu^VFOf+EDdW0yT zrJbG|O6^EMxURRAq6VdtIDCL6Yl#BL66tA66kt*W9H;Um#)AMGAw+RYQH3`mszPOP zu8OUdMY$9_GskTNX|f0~ESsGz;t&nYf#`N5lbt%QmYAe8!_u@K020}p=YLg+i(bn~n5_IG|8jvT%Q2QE8|m(Dzg>HHj; zrb5>T96Wdgqh=ST+btTw+_2s6;PO{pgQ{M`efNI`p%0kNTFhrV*t@q9SNdMqwSLX% z7{+`uqZIc9YAfB^lb#ku7B_jFPq7E65rZTInJ+=Zwjb)K(`~Cg{$OLJFz)~ zFm)>rjQ0T1F;FqMg@CLKaL;IGGk_|bdj1K#{L(ROZJoiZ?)VPu+H*M~cS)OG=J=@$ zutk#u9;Z`A9^RY{7|t8~;D*6hZEJxh7N!nXee|`(WqpFpWx057a)~u&HEG`#z*1RG zvPSe00j>yIlGspW5G4%^IKgbP3YX@R9vG1py4v(F+bGwZF{`xAOI?}jVFu}n&RY1I zm{pguNsDJnP-aDOgC7S8X#hhPQwFx9E)<&*I8WUrzS?>zF zfWYih3`SB)g2btGMVsOPxJvzRz>B`uVqO|xrJOjOTe`NQDKl30eB6*~UBo@IxhGTE z4fmVHk6v)e$Gzx<#U=0xc=O=WC6=$T@a&#E4W;7N4<_bTlz_oREAFXG4z7APL;7O? zu4_4zuctQf?n^?3tm z2OC|MU-FF>WMeTF$a2%b=|LNK;k!M#EMf+wTX~t`&+t`^7j}*#hKSJa$hwZjJm;V; zd^$GH!8;G~jK1$ybVB~b5Kvb&Lc4>Vb0@IA|0o{0{~p}+$&cc_-}}9oPKSMM%_QV* z9!!(6&uQ2$yU|xji3qT8Ez9EKeVogsh0fy$-1hqS;nvr^2RFa=Td=WrgonTM*EsgX z-=LlCfFtA8Z+#zLeEv~fID1^O^+M#A(=jr@lSlv!J9w7qpz}?|DYMjD2p5zXdqLS= zK|#Puy``*NN5FIk*t%eeMS*S$$^F>Z!hY(eNFj+hvc%U5fEy925y3^ET2l}bz}jAM zwVYm1aIywm5Tb=s7h3NsFt^ycu!+Oh+=3tYhrfdV^6UQqk3aZ%j4FqS8GiTQ{s<<` z6?pQ|yRm){a1&`yJz>nZfSrrLXhZglcCLWWIzWxM2Kx%)Ayx?!QKu+I8h)qO%+AdO zpeQymk+?QfC;i~YjIAvhgSwWoBLi%Gplxo(G94_wI`>L?@DhKnY5>%lQF+b^`beF= z=YRa)(9IdY{fj?}KmAYtD>gTe32UqsFeW*t*nC#N)54)8DNBMi8G|t8C{m!xlFt}B z&fK3Z&LB85MyV1|Dt2;xv`z>?l8aQ8x~f(T8fj8ihK7eDbJh}DsU~>NtjQGN#M%&% z5|5a^4H9sRAZ-om%@?D5rwCyK5)#S7fU(}99O(KGR-`a_+nL;Bq04%6zc#f*#ak)* z+3KqGW#mv^2EHST$(5M9`tEX##<{*nnZCw=oo+|Wcf@0iRfsjUCL!$w^o#0tO^~@W zpG)Pu1DeJmC{d0hfqRx*5P-(Z7qHb=jM0WeI~AL6eXrE5ZH4gCcci%?)E&Ad)ZPjC zXstnA6T0~v%!JNJF<#V|zGt{D6nP{F%pLl+6M(yd0-ixY%xblG zv5F$+a#@3xccI|YDM)EZbl*!kTAZYekA!EQJ&9w-{~SBpo1$o{!rFHl504eVgfM=H z%~dMG3>RbCfy9ke=;txjk-RK8i=C1qfpIc4WTR1cYzvZnS&2;m03ZNKL_t)k*-Q%G9xrdIBFJfhg*U5`&3yc2$CaCsDvY8y^PXbbZ`{Tj&?T3w7mdUssa-*Yju-}IR+s}=+|{6jNGWF7679% z;p${eEnJ)Rh1a>%Z3{5%8l-Geb#0m09JY7PBlgM=BXRp_>Kf?2$WBPsV5_@@`=k#-xatg4oRj)a58H$lMwzVn&6$gKL7DFzSX8qQmw@ zIkxlUlQHgyu)XyXwh(dURk!2d!K0`hfxRzIyjL4gne|e9l3>loPXen<3tE0E-MuPi z*D9M#=GN-6mMjWYs0X-DVHjrMu&8IckJIhhFwuLtFzvKHyzsabk^7=-ZP~AYfmP+L zL(3)Agssecy}U+=tuKtF5=@jKSm-Z?o-$_xa_Pg?2N5B}bW2%HUK*_8P_FoW0NUz5 z%yQVb*m+m?sPYeyFe}#nmgIB({}txg?g3G8he*(mo}XJJw^lzQyPg zr?w>q6Azou3^+~F1)D3H_`?+GRdcs=kIZG|`;1Y?KnBP-KfnC>2F%9dRk@E=+(bpD z>;zs|wpE}cTn)-zxqx}8zktQ{&&6cXHSa5=bqX$Il#PoUG|8$d?J3sii4RL#mwsIM5Yaa- z3rJvBlC6v4n96@>@#m$R4eXLm*FP5KkLdKAf&HS+`R4q4jCvq%@8<%H)3~>xi0hZG zm-0pB!o2HK3_emoJ%wH(B;dR4d$A7~Ad4-j0G%-egMs*Z&E%Qw0XF7+FTQW&a`MJ} z0Fpsdk&DJ0&47K`aaLa;X2_@SdYpRx1t125(87{IQTd>DO;i_`3IyjXROArCC76X) ze&|uxBZPJv(~B=*y!#4#?xP>Xjjy@`SKoM@gv8;j79_BGSoO4r$6}tP=UDW!J^<%W zJ%>H}kHFVnY=dHNA`&)IT57|oGTfo{k8gU@w`spgw$^KJc^j_1^&NQdp1;K20|)TE zKk+O0#2@|(Jaq4IG$UZoWtZdZsnh5>#<+$iGf|=n1Ee1OqzpK-U6_%wyfUe$SyV#a}JdsA%qsEUwl&RKa$wsr(b#$)6GW_TVU%9upp z{p*3KK%vEh>pKOHbk0<|fZ8c1(c~0p>N{ZXW$Pf{;rO#(Mm++)_Jz-3ZI`%C$FA7u z6ySx^V?pi9L9&jh?x!AUzm=pV5&e{T=4Q+-y?Z-ninIH5Fr?LmkNRq zB=@C{O4oWacXU7#U}gIXGA#uZ)A33fffZnk1x8RR^2XrQZc!J1y{zS!j7VD>%Y2JU z5)p3{#@=1~aND7n5VY2i0d)~mAEc9 z%O+=sRf}X-I>* zCiE?1t}zoWu~O_;%l*|rVIK-5+D1E~58@_bE+8`bjX@`?A@-&?3-}S$U06_B6?}wr zsx8Vt0FD~uRDMrcTUtTpM-zZ4Dh(WoW#fEI$a32;Di?KwA$S+zEBQB=OE6@?5d)i> z7eFM)FIY3HMvI#j6H@J~44yeBU>ZjObwtq0%nUswBQmjKaGV0EvI2z$_T^ZLgsP4@ zxi$8mQks)8Y>H6=3FSB6$m}$iXj>SL1i2Qpn*oaHWP+ifh)x=5vU0T2W|7Cq7)U8t ztBwcxm`*T+Q798PMoSM4#6D(brKajOesSLet`o$Yq6ps5a|%Wp7@(k8lvN-KkSxlf zcPuABEl`g+ozz%xS4Rp+1;)X{hw+s!e+ExI@dzRl*48)Bwlmq7V#Ii|7hnDAS8&OXDy*;V#_o-Mc=x-$4>5W?d+Y(MZLDE50$ODV_5r9zHJ&{7Fm^6@kSE-J+iP*v z;hWKi4upX9^*uOp=m;Kq>|T8R>9g=4%yuLLFzcc;Mr}}Fc+@}@Q0QrjGHE=cs-zmesTq-^W_KjlNRB=I z*%J4E1ECC7h_WG)BWzvVMB8m5c7)hZG2hz4*16NDn!RY&4&mV82Hr8wJbzNa?ZWWv z+YT4c9s>}he$82I?AeF4z3T{ljX3k7Y^mh$I|WoVnF?4>4b4SfH$8;RNVG&b&P)HO z*h1Krz|GADfHjtYw|xe-gDNgefG3Unq4LavT}vm*;YQa@)*O$_YVTAPu|@mStYn zl(}bRgIRH#V5y8EAR?7U^&;;j0l1Nl9_o__MUy0=l(VT5P-xiOhqi$a*%xCl}h#M z+P(NGmcT3Cw>Ub^Ie2xm9P$S9eX{Q_()kwGmOhn$5lSHY(p)rgm5M1W0F*tv%wm=O z5XE2utS--&ijP=+LdZb2jY*1f9nC^VC}e7 zWicvOrN5nb=%zFDZI2M5PJX~&Q);Eq3_xh`f2HnEX+e+RhaL* zjC#Bap{wwTKl~5)(VzZ#cwY&yF{ou`wHb@9>st7#hOa1R1SJKGdSL6~8FXEXKl<$- z!@(mr;qU#}zedxHBtfE_Euimm*OMCUDW48d~?o^jpPx8Qqz_&4$K-~TzBe&KP%$T)oUtytT22v2e?PMWA{XIe*f#g4tia+|e`nS!-- zqjN@CZI->(62qAHP^~Vg%)&B^y|U?8$H$U#=pMbZImHKl^(Qf(synY2JG9%;d8am7 zJt9zTZ6isjIa9l~MiBWyqbbB^Eonoipl@%Nm`& zXU#kcd2{sWOEQ=5*f8c7S@+AoKz4JYupz z2t381E3d_&BiCTMy@{>O(_+Kcln+1j9kGXa2$BGCu>)qE5X{D!oYKMxRJF|OuGPK@ z5UisUkTEpEsEr-Lk2!?~@D*b;mNneY8Ssq0pC?!9W+KUxdR;Rd0aEs=)35^}NJ8Nd zGrq0cwaBoQd(0((!jjp*476R3*!KvWW9_Ak#2hghQv$!>1f20sjNZtsXeR-h%3>3mVL>DSQ_l(pf;7pA03Txfs=xxLIT3YSoo;Z< zkO>n_xZCL{OJLc`0A2z2jQ&d+U$Cj=+FU2B9|5issmD=WECXS_BTO+4q^Ac`KS$zL zo91EwqHV;?N)MK?G36li0h3Aq-)>GIr>s&qJb(Nt%;!_=-n}2|dyZmfdKPWFjjkh% z#%lmMym^^W94?gxqfCLciyNJ3G`uX-| zhju>0c+z0k`fd;hoH+RmPM>=oS6z86YS$o!9^@H^4_*)NYCQGqlVT96J8IJ~MhbpK zXb@BgvyfFlfD6Tz<)kP?0`&rp_c|w|859PXdL6%7V$8Wpvnxl+Y*dzc=6!{ooh^L% zD|aEZJn9p}Got+1Q2Z2zJspkDc3bCK5HM=q0zQb6!n%orWh0Y~OYZX((9j5v zJ#rr^SEH&LkjaLanUB6^)Fhi<8yMtbG92oW2=lsO$Gf}_{ogs*+|gJ5+X z+Mb=oXuO8M^PV5UUw-W0VLpEuh@vnA-(o)Rurmwjx+z#)Ce6jP1OwUT09#)TnK4`* z^QV18GRn)rgIr@2OsqmxH}}hAY!k~VqJ@=63PlbNVab_N;NO9vJT%_SV6=iP;ey1e zE=>lzy)ernWDsn|B%^{bjS=bJ-B&h9oyLmw~JjY{>!oN*?D9Q{l zPW8(mLivm@?_;cNi7y#jYWNpJnd?S>3uYIf%%WkH(>49Y*2pVn(6zQ+ipIA1UR$bM z+_pRHLL4)pTkiHf2)D36OYq7*2F4A4f2CDwWpFVORs%q(sHOKVmp@o-3rolzi4>I?voLqSsZZ>1 zfE>XuA^C+|U7G)8A9*azY~2rBYRkgBD(-f!1ni4-m)1*`F*!u3&CkUuTe;=2$8ujT z_NY{om+s4d=I6M)u%e*t3!4a`M>IB}2$N#puv=LpLpVL07J`WL6{@;H*SEM-aQheo z(90p-&V`fMeeec6`OsJK`TzPceCzlAJ+xB{0B{H3+?i9T>IQrE?*|d^!2O@Zy?6aS z{`(*K4IH@qFj`Hl@z_`ILfwq;z?c6cohoR9(L_NgflN5 z!zcgnf5(ll`Zm1vy+4f50XtipsG14LRe8EomX9_+OBZNB^tYqezaHcD3i~d*24`M; z5?dE`Fj;RfpS757pU0cN<@@pM<9Fc`fA|;w?ya6g&vRbm53+)W0O}23+$iWalX52O0_5#%3ZT$VfvqTDVK~w$`n_TBvt~$!=gejrgIT z`c-t@6#xET{d27CCOALC$Nun_@Xl}jF&w)3I(+y)|D5zs$aBdUtfumNb*Jun26v2l zjl|Ae7Zb8J1el9cF&};y5f~i?Z z!2)c^$)C(9=d$7GG#QAwACME;WeDnypmjjViz+JPBXj~DcA>>>H(igbue=JM`Rl(y z*Ucs9Co?(_NGry{fH)H|&uari(Dak}EPnTufQ(8+Cz^xd0n*fjwYAkvLiR4>j=Dg zLSruhyZ0Qz+J*!joNPGEXIr@MYoEqjUiZyd8}9b6kr#`b1~=}w2niQs7( zhj#ls+8qI+#kJfFWG4Z(`v~t{3R+uL4m}b2S%=BSe(XB1Pu8U|fl1mZr?Rqk`V{k2 z=2KP430ouqL7d~{-rH*|OCoWVkAX40C`{(bp1jeI*JbX`w`LE!nTi=Z9Zh93SfAll(^$306p$~K1bN3(N=G)$hn{Rz5s-{Uyi2{Bq zM1D5PN{2ELD?Hdg%$WC;7bQFnIf$9vRk+U<#aNlF!Y7tN8;g1IzQSUH0iUTe2e*|(loYcr zx7sS4I{*UM%Rnqcd&v$UtN`qm8jBPwY{j*~eR^++)0rZbDDNx#Zt=sfaBbm3-{@XmzN*zpzZJ}KJ?^=5nMnpOU*i>j zE&s%?*sR4ezHtwTEn=ga>Zp4emGYR7V$zjw`9=pU#f=Nq*SX=`vJ5T`R;+H~$+f`N zWya2hEpQBoeJ8IW6nhx*!GZ`Eq&XX-g6Tb6RimjIbX|+p2M0*rO5hgL3#TyNa|CyO z{6o0uo9@8XH(o0StR>eV;DLKSiU;odB);qYKZ{qr@eO$Ki zy$@si;u*Z{+kP15&peAi`%nJ}KlqRT6zjVxbZsFT%IX)YjP*@qVAt-;aro-j;PNB4 z;JGL7#i6UN!*{&@7x3AS{4%z;UdD8L2VFPEumASZiq zi~x5?z(&x_xQLW&L=%ynk|{R!3V=8o3!|=Wz}7_p2j|nmiwNr7MVXx!2!O^W zr%K-MlyEWIAxt-As0CCoHlW0X2Qiw7)@es|;V6gK^%z*&Cmu;0BvEJ8y|4f|Nr9ja!J!{?lcs=0Dg2&4$E;3@y-Z zGs2wFGNBqPP&O0WBdA?wOfnSXZfj6t5o%5YM$RD+IISH^fN7!3yeC9LO0YvjLePqu zeV!n$w8-!16%cgVZqy{qRnz5bNw6aFNE$7kQ=IlyOYCR>rsxzZlN4++4?+VJ`%Wrh zu>+$rhl8>h4Gc6}sHDBKGVD zuDS76oPYToD$O7sdf>X7UyIki<-NG;uYLn-dl<2GXm+}9J*;loO8<60T!^>LL3CjuJ!XNG*dQm3}}^c zA&6|KD-r@*i*z_p)lzidMkT#1e^vs8&NCYCG4FcxA!=4(Cje8FQ$p_;fVlL|XCkn? zw*eEyHAL1_)SA6CMsWf3D$Gc?Tmr}mi*G)U;^z)V8=yfWB^ILjg4^V+eMEGc|S0@jG%e`eOO3K?=z2bstWu(&5j z0=y;=5!VS;7w4EM&rvoI5k8DAFg$YtMh!qP+Ou;kbXEl*oTt=hRwrl3h=H3d#te5r zuWfUl6%ZtSCZ+|N7^9{K=~(nZS(il}3`9-Yf|TXN8fVQh#3ejPK-{Vl;MPDske&ub zNa6=I&U`JDV`FA{)v>`U7$Lxx^T zS=EHI=TG9)JfdklCQXeC7ti7DuYDR#)nIMS;qza*6LnK#vJnz+)if4!K;~Rsu`no= zJr`$csDdT6J2NUxc2PU1%P|C4S1_?@fY?VgV+sBnSb<-#oZM6Zw<0*0w6f84fgCmf_C~Gx>*#~s{{NbW=t*XZXhB zn%l2|bBt5R&SLxgCg#&lVw;dKL)Uii!hs`KgTUxjwgkxL=j*&{8ykeY2xtx(%aDM1 zi{z^6BH&3CB333av$CK8N^xhj4?1Cj)irBIEJo~DVw-w-r>~{#t^>@NZSlK|)7ZTD60W=P4m|tJJph1PZhI#-x6b0+%g6BSQ(wXrm*0Q`mtBi&7;rWSi%m8G z1IFS3;7WtPG`-1;R!Z^lO4|)~C}Gz9f-%d*1G2FZ23o>`k}n&c1(mlxr(q$oIbe_( zcjbpuc|$kH{C;aEtPSSJqo8B1R2u#QAQ%>zIvB;hg(ft39K(KGorQAPmNL#&0w*Xx zOC<5LqNb!Idn*^;Qd@@QnX6z1m-4?M6fwCVbJ<+n%>6KT2M<}Q(7j9N(UKcGa$TJI zw{Ya-vT1#g+6n=VL=lh6z@AzUmb4=s7McryR#n)3dcP{@DT^%@SboNoB1+7g)L*cJ zQSDeVpv!<{V3_2=fb50gwclQ0KChTFY$x~t03ZNKL_t&`m-sBc4VqZXI@9J0R74Ed zY`Sx~3x?}-Z~@B=TBwl$`T66D_mtd2-QyFOT^2-1Y~=MctSF@#mYr;RN-0N*hLi&` zn4*hvF|(nOr+J!}xq#(Amp-YSg_}}+7TUgQ(p<98zs={85^1>{R=K>2al6=mAS@k3 zEQsH9XOhjU-kkp;P>M^KuPn$BeU1126|)W7vjjIxa`Flvw}Pp6$$QDE5z8yvZKYVO z#FcPkA*+l{YqvQ^m`!I0ov^~9C_D-fmN8WGdLkfaw^!L1nL#nY`wDfv02VGSJISEj zPS2qlZ6Jma;< z_8*eN^IkIr&$f}=Ib(pblK`l@`FKs(jonlLiOFPyx4q~6`1~h6h|o?Y;R+-vLpTRi z8=_}NEyuDBkH*0IKIyk+LrgJaWyT=35wxvN?bQ$bos>wV5NYbgDZfR#N7;xz0;ux4$z{+n z=^kuyUjF6?jCKR-m&x_*Gr-nMiYaJVm%fcScN#c)^)>kae)kvg%o7i)JtAZD z2af-J%v(m~0FEBjD4-dM%{H3g|7Yw?gDtzNGr@1|eTIAQoATwHN)1RtfB+!`2w`F4 z!g!!)6Eu95Z>1z-yqFOvZ)djKYp4Xh!r0!!>naNEaONr`}EGaa9-)BhUta%n9? zJ#pZ`$Oa_kD1H%>l{mCa|3B9yH!6U|{x|4K>z}YiKm8vk!uta7AWh7O$&0xMI&rT zMoMm=-wDfr(+(v7hHMovfT(7BK`;Vfn9P8fg?XS9UqRMG(rpt9AcRE2218>7Xy#@w z2~yJH_E6(Xm0Ml7furD(N#$Z}!k894xn4in=He$S^#&LGEoXCne2Pj7eJpb|t{%2Gv<{=aje%0me8iEti;9 zo0v{3>^*o0OKV5qrU7N4Zp<~KumEKimo8mEJvj%PXE2$t{DQjIn~Wsmn6eUKfXpyB z805JeQB_lq%v$VhZ(s{RmRl^X1~l~)O)VNsP}lar%3M=no|O!%r+braVF{8Eq0-%4 zK#ZoQ#BK~;Hs;X^(11`?GC%6TK&GzZ8KYd03}0N42y!EC$myhlWNv4+72vK4D3*cI zNag}<0cH&Nskno!?%j{kYKG?@drCIQLQgYwUS*n8q@GmRJhzR#M~>o#*Q}u0*}=uL z7qEGD4A%sV@&Wc7-j8ym>_5n})aJV6s>S^^()QW}Eiv>68+A0QFR6xSTK% zLfy)Z#wkO&)WZ=YfIBx0oX-JxltYW@bORyx*t_Q_ZoBP=@z6t`!E?`j4WmJUgNLuh zH7|N4o_qE|96E9ouf6guaIV65X9G*4brgdP5>>k^>E}VJ-M{TV7_F7ogzrDs{wqe! ztbsgh@+rubHvGtg?r8HWY{WgPb?xgV0MY8Ox}+1LQp5t}beJvx^k65X=VM&KWcWox zL&021b&#@>OC8C}ec&|rBG|+c!00fy5|b_7!+KcOakGrzSz<7Cy0FLOLtEawS*h)c zN?BCQ;{MScqZxoTCF_WWZ7MSMMFizqMpSYWk9|z7LE6@?Gxq78F5oiLf=-!>Fw^-@ z+)4U#mf}C^)j-fGl~6IQskS@aPk>3B*G0gIRxHvN7+}qM-OB)yKyANF8)tg2J>B-| zSaqjzoQC~ro2o*+s(f3W(|x=*BGH)OPBZ&U_1gnOcK_@yX}aqzj$I1jqPiOZ#7yRK z-NfHx=z|E*rBUK^L+9O6zUjm|6d)X_jJ5z_Y^AxNU-YFO>HoP8{B}Ax%7FIpqyuy= z6ru8mkpLsu}^v?5+zx~&Up!~LH$gc10)F270 z#XD2CUH#aGNFa<9k!Cl%`k~FfI(Ad{dgvv0nhgQbx=uH>>tt6)yWgpdL@Jx%o$s^_ z9XSv>Wk=cDFE5hR1IK+$gu3@W$!anN+Z&fKolFuKI|mY`af)*}dKsmuBszZxaB#?M zhP)`yxMranazKEE1(C&M^Az?Rei@#A^ccQ)$ER`QEjQth{_l6;(uFg){^hse`j_8| z)%CqNec}{;??3!7hRXwFIU%#a@u$Cnk9^=)QB9`EOTtG#_$#>MW4{j1J5sR>7%UTZ zHYMiv(yB20T|LEz|KykOz!&}z`GD~L|M)X_$N&0o@h^VvH!$4+wl1E-aB08ja~)Sb z+4se;G8U+(Q+(yFzrfeO_&0dh&;Nf|S{-0A0dD>7cjN5IW4Py2zlR@t?;q*Cz-RyN z|Ha0+^BU)m648N-*doz|(iQ~rz-X032h`9w0tU%OJXnFowL?m@myulC+>mfMkpY+I z0`vt>m~1m*I46^}U{l=Nz@jG)Rz%mWr;zw|rHq}`vTUY@LL1Sq)W<9bJ% zP()dfnGs@A@cra7X|IktqZfQc$WgL4C3kEn7^fb5F`QRc0;Vu9CPzR(Xj+!4UAREXy57vO3_G7X&MqQ5q0ITckIC%KQsH;mj{^Y&5?!~V|eGTx)gZJR@RqJ@? zdwvxc&wUfdW;l4oHTc|Teg~VIPoW%G6cwK#lysP&SP+ z%QAUZGT3-H=McOoNMwKjkCmk*FgsL}shl3NFv;pF=s++AWr2Nr*YWt1PvGkhJs{{h z$NF%}GRh&Ks%zxAL0MP{<_YOcql~dAtZdf*J2EXu6+RgQR)2hgaST|%cm+|TLZBc* zdX|tF6lre9m|P*U=sbdx*oc``O0m+5DKZ@ZSPkM3W8US30!Gm#(+KDm)Ll3UAw5N8 z4(-eZS|u_mUiwD_L{&-e>C>t_C3>+JaD~t^5s6sc$pKU9>5ih{5xkYGT4E#!4URz} zx@bdzLYY&68O0#BmqCF+86nH|V(Kmd?2u)I+5^?3!S?1Etn9fK)&pznN3dt#ArOG! z9$|mg4Qqf~h6NBn$OaPZ!3PELbQ7{x0AA-huH`jjI%Q-EFyU&@gHvEKD$v zFEeNe)Ve;h0KiF|vJ#*HW2X{#Xn;W_Avc728nAguHr>Hcg0p%@7_2eKIGlUxJiL>3 z5?UoD#1p$5pbGF@BNzu$GS*g?FxoT1XxWJyurU~|G6u_xoekMctp%uN33jUtL(KvN6*(Ad^E5I1s$tE67W5~katjFfvA#sv7r;D#5y5my|&5f6R+li1lg z0Wua0!s(O8@v*=C&!DV9fW!S?`8f8iUxmqd3r#h_Bj30S%gcwcyu6O#=pZ}??CeaF zJ7(9V4-v%CKVw1>X4cqENEXB_un|&5TPZIZ!I2ScECHbtkG5m8a>sa2%uf9>&eNXh zMzL&ZgHa760R}V2lM!ez8h&CP)07+IdP?#yin>-LwncX|7ly__>i^>_pEMfd&YSj3u?Q$MGxCDV|>L96A&Gca<*=! zV{P}L1x{nze%~>qJ2j=wpZ$Bi|@{?-KG2bb;#y` zt=YLiZF|h__q9btw%YtC&DWLM^(TL+OpcJ8$W^|GeVjV1TMCGqxYMBvDKFCYo&s_X)C6vco%H}#vc_qO;BahaY%|G; ze*!&Y+}IGDgoX(o&O2CZQRD+OO^ui#y8G%70&JEec!$a6X{_$M7XS3ozs8GRawBek z%e(RZ|MXs*JnNma~Z+hD=;_F}dIKK3$|BC0I{33qxmp_7+ z+XYiWudN*#q{k!nwLyzIUJ3gQZIsqv0L1O=j=@P2wek!1u%>k=!fmXAh0*vvN zGWVBOMdGtzy>InfYGYdsj1|>CWp+&re%2fT0lTHz>#aN!CT(;)42Due~+ei zD2G6`iG%@|j1XA8=9GPxl`^-R02p<*4O0O(mevID@Il-?82~B)`m($Q27M!aGFT(9 zCe4VV)F|i$%^+>G@8kP9XkU?lqlM-vkW8~8*Rm4G2Wp!tlTH9=B?+ioXB;Q_DeEgH z9c`Qfv$+V1$PN_{Pu5n`LFwO^7$~mAcxXlh!Btp52DVO%JAJu_apc7ZP=qV+_`_es_T~;&*AC)+ z|L#xm$G`O+Y+Yo${U?46XHUI=d+xXgZ~yTh$4~y;uVQB#IPuKW_?!3t8=O7!6qZ*F ziV}$wQfXKKbV6hS$}Cim$flBh$_t=s1S86I?oGymCoC-+4VflPV6gmdjkp{8T4D+o zr2>aR!FI#QG+9jujOkQD&NU6-D%k*SK?s$Me^W7-gt1!;<-j4vLEH@zT+Ar+L4#V$ z-_A40i*Yu{Eo#S@R+20tPNt|B$Q~XfCA({rdyo+WG$;bfIveV-N7Hx^0$z5*^*DOu z2=4pp*ReC+LXl^v>wwpP=j-s?^T%=Q?4u~kOg%u`*nHCoqb84!ed3ciedZL{IAocH zYXZ8amKgRqZolnzJoWUGIDg>`thG`{1!c%GD6;&b5=9+gk7Pg$l%+Qk_iRjLg@nKw z8!b*4R#ZZOGYYc6L9A{+`k|1N$_Q%2IC2pgVuzHGE^0H;C0CI8Ho!O;QnWZIT{XsO z6qiXZbu(5^Vk5~Wo`3*|j2>eIkP0E55GcT@&#@PwA4Bl4#yK=fmnPPD1xDf& zx<K=({YSJ7m_R7#j|H&ZHWU%4`T1Z zH8fL(Ednx2m{g2v%qVgNSSut(QeMag!jSKXRZ!{7@(iFs%COZqk1G#dkDG416~`X` zDjt6P3$V_jnGz^x5irC60(`@;Ibkp)ghpI>1vm<5Vsjf) z?8vkW4)B!-4{8{eBomi0lA2+%rGS^lK?wywHY9gglLt`IAn*iD-5_jw6!|L3;Q(0? z5PZO3MFJX=WeipbP1RuI)Fom1TGo>$h6^#ktIW-EDaX|}?Z~00z@~{fP5gvYEyd{%V0LB z#*F0yQij_G_U=7`!C(pN`>w*y&I`zL0ftS{psp%dBNU#!Uvy+lCE3uM$L$;2Lz4E+MeQL-T*Y*Fpo^!= zv`l5NhEqZOE?OI%HANz!ybpRPsq2$EbD}F^6jW;)(e_lFUvX}S&>O|nh&12gJm)T8 zrKNVJY8UJZ2`rPI>;kr!eVx85#aZml`#y*@U(@cW&P2{jvW;iEA(hoHsq;o&@ZEyt zJy#O7zGuDYwFwMu1+D;D3o3xlefouI13q@%x4=Ce9Rb}bl>2|CSMVa4t?f*`M*YD` zvxhoE)vE^2_0_!i>38H}Kpp*SByz_~Mj$Y^aZ5on>y=0nmXyoLie*rwHR-xzcJR#v z!B=oi1@9U#7+Ic)O*JQ{<$alEL2gzK0H(mF|HlXM6Yu@svG35exa(7Yg6E#T6M;80 z)Q&+h0N_=xRlUq;N_&ZaFI{1X#8@_Ry{Mejh;cx!vU*d^}KaF}@ z^u=e6eFEQn{yzNUhyDXDo;{B@zvCA%S{|kBy;%-q%gZ=&^~>?Z!(YKYpZQ~4IQIBS50zuA*D1Q&&=u5)^8**=ixc7ziM%_KKBuv+AMKZPC>S z%K|Kk0G4{xJIW|ix9RBW>3zq&QNf?V2pFyiVCE{ZH!J~x#KqB#)eV`2<<&I6;UhPr zalj}4{tw|YV313{K$DZ20fQO?Kh@A?kdQ*55msN30byD6dhZ21tj7RqKyw(u<^t^H zOAyAQf|x`Qn}!9vq+H^)2csFEtuPyHY#TL}_IiX4#(;Q&iYtajdPzGnD)ZB1t3jFEGT;w(Yxu z#t9^#W$$Sa@W^(n#|%cJz&W8DN!H=ImRNhFWDk?Isv4{P6YH81rAAdYGE9M#U?sXM zr@5y0DMu@9?XfS7ff7=5y_=3sWMwRV@)v#sC!T#AcYgf8Vb5Wi)4^*DN=3M;O<|pG zo_YjF4g$aY>wkqW-~CZM^1xlFrrWr9;RU?@yWfS^f7cJ<(Fg9wOJ8*@Uiq50Aq3#d zcYPdx{M+xv_SSi>ZSsqBrq=t=HXay znW7}*IiucDz@G^z!Hnt`nGx?KIS?RFMrfv9yaBVwT$OcK?KoINiWf1Z?r#f@Cn7#yu)w3@) zGea{$GMdWa(7}T^diXHToj-@0U-3%Z_>!04=;0%{>x=i{no4~$YH!A3)+|p&mTW7ot_a~9pIIBL;4}J${-Z~Ufl5F>v8qrSK#h{`aCu+UI56b?W&BNAoYkRS5f(lMMOF+ zMFylCx(W-*Py%BUn~6o38WMJ5WVHmZE}5)M#+VHN(bbA{af#CJ<092~ZK7*vJ7ul5 z*&=m(nHUrUhIGYNjzg>K#G5jrOf~auOVa{es%}du4l) z1&j)QKx1aGfNx9!<}_|YvRM+~B&8e5QepsJI)4%ej;^2>l?Vv%j|fz4Y0%#)1%RiqGlKmWW;bzfKcbuwcapH0aTSdUkn(H3af!J zKw0DDQztOp9>aU5nQ|Gydl)Kl^!lSX`m%M@mGr0004fR7iZH&o1NL6ya>^u#Yy<-V z4x1|Asv8esX>AE02j4KJ+rqfZ3t(xl#ICF>VEdxXgDeJRRz{x9WMX;`jfE-9-z>Qa z%e;vJroG-Hly6P|d6oeUE0D)>Kwz|NKu&qA6(wrCB|EUmBAKC^xQFenP)Dw^U@A)VIgSwi^Ix`XrQ%>-# z^xsqf!(uqX&9}S_*WK`XfC!tHPJ_r`G};pdHhPBTLZdEz$L$T&F@L%S0zsI6s_`(W z7ev@}HuTm|;hA^$ttpOLLi#s-)iEezy=bP07y~YJ>d`fj78qDs79+adQj$AeYc48f zscxp*9!h1T=0)SX1DejgzB>>dLoT|4%?*#rXcPT8xKjwH8nmtLGO=d6z0*1Zru1$H@+>=sbx7nT1i6zW%=sN6BrTc(pAM2F1U;!_f&a-QHm)m+@Q1?Pt zyzg3<%as@F*?qY4h8~E_bMv8F_WV@YttD+Ack3~K_so$-l=0t&_?i8p`|k{lc$ort z*YTa-9u_>`+qgxxcMn5i8Ns(oIAM173X3o+M#u+ykmc&zIdj-Xl*42a`=$sDe6xe5 z+J<+v=uNJk+0-Osvb7cj#@2=Bu>a^yc>c*J@CX0bkK=}$UWFfg*T2Dd`{(ff-+dQc zI18JJon#A5tXLs|=?s~FQ6H;Shs$6kW_u3tNGJgE!WIev7z_y~p1BV;Q}C=IrVJxj>c-3t`iosA>K25*__x=r@dF(SdcjkVqtc$V#@R#odF|c|NlH@Q8 zc<@Uf!84EDfu>;`x$0Wn`06)fY2^Su@mIek`W_=Cv&srWo)hXy-F_#G$ynrm%#u1- zA()ZGQm2?)07!8FBj_gps(@U~z+{6pp&vBfnlim@fHOi!$$b?EZYy6!$sO0-<001BWNklZ zKf#yo`UkA+hj@i)thS64z%vqp%5RFkty2%hNa|BLNmx=%kcbDx2%t=Dpn(Evr2w3M z0s#mKm|T)c9U8SKOSF3xBy>^J$YGN(#AHrS$1IFLiTopsQ0)sJz2J1=u`)p;%P+D+ z4N3hXSf#2S7y|eL09#3<%%tB%d_{}*3GmW=k)ZZ*yJw{HQ^UwWX`G{XS+v8f+GcV_ z#t0}tO zt?`_V(DvVOseo&|$d?$$k!9@if=~1|SfI`^3KVJA@lw z`8HfUdmfiAoW?cRy&5;Y_RTnT;u);1AHWa&#J|T)uloTo1J}LuwfK!cxEm|02Qb+N zuDs?({PZvV4z9lbCOrA*-S{uR{nIjjM5rd5+^=O`X!h}#0LHaoaO@{ZE<z6ORMP;fhjKO}P=SS? z#XLjA?84D~N0i;*8L6b0dHbOZ*eo1uSPbPJ6FQ@T*CN{SfMvzkhBi$dwn>>1^NQH_Co$e@C; zbx!Ud7BaT^h>_<3v|~UD7}nb&fCm|n83SJtrqu?r)e?pZ6lIBxljlK}QItkvgTSJx zYCLo78JIzX16Le`p9G9AIW%D#J6oIBJhLUIr-cY3?**jGG?P|Sc`UCkW6!<=@KXo2 z3ZS_Fn-ivEnOB(s*fB=SoLo8TN>7xW&Id1t1H=L_Lf2MM#%GkWUe|&xTgGkNyu1Q` z@xUVq^8ipO*rZ7$=@EE~OKr;2e0rQEy_xg*MzxKjvWg275XH>CiSuBz~& z7rzcGEBo=KFMJF{2Cfn#S=@db_y%~{E8c*~cmrq8d;>(8G8F@eGTiWz+c4hVz?sue zqbN!gMTzrgpT`&P{tFyBd;@O2=#dknQQ9%^d;K$G zGVW@4wnir_Xwz-(qG!eZyHkG8fb16>G#l=GlUkl10Jx?}0AF%-?9?sQhXLUegx8i)X+#qNsJW_X)TX|Ko1Q&QyC51Vo87 z+=fNubiMykSs42^y(FpcotMSBw2J&pi|3o${bmk%}|?O|0S36Mcvj8HBe1V#VUSRfYf zIb?EVgFUeM5Y2QGO|=aM!!;c=NaeBj9@gaW)fn3wC$X~kMR@j!JMj2pe}~V1`nRyO zydS>VL@^*V6cBX2n@kA+0UEYS4qK}+WS)JPLn1=0J-4`TG!m6COt9jf6*@D03cI_SOCgp0pl`_ zbLgUraF&KmWu@FVQC*uM_u3o+An8%RSacMimL6LiOGFPFLvxB3gh^Z%_7x(IgT<8J>Xl6SY!a;sJ_10fKrswM!=H zoJQ;cGGc!O7xf0IhfxM3vxJ8tf+Qgc!oD38%zr-(<>CwK4gex>E;b z77~fw#*AvB1u7%X*DT3I#GO)DfmUo^@5CM(Ew5r}rGWPr;cG%&i8rHBkr~~wqnBjp z*xICZCzkt7F2L1jb%4oMgK9c~)y%dbXxU=C6Ob21Z0v0><6wcjv>5FHsvQGg8w3}S zm)Wi!E{lOek{RmNxNMC?$%>kkUBFwtQA2Rd_ZR6sSGE@L)@LE&4~r6LMu{o zS%c_ig|Zg44=Bc!6uG91OA%zz9+R+1;u4=3h9{OpDw+@`2C5It@J&ED8loI7quCC~ zz$gpNAe;w`w*jcYES7-DIBu{EQ_B2er*R@k1ey0z$HqglUuQsQ2pgMcBq0kK1j}G2 z#-VQ*6;n#=5tAfX3pgW5Eleivz;&hNLzIcoIW3e;nlagyO*TLP)4@>Uv^Ww0Su8Q8 zn}p4a=RwwEw7MVM$ht5DSYt4qY~t*Rr?9iUjsr)o#-S?@V|DKc#~(j|YCIMNmIyW{ zxW?o7u~W!Qft`yxICpXb)14i-+9S&|kkO5%3Sbwoa$tzy9Gc3*7=!gI4x)AjyyY>S zGDgcX&f6Cl)sBE{V-Jis7|VNswZn|*m{C_UpPinjKu=3-CTlmwLX9jo32`7l4Ip!p zJEx%vNDqbr;!ifEOq)UCDFQHF5(`25+H2fTYbeFs12is4BguhmA#*a+@;iHmQ#GBP zZ0e29TUfcT%nPiptOH==c`kD`G*Sl=3B9(yA9cNhMuXcLWm7j(FD&Q0gtZx-eBxeQ zIQJ}ewlCu1`4@1_i(Y}1<)OwEY5{&-?jlX7Zy>aQ0r&0NnNP|LpUPu^Oj}m`hG&A! zLzP78{0<}R=zul`Qdiy)A+i@E0Gm!%Rd$*lcp)z!w18EB3fQh;-nK?IbKT3@CR4W> zNlR=q40qob_r%UMUIbW4f!%WDFr;fN&s4ATM-M3EXb|)4ZJp<4zni zfbslTcYbzzX70@k=N0er*KTc#{WgDn!HvDoE>pU_x!)1;L;~Aq%IZzKSTgr8pBCZlEc@Op3)v?U< zy8btPS9n@*Pv>r;UOVFx-PPFr80sFx>{_grjd|c|;bN9wO=4N^wz@iV@G`e_TiiWh zSeX5NzGSe<{8YfgMz+^eY{mHeW!3|O+axE1vy7y zf#`Ow7QzX8)d4(*&5+oFF}|^P zz{RsKAj{=_ZY;K?fwK1ofYa>GF>XGSfIV#FFrF(Q)Br_0)pbY%0Wd>3cn@C6B0`Gu zM^IqQi_KP)Vz+fOU>nU&Z3>Cq?i8T4YVWy9?7(~=<)&C}NpQ%F<{$+MBOn~rm>@9`^DN2( z`O3#cp$Z(QgD~H^P&12N$sS}+m>h_H6xFiu@~3_BMYFTeRs zxaNje;;y^@5Y7iUl{fd_^$!?dsPN0b{?{l6HYKkiATd1SmKWEFF~H9DHvaKre+kz# zICS`WTy^bBao=4Z!q&wt3ICJ>d1(@0+)PDBA1)DmqvS6rex-*}dD%}66iSl0s%y#D zf~VvKr4R&^k;MOJhN;?vMw7|-$MQvk)juhLCP}k@Xf=blhAK01ws-!>YKeEtXkvsfcDxU5WoR0%JUB@R}Q5hJy$8 zBQRq!nc|sKr?I)cg?)S1apjRKP!9$TbD~_q{IbTCZ_>T!KLWVCL}{mSW&i=Ua4DaP)2vo2p))C9*yLfSvqE+ z)+t1+%sBu?+RI*r3AIUF;wc0m3W$`M)@Da)QA!L!R{oBlWy;n}n%F@*ATlMRV#Qtp z*xF10FsY}!jG-B)y^K}RSREjilSVB}VlI7MC?p*XmZ$2_c}V2^ibA2z%~PlLV%)7Hy7V&kFmO25F=E=SS(6lya~VskkzClHo$8v zkF<%$7)LlGOl^~b=Ww&fpf>tpo#HU ztbwl@Jo&Y!5WGhS0Y#DH@U;h!mpPt$_yu?uu(EG}{a3GJY3~S6ed$^FCSdJ~y`XXh z<4YdRI7oJ=0boEFEd#j~7me-n>Iz|ix)L{JIgJTuc4XtSR+JbyFwj_`49!9eNtzSa zjc1g(3gw0|Sdz6=je&YhFeaqfBk>@@R{(jLTPBluDNQAF44GInWTb34(G~V&(AYD< ziDG6mP3jcndREAsl%(>ErV7aN6;#ax&ehnv?+P5g@+J7jL!T3Ob0&}t_}cv+1GB^k zg4HA_rTq8AQ}+Q*5*^gl37kLsoUBVjXzB(#;~I^tVN^>A!L_kK^xp(z=o)MfhNyE_ zycYmJ;`F8xce|P7xZ~58v zUhe!i^9eIoV*?iyF?%(+f$yS(C9ah|XpG&J3EQhGRIiy~7tOsNZ7(lkBE^DcYez|7ccGivD*%z+%U$ECBNn<-g5-Xp`LAZnmA@lnEv=x%ZAN#_lit~9 zATDA(?habf%&#&TQc}B_dd}6KfS47%`(3+2`aPL|tJ(UIwz(&V8MW^oNbJ@pQGQ0Z zc*fiS0_dJDi1_c+JE7#e|q5K?Fe9o+isNT;7YKT!Qxw)pU%k zC?xK-7x%t6WNGi~EVjTFdAW)#FOlV&7;m0Ki0NGcfEms;D2f384tWvjLlQXIs7#4uZ2-6*a zJ);=OzUdo@hYpe?6svm;fPl%)7Hhge%bg5io7{?R2oDKW&( zMmv!k^*~RdqV)VEE2RG314RKW?U4hDx{^b6tAqwB1q<~x+Av%hK!ti6ZAo7kBN<_t zfiTr~KL(Wl8|x!dD0|>N<>f)3Cj3G z5b(0mcyBRAB3Yo;GF){3wQYX^k)$J}57HzRtZnf=dNxt=((I2h+WnPe?NC5+eT`%s zn+usiy!PavDlf%Oca7A~WE%5RL8hQ~DJ)>2Af|>c4L%;kBkk9TrKloiX%#qk<`_0N z&*0WKycv%?@CYuPJ%y!JS?5k&ZX>pk)GX4rJI3msHN5ui--m;TUW(6u`U9xyQ!v(G zT1_w>1IrmF@1)=ZY(cPtZG7UxzllRvUJEw`CYwMpD6qYA3V{Qf4Zw{Vum7&^!EJB) zDU6o(188w7>JLv(#fc=I^QTYX&;QG>AS*52@niod{^xi9B0llqH{f@F<9%rA27@I{ zysJd4s82_HBlcOup$sW&RLN^ZvM!+BS6;JHCp(h_ z#UTE*`niK7Fm+QkSX*Aga5TbK9{4&w^V!d17>;s$ z|M&h+IDPswo_Oq0uE_A)q>`iP7&STylVWDtt3EtyYqWsb?>7dr)C23ANxW7#oyz!GGXl_91y?WSgA z3KDrpvKFBgOXgdznV}dlE~ds4&{!TtAp+WamSDb+IcIVRi_G*Q}4GlOA;5-6T#I}Otvmzw6-6Bu#OEBt<^CxlOifd#;&j{O_r{LTe zd2UeGHKyY!Y+j-uU~ON4#_hqS^ILGP0yh$zl>r17u(V#_;MM!EvbF|09O3w5Cox#g zap>BESlPRV!D`Xbr(3wSBN8K&@F)H;k;Er^9u|a2c3Rl#Q#*K6nN> zMn2TJtS5Q7#)t6@W4y^24(0T)sbs#_Q)+`{6C|TzEi!@zvH-;0fYYfSF_0C?sx~c3 zP*p9%wP_gTKv<_7IK>1pBrYVjl=&@jMi|H#h9$v_4-AuAZ0}se@#Dup6tK3w7q58L zcj3fy4`DJn116h-1!t1!2lWJ~rww2jOM7!+WLJ#AU_bU9Jc@6=@DOY+2M3esrm#&- zhVAY1*x5RV^}ScZW`)W@HDsxE*-oGscf!qg7)f$)pnz4^a!<#otptmjz?>mJWAIDh z^+j?2^uCTQL~3*w(UL?8JZl~Po ztI*a-_aLcWrvY7ykowMPH;{d!>DfqMud#ChFV^`m{Ti-6o|s zwJ3kmNSXV!?ES^M0wWuC#vMK3=nGH`?#-n|ZEa_vtmF3n?%2;hDg7VN)S-V7Gab{> z+P(LQncN*|>K2e5S3s=W(B4Am*1BKs`6g2D@uUka)~Dw_0$lOp?n@=_^6r_tQklI~ z-}_=gSv>b=np>B-k9+;ROZ^rt4({ImE!wqcVd;fl5tmzL3&yP-oB3Xwh2Q!hb=OLq ze;EV2j{7bVH9Oz8<iP5EytinfFuK8qOq;XPy)$F zT{f#}z@CwjIL}i9gn+Mp>A&GS|M`2dvi2eY_$E}_7#FM5P6KL83gwwk6Ajl#rF$HdVct1t@CH2mlvJ z9c0uSX$nloGEUY?+1>-AHDIs;Y@JhVie2ppfD-qP;6&%5K`Re}2S{~d zqX|j;4uo-N0Z)*SQPheeI{*f&fF(lM5y2Lq;MG(Zgv???1~2_ixr~p=rQf2rS>szB zKh7`N`{KOl@9!~_bpU#@MlvG{>o+*5EC-bmA?lb(d+q7F0wif4)$JIPK3k2UVb0{; zxdPGLs9g+h0o5c&q2QV5y38G~zsRm5**cr11;SPRMP>}Xc+ZD%-HTt1AN#3a$CJl? z9@YZoP~H<5h{;;w+)9{Kka+L6e8-RAN8j^nxaRunapC+k_{zP1g}R#Jn(JPJpZb}1 z;|u@v0X+5iS7D4K${CCdybrkL*0-Y^jqv5W@58HJdplnKs@rk@Js-!`8Q?A7|1VKZ z&*Syq`6Kw}Kk~06&ZWl4jAFDCe!OF~=zM2jd1W1Y_8!LB^H1W#fBkEC>hb$<>Eecx zwnWa}b9h_JkB6n&ikECvc$jK~;3J5JU#`cIZP%s?z{hMAbJpW-QcA! zz7}tK;~R1A{5kyfhtFf<;zca2=CGMiRBOk{cytxu6)=peS2kZU1ihX#O2*>WJuxZ~ zE-Rrrhl{sA~Y#qy|x59$IL*<5T=Vhh33f+}J1j)0(9YbeD&G!j5h zCn>kaVC;7lG$yJv(QZvYdNJZk9a6C3s8{>co?(CCS5Ja zb%MHL8<_{u%^C{Ufkm(xWhes4UXk`FuopNAJ5JeS;i8a~N(r@H{qhy!Pe6b^8|@;} z4Tg;*sv$!d3^O#9lYpz9(9{fPWv-DW*n*TU9$@nv`9Q|ZX*|HZkU5lPKt76~lqfs- zk{S?>=MJZ^001BWNkl+2n+@UUjY@`EJqyIhK}c8 zS%r$#Y`H;P=N-#fkCuUQC_t5Ogbha|?`vXU;$Se{*}|m@#}Pt}!O~GoHwipttQ|3^ zs*4zJpNB08lmX+N4Fq0CL3^=z@eH;vokx}#0nLL$UV7}=XEB*<;n{DT!TD2LXzCi) z0IT~)-~eo07=sL>D03XR@(@A*s>&fR3tV~Qbr>!UG1>H3+T&5I7+iDn)i{6r637%V z#={O6>qiK~B}n`aD}C&p%pY4ywlgwvN;@4({1Hoxawu~+Wwg@y>osntYt(%>t{o%& z+Grfoyg%W0}#LF%Wk?I`wkz0b8S4qR9^sHm%_Ft z@kQv}Oxj2Sz;iduJV(8upZoN_(|@|Qdy5_LTpAQ8Y%b?LTZ_YZY`Y2Oi5_x`iY-S2k) z@PZp=BIfP#RHx3}wc1%%m;KPLE3;Fv-TSh4JtK%pBtNgX>n#4V_>_C z-Nnu5%UDvDZ+ERRgN#8_)i~Zf-#!ozge<|>9OZDOLu`QVj}mt=gMqb@xX$|qjdO6$ zC4F2Pr#l7%E7u7G=H$rUNJjBW!Fu2KGkc(Zv{SUJiFzS#3Ns zlr9JvBg?f(EUvoFQDQ(@jTJ{M?7slDqgZsh;D9`a79feyHMt}QuESM>^Jh=sZ~o*L z&`g2Jmc;l1#9PdbB`$bAAY{2R1$8M|)de!G1B|fb#08l!SRo)V#v7vBHkH^A5(i#L z8tg^KEplK|F~%FxZllRqu{l7wfUc3n=o%z&hJs)T#gfFltQv-^z*8KLX(RPgg{9irm>-pzNu}C#2;oPcVV!!Q9|31#!sj8K5LOk9$7BmdEbt!QC1pz z?OV0AL!34?@5E?nAHI0!r*QSrSK)to-+xBa?0`WS@3i1%0th=| zH1^jQ?))&GeB^$-^F99#t7{{y?>mS+`_^&p0>BvKp3na|CR-v`j3rQvEvPZ@O-3!N z0Dw2*O)H5<2#%$n2O}A)x`~(w5=kYI5|lA&AZs!(AsYhuGNG9={8)%qc_vno*SMU; zQ0(B~2;NJ_7dc_HD&sw!DEY`uC6+5&kjWrfmF7Q-V-yCjb{r>mUS!e%t->yeHlhM9 z3UTlXbdkjEiiRaKFd0wrz2E&NT-w~iycwV_RbS_z*KNo2V!ej19`>VF@_P ztqXWk(p4!E;mh!i!}`I4CO2AE6?ih?j) zVhl#2NO<36y#g7tP=>9<5(z=(lU3myNGbgRnAVIuXVp$*!zzc+m;j8zGQhf^8EKQt zhcZ|53^19xjc0_sVDJtwZGbvvb|V3>8y1BEhEfJ`pg}b`kGv#ItFtHuC6-qAqZksV zI~Q=~h2t<*KrHqQb^*Q)a9rX1$&+AKMk5nU0BUy$r(d{$3uiB4yiv=kM+O|Y`Y^72 z*bJ62V~v*CIHAlH@{%CTq1w*A#7g= zD3=&@Az6f*nvq*!76QTmjPz|lT{1`*qJ}g%k)B9~ASY>7!EDr=TsVS}veTwWvbnOx zytIZcH zMmfc#Yb0ZYfiP0V6~DnF$LPt*Dp1u1Kyz3|-{m&w14FL+E$EB$IK>DZud<589 zuy6jxSOT#yxl&zvVB%cu^s5NP$uktK2<`J2;5Qw#EGR4)UmeRXoq?r$IT=TDpSZ8h zRj<=Gac|V?p?yKmh>X8mYp}e$j=S&pOH`8`>{;KBFMRHUsK$(&Uh{pJPNv`xKxD+_ zOFtnne9U&)c1!0y-21NY-WVU49mw|C%enHUHmiJ`b)D?hA-#(GWpnqwMOB%-VZIXc z<1l-5ZlZRs%z_i#|8tjjUcTN7;1<5jcir!4*Ozu*yX*ItEjpF|t&l3czl*-<57Ld9 zdzZUtSbUZ`7Z&yDOl3)r0A}XMT#}65Bdv^?U0L1F%&01e)GY}8r4aGG&V#*#9Xjtx z>)q>L{Wl(W7-Kq`|4jYSADj65Ts*;D@vgR2bHLN0!aIVGA^j)WY8TB175F?;57z$s zHfWXoCUXjb(YxM}3$v6x1~kr|!R!e4$FD2&X7-<%^?ljd-;oQctL9qMzk|UVj;SLj zb?dksFf2Mr7ad3&?0mC2GA;Ok7^^l7D>IyPsz0e>-!55X#SmFOnA!NGUq!{1`9_(5 zvc_gGrq)DP+V2V|NIfK7%4lUjysI!BU+S&X=z`qTo0v?`AfkZG1o(iU`V!Za*jZlL zV&In40XD)K%mk?6O8THEfO19Z;xqxraD{-9vArp}@?e=zZOb7mAPl7OMIljSLkUli z7Y1Lt>%$V8T$7-LARZjv1JxFz+U68j94uoRW8`^FL_pNK@!Cubbq<8m0sbPZtq6dTpp>mAZzVU}~)BC<3|Mur!fx${Gwu{zL zlBLKziE;S7gSeTRR!#wGzmWobnUo>P8sy!cG#N`S*J@>#iS91#$@G08C#|#CcA?UD z6jCOj;G!c5>7M}&i8*C53B`qA;K$H>32KL01#+kAa;)H1@`6yA9rq{a6jY2!GSrTC z%FfJ7b(dDWz(?;RC6F{z3AyDOd4`O5qL~cBJOnzSZe+}2crXs~&NRnE)Tgz-Dg`cM zV8u8$I^zu`uW&%}_6yNlEmO}$byJVPdZTsfu8%lpNNw1ZbvOdSh(NEtHz2VK0SIUn z=d6N-MOol=-|=SLfA^_Fp6YPhFCwI* zKfGo!j=}^KU^7nid65k*yOH!%Lf)YpbP!5vY+sCB;GD20jS5ebDcdm!xD@pc5j?_% zlC-Xr5@1A4CP0~yUcoa8$z3yUY^008FvHc+)l$Yj#xU?++LNc#4sPk$8VFjbx&~Rn z5<7tbOaXQQ2nt9TjpeAqFS>h&#$&XyjJ177K`_Y6GpNRLVvv;<2d=mdgTWduo_iXh z-b~Vy;twmsH)On$&w{uNuDHd$2MRHm;_9l!3?XLKo~+o=nhFkcNX1grj=f+ zlSN3<36QX$!(s>!z_6fgW*ghs7DmXDC0m0vm8ztg>y7u`Z#d`d{$uZRzVEy5zLMOX zyH?e^!}p!p)8CI=))r^sO z&9vJ{xf~dwl2uxc8CgkSHv^jtqfw6O<~EvYj;t_XA28j$f|IX3jCQtz3(vm@n1FmF zF3^&_%3$;AHTdQNT+^luWihtL`4=uK2q|?cR|~8jKaSDrafDzHO~FDy47hamB5u29 z1e;lq?Xv&QrVP`vSThY!uIeUdB(YIb>7gk|>=@EH&AM@u#$sR=tKNDk7E#|aGFA&7 z4-=M1mT(oSS@PVb2V%LA-vqq0V2MNm0tM|yxWoHC*t5`dC&z~;H;7Y%RjACjw}ND{rc+iTM0YS{>pJ-9?!?rVXOofR365m00X zh6Ss9;uv4HR~|axE5-)wz{LKk4oM40f#|7g-+rYN=^lo-7+ageX6q;?EBN>)eg$Pd zK{cD<*wNRbZUX%F6wD5LdzUa89T_Ok{lb^RP~2V6>!My4^3t*v2D}@;{e>^Red%?* zf|h}}2i|qyWxR0R^~)O;wye^$@ZFN~hS*QMd|8u zjF()rq^S`gS-fTNT~s>LgJ_{mz2%XHp1Y3G-r{GwBt-QkeUjX42YIxw?OJf@zP|+j} zSd1=fb2!?R|vU{xMX$~GWq0}%i#o?ieY7Bw@tXVB@_!}|3#@uXk#f?zX3t7p1M#9##}t%5#=#JzH~n!%O<_0&t5Kq5V~(-@PQ zQoJ(+Nk9-<2^*A^0vJ}iqANHBSY*D)8O%9xHSU8KF?JzCToajv87cP(@D#CNW-*>W z{?MP|=GVLdP2*A2!gy=u0+N|bn3Bz0!d@kvF!W?Msh19G7Vf#v|u2zCU}s_4hcu|bw2 zxeZ%`WYZliFLj9y)YUPN0De9Q*jNaf+x6!L7rhP!3BUT3BwdY z%X}n%n;!lU8fe`Ao^jKmO8h!aKg~ z8!+9Q;qU#UpTi9|-j3JaewP4XPS)6HRN`-a!*_PEBDq`FujX~z>)wd*WP)d&c@Sq` zdQbshVVSq{o^ggmU#;gdZdpl*T?%cNwG!6eYoeJDfY64nA6X_y6vI(pv4+6W`zZjm z$_cQHGehJg8IAP{R*XvIvUBp@d#SI<#1pZev93=-J|-o8Nn7g5!@1?1v=k`J3Jn0O zG*`*KBCfYmsItDaQPOFQls-vUBWtX}pdem3vUvKrr!lYQ$Sk4pvM8T;`Z0X=smB2r zRI?gQ<#F#lcjL?6@ns1lTVG$tXP0G4V^=3X{`WPN0xAjFrQHYeHv5VzG8* zmO&#C0A3fRZ)BJ8K`kwf2q3R%*7J!-?Pd?)mQfzk@oRvs^Dm)k2shq!8=7hxZR5eg z!We>UTWnppgq_Qm0Cqm=?# zE+-1-yjs3wocFHnVQ+gEYex>FnFZKP+_{+<^-MOcd_~$DY1w%xaT3~Ehk(2!Aj>pi3ZrRQHw9Uz!83v$#IhpbraeK!aztU&=MQxA{m{Y7=N`mpyaCV%HE&ANBHMy8 zVB^?vjK^!Jt0^9N=(n)4wt>6v`D%FA4C_w9L>i(!!`FVsC@WKny86XSdf>-0g`Cvg zE@}+D(O+`4yo@oh8Z%&@!rTXuc-RDMtt!3UJ@R`8pmDDp!t#u;!*=z10V)e=5V(7= zHm>v92Uab8Ej*BhcPw`M!W~_GqUG&6jIPC`9Iuq*VOa^XPWnK3|F?r*;#m^(I6(^y z(+`8HNKMy zuz$s0|Fv(tQXhVP_sN{=+|z}S;3ezhmHX}b zW0V%(a5BdC)O%c<_lx&O@kYJx(7$K@_uRiAzT4LcJYXRKL6Y1=hOV)&*$6R?AgGJa z+aUnd4UW!XG6u#Np9 zOJY8ga~3?XcTGJgNB}tBGIp*oLL=v^sCUKg5j})4-O_V_ldwhEintAXC-J9q4`jK8 zFBr{S$X&af4jUY_8 zJf5*M`)*9y7eptr26+DIkKuQIc@Nc|N3I6@b`Ax(6kJxLxK-9;%6O}K00>}Z`?Axl z*%8Fah9FOz`VwDX{#j+2p@Mjd=y%OjJYlE+d<8T+8YixjamXx5qDC8W1PF7q5GE-F z0y%(6)kPKHG=;<`WWdllp%dm?@5{27#(0`JuJx)4k%sQk!$XEfKVi+%Lpm8xZ{+zDE zm|TY;`et$#AfzNLHhT0q2-7ii;A0dkr2knXV;Mn0Mz22@ogGX`SSHKx?B_m>Ku%-! z(*Ohu+J53ls6rw)sc6!?1iGhaKawFd?G8ThJ3ocG+J$!xfA{*9^Cljo$ z-VW!WVeXIs@YvSA%`X03+!~^NEbAd*d0t@S@M&DW`Xq|N$~w#BOxa-+3h-xuW6?MA zTnhCBiJht5ib2U6c}~bm10qH}4FiU8mIF37XzP&XB~k1PIeWNT^I{lblEyjB5Z7j6 znU~%JWr+^I$&>+1Kyak-JjENWsXIbcQ4*FNrGNu~LSsX6bqhgU`HGSgpb!seMh`=h zAVb+=EZ4AA;7m7e@zxb+``H(sfwdVF;R1t%+HD+ym$)J>w0QpqKY+V#zYBN1_D;Nf z;bn~TF?P*12woC>#5#f@OhK_kuxCvuqT(zHJ1BssGdM(dLbc#oniQI03L^73I%jhm|PHdn=8|j418O|MSixoX(+12x=Tn-)N#2k~zge;(=6^2->`0uw%7G5{ePSRbm_v;8EFq8mqLe zG)72rnP;?`9WQ!inG9mYP)n_B>cR>naH4DnmbS)BR_p*z!URN!)<4am8{%O=f#orkHLIy@Qg<-1s3W?IDJ7FsYK9l=~%$x$GKxurRPaKxm(0xyUGs zecNjn4+@v(XLf|TK8%n2`7eU(D)KzT%IG-uHn-vGfDbxU@s;oX zJ2-jrDB8Bh;)~)*v+Jn@>_$*+;RdAGQ~TP!aP8opKG?$OdYcZukFk&SMBKl6*f0H; zcVRI8@z)fyccIRVfTnJi#|B(lJ?S?|Cgrd=Qt1ONe2(=9q2J@(|GlZc_*mvJaJ|0X zE3Qe7&+7MKDo;P}Uq%wIaRDgYhd1-mGqdsWZT zfsRa{?t5Oi=%CKy{g?N?U-o&qA?$#A4;F{}ucO|FWB{Xu`}WUAQvHey!MlJEgjr~a zkl7q$b970p7?B1?ilEU#d9k~Q2zg$@T8p}_;M4-z2Yi_cHXEZDA4Wa91`Zw}%wyJ$ zCjbB-07*naRADH`xIBqGE3sFdf%g^20GBVkh{K0ZQC~tN31hCF1~fA1)|6AQ1&x$h zOr$?E)w2jPR{2Cgl=%!Pw!AfR#ttnop8~ez6r0gzjFmM3V;XhQ%_oHM1ek9Ddsn(K z>@x8R=opjCM7CCp3M2UkFz_af0CxY##(@!tpyXdzh8lC_{Nx)U1SJp3c+ONWKATBB znp$IBDzI#$r&Z`OR%ce2iHY4t-MA*X!btHK^%yMkFc9Xm9sKbB@YUGc1=f#1unFb_ z1cJM@E3%@KGS>FM)J&hD^)NYLEo1BIF24HfzZc*By+47q^&8OCosJ!uaGg%jJu`U& zEN&pC>+cAF5`c4Oo<=#E;KZq$aKq^n_<}cl5uSSLgJ`D0!UF-LH3MrH)h?s1rH*Td zfYrkWdz*}%OG4tv3(>J#O>D#@-heRyOq?5%=YUYp)w9e>q7w;i#$Cb{&F|n_1`5dr zk050hrHrND6`Z80RM&BYx>B8A0em6>5ZjtzNdUuo3T4M?+}8s(+@(OFbK>5`pp*%> zX+yF^u)3%EsDN=llO87)pa5esL%g~P3cFAujFQpkIHgKLK%3Eo@cfx)P&E}MUiq7ZQKle; zm+uW}Y(QY>@=Ppi2!AaN=8p` zKp1XW#$e&b%Yj1y64FTzl0!tU%+kzi@)ZS02dn}k;4TqTfY%t`5m4b79PYjEet3*< zXk4h-P?UsfYg-e#h(wIpaY2!VDPwF5 z5R7Eoj#eAz7;P;U=TgouI&_St@u>D@IDUE+#M168V^AAV@69!N1Z(yzLOm<7x2%&ziZ8P>z6N7BJtHgI(L`I%APRuqrS|;!ZEF z!yT~gTb9H;aZ~77Yb7(e%9;ksm=(4yAaf?p@wlmxzK2m(A~CX@;k&$h=l-o+$~FLyTsI6q-R?;U(*K+W>|Gavhv9*N}-?cOq#ELxjFpKbp~4 z+T>bJb)QUDapHzk*f_kF!bfzck|?vVPoYUnnfUV}5T-XpuxL=k^^~r3`|!IMcPZ7Y zXA(?GRsR9H+jmOa0%MA`Ri(7I}yf+}}1Zy3(| z#0VI4OFE2I*x0xi$By5F7tcP5lPB)PiBosuGgm%@B6qlW{y9AT#3yk1!a1D26#%KD&=`$~{%Dz5W{^S0i zhT!b}IW;U{d3nq0vUq2|?ETQj!ss9T#NNXOztR4CyL%SKi-&hF-nWk@xG*H~WsP|S zCfKsN(ZZkM=cTQnMcAQt&7ufdtn|SDiHFa`!X>@BEaPS#+|*@t=7Vooocpx=hXcoR z;o4U-1dAme*lqo*uU~;>#U5B95yTaIpUi)?ui+jcAeC7rN)E)2mp(ZTIrS5qFb?ef zw0U2@9a2^|Z#m7qp9#x>6TXaZyYNmSZ#`)Tq$5O{`#dHCbf&7(CuUGuuR& zA47@bFeU?|M({1jFxC#;f~~9Papt+l@y@^Xjd;sj-;Jj}^RIE!t*3GCoBt+O)=GTO z5Bxj)$?yLhKK`NKOmmtDsGB%1#Y<)H8Y3#2<8{J#!@yOHq6Eqng3SzeuY@knAB%1m zvj&5t_HE6WtQlB_7>T_Ycjvoaly-HWjq%+)D~6K^lQ4k}wgYR}EAvc^)LALl#L!hS zrZ$l#(lcEm6{}(KjS+^UYk_)C=EWqF&|9-TG4|UG;6mD0js=9^=uW60OqNOZV<8X- zfMHZ`1|x;xff1IGc(?!@NDSEuY;X{8&E(QPQV@}Wlyx{y@#q0^kVG|wfQcMK{KN|w zFeSi|P{ah%P)sIguo7fq!W2sYYzKZ(D-6%zgiTlN5lpG^-!&sMm|-UXm5^AA0YD=l zX-;yTQ>I&BAXGBGr22im$EddzW5}g1V=Tr9{DpwTBqTASIX?FvWE2BXcp9X_eg<6Mh(NQwVVlXrJo* zh6gI=vAVX7uldGr!e4#t{WyQ_yt?=!F)6LG52F}1xwt0-Ks8r_jn>8Fq-;RJ39JFO zHeUk4_Va_JcJ%(pn7!f#GHh3D=ZD_^AMxpj{}}(^hkg*LxlMp zDVZ%9!pa(}{c-SE|mPBXp>+gr+(IVJsLtuTxQtd{j1TG>M=TfR9h zk5Q;BE9Q`dgli%bptmJ*gbEeViEeEK1gEYiad2Z|3@^D-~j}=-us}yn87F; z!8t%npxO-0RoV5EA(_yw?G61zx4W|=qR9M8P>#XteQ$b0ECbdGZ@PnlTZ31r4uIz z1Can&vF=1m4uimIsu0&_+3L%(z!Q)EHJ*Cn5iooB;4vPp;cLI)JCNBNzxGQ%4R8xf zMo?;PcdQ?g0Ga~ak}?iu0&0c&N_#Gu9F)o#P| zA)p)qC^cX)Q*tXM?IY(Ic~Ri`GcSS($g_Y8XP!q>S2%w9W~?2)0aspn0f$eVLNO{Z z-90bVdJbrs9Fs#Qu+z+e&|hxK)-&C}N=<$dATbM_xa*Eq78-o$cKu;yXnse(t~N6r9j|pMbl?y@W-+r;pqjAckcxDqRbof-s+>fdRc@ z2Z7?RQ%3*09)Vy82%@Ju_D>c)wk1F*yn|E(sSkqo(V}NtDv7%14-~&;wPhAXjtq`$ z+<_OLe*)*8e+JwdY#cd)-RaAC?x{!N+knwz1^2x1E0ATYm`!H~bi-o5^S+1j!gU8J zu8}3bzv$(Cp1P8kJOjOHvM}l5Rj>T4r470Mt6%uyYcPJp(HS^7F9It(Y{FvEwC|pS ztFTzj#p@Yz7BbgGf%cz>mv)U-HfHNytb5m0w4}0xrB$E^IHcqHzI$Iy(u?j|qi)=< z>gt6$QBs!DVLP+eef~l4(XzElP#5^9!E|DHvyCCV2)!I4n_XUQbxq0Abr5n8Ob zd=ll~;8njCyJexerpMI&|M$|(IIbHlvjfC*t*L%71Qa=KNwl!Ax;>$Pb?-ll_g#OS zlF=i|^}+w09Ozhd-2__(BSv?9nIb(Qw@hd(og#7HiTx7bo$tO&3UW_IK|DyJ(cX|8 z`$#lN0R;QqkX()XPwID$mYt9~18Z}XBLUZ(Cop{|n_u-*W+=vV3FQQPu z$2nlOD`(7d1gviy0V81R^0t7PR*ch41xyZG6eEp;*K=tkf_P(;lt>6Rha^+W492C* zch@jBj!Sr|m}t%^Yc9s`lfIR+XksQQIOHmgGakp2Ri86J^dK8g)=^d4fRT9UbP2ow&jlV_^@*EWKF?(4I2oa=F$uN|v!gQiv{#OyYKHG70}=EKrnkj)avmordaS z7Um%tLbWS(&y>L!+K!C|LdsxFMiR#e03qMIR^2Y+8C>VMQFmun-O?BV_+S_)fjm>= zw(Xun8DOnJ9ROeHX;fWz3Bti5xRL7y^+X3vO}Z0E*?%97+CuZk$5`> z4M)a+SJFph62^os!2@j6Ex$#Ix0zf~J&+aDvD0)pN6$&us3)LR0G$-nqwX_>Ub|Sw z)C({*8jbL;fBr+b_4e1}+rREZnjnUIHz0xg&~i!14=F3OR}8~q(0{(kaP;IFKJn4_ zn_NjDX6iI5)gQ%643iBs;Ndr9m-q&3H}LXG~Os=$>u~?Tk@Du!(9WOj_n{Eteyt zxU0UEJ}`w67+ThOB|4|LjUy4J7GFz&LWf@z$=WQI8%mQmX6NbK8<~R~9zt(HYP+EaB$12( ztSQ$)VjW@OxGLa5%F+`Hmp8)H0|Bn>q-ON92V~wn^W-B~J+z8?zK!Yj4$5K$s~g8) z4X|?n%Nv#E{aGnjNL0&F`2Al?dVZ}HF-o_6;}E? zAIXNYbB(dOAscK{iLeu4Ds60Q5zLtpu$v8uyR`2B=HP*iBeJ3GZ41y`PhZA#YY&Db zQ(W6R1SjUIaug3P0h=$sEFe^ABx#5fYIZgvG#cC#3i75yb>ff994ZP1TG^!6kKTl~ z*};`_=Th8~2yo7}r`W!-hkUdOK;w+;%_T#%2kM!}ksCK~^F24pA;JrDlAtaH3{+DA z<4hhjo}qw;jk@?Y8dstyfvT48QsUMCRJ#If<`&3GM&qKQrnGjJ0~uuu?_^^YiajT5 ztZeL(T{#fsfYIu@X9asRVLFQ2v5a5PzK}#jdf-ua7h@&70qNn>S~<+PMiQo2UB}}! zz~;B*318kz58IUS$~8fzvKaMXEWajvZE;iqDZSCy^U0IXTH>pezbkJC534ku3D zfh$*@!@0B1pq>&Y6AR`VO}mSi&V2@>(FS~Q0~W$iJ&g)YrvP;ua11fgT`yuSDrcS6 zBNCIlxz{UwS$w?xIx@$L0d$AVu72_QcwC6z^FAC?aox06?R&bi==Qm%s!Z&>LZ#sb z1Z#~3MW&w4rU%HV@X|tE7att^NkNv`bPJz-{Lk^ivkw7=U^0W3&OVLI7JyOdn+2>N zIgN632!=e0qUg%$m2b=fys+=t!wU5eU5npFSGE0j^*;65*bUXfKtRAQ$*wy5e)-7s z>lJ~5DAeS=i3u~7-+n-ei?6Pd5(Dvo%RZkMLx8T&0~||jJfLoaxQW+6ZU$u}Htu4n zv2HvV`+fK00I)6|{^R6V?O|xir^U=_Q{CeqvA9)!*b#)o=XpC z2A0kKgI;2V_sR1i%Bey126bG%x=8Qwq3yDjANWwchZdH=lE(z^;k`fLsS95W2Bx%lFUPwU%QSPHTexN>G4D7aw_}Wpn=s$m!jV&V z2;%;lk0dK{2tYZKM1ey8;sLPe06@Ky@&b|s1V~9G){b@teE^tV z1zaTu+Ti4TI$mdtjR37gqVF^w7>y)ESR@_9@Jz)-l9e~nC%GxS6H?JVi<=j$EG)88S*q_b; zP4ukml`r6-7&=_bxc!d1@Gal*KjDx6;OB7p((~Banj$Y(I6t zU3^Uj{7kNk+11%Id`l8j)JbDGNQUlmOiJz|copVp$59;@BrHw?VJL@X$;)sMP-r-1 z_ms5H3?OueGG)@m3@tIa1%SlDkjg5E=(}Mg*2&8OSrH_?x08e*t_`^Tw%hR1`EzKS z8r}!gl?dszwH$;1BF5Tk32OFtbRm zmzfbZpU^~wS!UHum7T=Gvo^ppN&Gvn@#d8Go)M=5KNtv}Bo>|mF(zhCB7%1nKK0U`J&9Nj3kLoZiKn$9s)34If}I-bL?(jMj)iD&gCS>Y`enY$qLGmF#g)s zp{+eaFgSh3aolj*N%&U4b)m2eunz;wcO@R>?lz;o0<0fl6eFP7W7Jb5BSbTkP259% zceTXdC`Qu9&I6SPf&z)GhgXSBB6G_(K#U~JEQxhmC+j3vE2Hyz@bVI?Q4*49R`wnx zKg$8i0Mwlh+_unUJHjL;ATeeQ1b8%xNC`n%n4`5?fh3-zv=s!5RsLqbk z80^Jx(o|9>=jD(zT4R_@SlLz&f=vy~r@PqQzKrc_=Mh|uJ6`t|yygC{!efv90nWbo zX*~bjCvfv??!nsHaWH$Kn4{ZByII;}kS!`keFdebj1w>bM!5HX|LvkmwEt>Oo4RCu zqrvZ8y`oaXy&v?t-oYnbMlt!4$`qomuz0ZJP8EycFJg={=B|cf8I9l+p<{mZo7Q8% z47*_8Cu1yTvt2y&;2-1cna^VN&|&QDT*a}Ir_oe3u3fx}tS|`dKxA?G%8U5xzj`0O z;;a5HPTX({T-&AtW{UgA%b#`7Yq4&<=ITL}_g|g%9qR5`EObG4XAaBb1-?R!4*Erm zjlP@53op`!#B&cZ9Z<4d&^8VF)f=pXK@DP?<9xy3Js^f6*Nb!5f6hUz;w4wb=Sy72 zgBZ2Ft6xp`0+L)~A85buqTw4F{--~Y)-RSv>Fb3;5GQKnF%H>Vy;ks&q$~SenGdLN zL^cMU8SmZqBmf7@-~J_^$M@XVV0zVhH4j*vDNzQ!QmdB>nI#jse_ii=VwO|?zUw`6 z*_?hg9ksm7rM-W>sTtSvU@pGGT;|>z-uI=HmzgehGD}4`y6!}nhvxfQV*0ko8rDE#un*EITuSgUCD8(#A1nLU(WIZB^a2`;d=)t zW=2s=;9Ub6A3_K%f_EtM<1qFHn)u*Zp_z#eEOF3*Y&V{ylEE z%ei^BBN>^k(f!Ma8??y; zcpI(CwQVKfn{Nq$7{SO~2&}{eBYFu>qYGo_DPs$yBny%-0idzyx%G|=Nl}sjV-0C8 z1L?!MhU6Ons{u(nJ?~;fHjVUv$&unC=IYti&H!W*pRiaJ!hvgna+O7|)sRMp05cN( z3+gSDkD#nEVD~Cxe3-CtM9OS6VWO|QdB-d|b96C}>{F}p`qLbY^H7_TT-VjRD7q`h z47PDCTSG~2i$vha=(IqJA!tm;B4ZW+C}v>>Bq@+Yep5Y(yY(GG*6325Vk@fqs!=kC z(v^Xf8*FYrga7=CKZ4cO6ZqgC{0g4?+#~pbfBH)}a`bp|g^u^e`Q9gA1PGXV z>B2eu^iO>o+P20!zv8>#y(C`w@SprVtTFfx|L(g`<{J=Al(BVfTUeQaFHthAs&>}l8{C+Nf269O8O{d{34Jqe_7*HG8zUj5;k!@mh+3>k)$a31ei8L zn8~5xz8VDwGU_$Ph#|q`3~p$UaA)zg-1S(u ziOd8Ti12Nklpd3kh;AG@27BmOg$>JLIk3=@$wDuxOnkA zsLb$|`yaqd&tAh*pZzS1wO#7*=(Fu2uq8@&mI2_c^8zqr*#p4Q5a1z-F%EqAmmII&_@M_RhLWe zHSoRxyd(XdP!xsKgQSgKSxd$c*u?|B06bPxc}G`jZY9WT3QFpsCLNh80ul=7NV?F$ zP+*Z1xI}ko3XFPgFj`ZZvTm}j(R69bt(|=Il&k{RJDDeNNMo5g z!B@>y=;#IrNE|x@v2-*u0WaBWW+Nl>g-C@&rgMV@c=9zPD=bE8Ej@bUDXeVd*u6GI zo*Rr0t-+4hK!GruO8@zm;A)RNFLBcsoW{noQ5;Ma0u#Fm8| zK3prH+h`R4u`&+>_IBj(;=FA9rN&EO$ib><7-XfK;G+w=^b1)Czz)C^GXKdi+PPX# zqa-DX34vM;9jpgDhCDMfF`-BlqS5Unq}Ywc3V?YKmukdKiZ$k`j91Ja7w5EOEeps> zaUY2m+qh=E7eh=;7vgKyI4EK{_pN||9=u8n%Vn7qD2n(*$ z;~1vzFBfZ|K$D4gl9c5=BeM9CjUnmGTH7VKNoVLmvh;c9!6}k5b=JmIk3Ju=ua
O_A;(sI)mHpcmRHo9HU=99)9e# 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