diff --git a/lib/data_objects/glicko.dart b/lib/data_objects/glicko.dart new file mode 100644 index 0000000..50b4e88 --- /dev/null +++ b/lib/data_objects/glicko.dart @@ -0,0 +1,147 @@ +import 'dart:math'; +// I reimplemented kenany/glicko2-lite in dart lol +// Don't look here lol + +List scale(double rating, double rd, double options) { + double mu = (rating - options) / 173.7178; + double phi = rd / 173.7178; + return [ mu, phi ]; +} + +double g(phi) { + return 1 / sqrt(1 + 3 * pow(phi, 2) / pow(pi, 2)); +} + +double e(double mu, double muj, double phij) { + return 1 / (1 + exp(-g(phij) * (mu - muj))); +} + +List> scaleOpponents(double mu, List> opponents, double rating) { + return opponents.map((opp) { + var scaled = scale(opp[0], opp[1], rating); + return { + "muj": scaled[0], + "phij": scaled[1], + "gphij": g(scaled[1]), + "emmp": e(mu, scaled[0], scaled[1]), + "score": opp[2] + }; + }).toList(); +} + +double updateRating(List> opponents) { + double value = pow(opponents.first["gphij"]!, 2) * opponents.first["emmp"]! * (1 - opponents.first["emmp"]!); + opponents.skip(1).forEach((element) { + value += pow(element["gphij"]!, 2) * element["emmp"]! * (1 - element["emmp"]!); + }); + return 1 / value; +} + +double computeDelta(v, List> opponents) { + double value = opponents.first["gphij"]! * (opponents.first["score"]! - opponents.first["emmp"]!); + opponents.skip(1).forEach((element) { + value += opponents.first["gphij"]! * (opponents.first["score"]! - opponents.first["emmp"]!); + }); + return v * value; +} + +Function volF(double phi, double v, double delta, double a, double tau) { + num phi2 = pow(phi, 2); + num d2 = pow(delta, 2); + + return (x) { + double ex = exp(x); + double a2 = phi2 + v + ex; + double p2 = (x - a) / pow(tau, 2); + double p1 = (ex * (d2 - phi2 - v - ex)) / (2 * pow(a2, 2)); + return p1 - p2; + }; +} + +double computeVolatility(double sigma, double phi, double v, double delta, double options) { + // 5.1 + double a = log(pow(sigma, 2)); + Function f = volF(phi, v, delta, a, options); + + // 5.2 + double b; + if (pow(delta, 2) > pow(phi, 2) + v) { + b = log(pow(delta, 2) - pow(phi, 2) - v); + } + else { + double k = 1; + while (f(a - k * options) < 0) { + k++; + } + b = a - k * options; + } + + // 5.3 + double fa = f(a); + double fb = f(b); + + // 5.4 + while ((b - a).abs() > 0.000001) { + double c = a + (a - b) * fa / (fb - fa); + double fc = f(c); + + if (fc * fb <= 0) { + a = b; + fa = fb; + } + else { + fa /= 2; + } + + b = c; + fb = fc; + } + + // 5.5 + return exp(a / 2); +} + +double phiStar(sigmap, phi) { + return sqrt(pow(sigmap, 2) + pow(phi, 2)); +} + +Map newRating(phis, mu, v, opponents) { + double phip = 1 / sqrt(1 / pow(phis, 2) + 1 / v); + double value = opponents.first["gphij"]! * (opponents.first["score"]! - opponents.first["emmp"]!); + opponents.skip(1).forEach((element) { + value += element["gphij"]! * (element["score"]! - element["emmp"]!); + }); + double mup = mu + pow(phip, 2) * value; + return { "mu": mup, "phi": phip }; +} + +List unscale(mup, phip, options) { + double rating = 173.7178 * mup + options["rating"]; + double rd = 173.7178 * phip; + return [ rating, rd ]; +} + +List rate(double rating, double rd, double sigma, List> opponents, Map options) { + Map opts = { "rating": options["rating"]??1500, "tau": options["tau"]??0.5 }; + + // Step 2 + List scaled = scale(rating, rd, opts["rating"]!); + List> scaledOpponents = scaleOpponents(scaled[0], opponents, opts["rating"]!); + + // Step 3 + double v = updateRating(scaledOpponents); + + // Step 4 + double delta = computeDelta(v, scaledOpponents); + + // Step 5 + double sigmap = computeVolatility(sigma, scaled[1], v, delta, opts["tau"]!); + + // Step 6 + double phis = phiStar(sigmap, scaled[1]); + + // Step 7 + Map updated = newRating(phis, scaled[0], v, scaledOpponents); + + return unscale(updated['mu'], updated['phi'], opts)..add(sigmap); +} \ No newline at end of file diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index fba0986..0e300a6 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -17,6 +17,9 @@ 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" +]; const Map rankCutoffs = { "x": 0.01, "u": 0.05, @@ -39,7 +42,7 @@ const Map rankCutoffs = { "": 0.5 }; const Map rankTargets = { - "x": 24008, + "x": 24503.75, // where that comes from? "u": 23038, "ss": 21583, "s+": 20128, @@ -138,45 +141,87 @@ const Map rankColors = { // thanks osk for const rankColors at ht 'z': Color(0xFF375433) }; -const Map sprintAverages = { // based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098 - 'x': Duration(seconds: 25, milliseconds: 413), - 'u': Duration(seconds: 34, milliseconds: 549), - 'ss': Duration(seconds: 43, milliseconds: 373), - 's+': Duration(seconds: 54, milliseconds: 027), - 's': Duration(seconds: 60, milliseconds: 412), - 's-': Duration(seconds: 67, milliseconds: 381), - 'a+': Duration(seconds: 73, milliseconds: 694), - 'a': Duration(seconds: 81, milliseconds: 166), - 'a-': Duration(seconds: 88, milliseconds: 334), - 'b+': Duration(seconds: 93, milliseconds: 741), - 'b': Duration(seconds: 98, milliseconds: 354), - 'b-': Duration(seconds: 109, milliseconds: 610), - 'c+': Duration(seconds: 124, milliseconds: 641), - 'c': Duration(seconds: 126, milliseconds: 104), - 'c-': Duration(seconds: 145, milliseconds: 865), - 'd+': Duration(seconds: 154, milliseconds: 338), - 'd': Duration(seconds: 162, milliseconds: 063), +// const Map sprintAverages = { // old data, based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098 +// 'x': Duration(seconds: 25, milliseconds: 413), +// 'u': Duration(seconds: 34, milliseconds: 549), +// 'ss': Duration(seconds: 43, milliseconds: 373), +// 's+': Duration(seconds: 54, milliseconds: 027), +// 's': Duration(seconds: 60, milliseconds: 412), +// 's-': Duration(seconds: 67, milliseconds: 381), +// 'a+': Duration(seconds: 73, milliseconds: 694), +// 'a': Duration(seconds: 81, milliseconds: 166), +// 'a-': Duration(seconds: 88, milliseconds: 334), +// 'b+': Duration(seconds: 93, milliseconds: 741), +// 'b': Duration(seconds: 98, milliseconds: 354), +// 'b-': Duration(seconds: 109, milliseconds: 610), +// 'c+': Duration(seconds: 124, milliseconds: 641), +// 'c': Duration(seconds: 126, milliseconds: 104), +// 'c-': Duration(seconds: 145, milliseconds: 865), +// 'd+': Duration(seconds: 154, milliseconds: 338), +// 'd': Duration(seconds: 162, milliseconds: 063), +// //'z': Duration(seconds: 66, milliseconds: 802) +// }; + +// const Map blitzAverages = { +// 'x': 626494, +// 'u': 406059, +// 'ss': 243166, +// 's+': 168636, +// 's': 121594, +// 's-': 107845, +// 'a+': 87142, +// 'a': 73413, +// 'a-': 60799, +// 'b+': 55417, +// 'b': 47608, +// 'b-': 40534, +// 'c+': 34200, +// 'c': 32535, +// 'c-': 25808, +// 'd+': 23345, +// 'd': 23063, +// //'z': 72084 +// }; + +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': 626494, - 'u': 406059, - 'ss': 243166, - 's+': 168636, - 's': 121594, - 's-': 107845, - 'a+': 87142, - 'a': 73413, - 'a-': 60799, - 'b+': 55417, - 'b': 47608, - 'b-': 40534, - 'c+': 34200, - 'c': 32535, - 'c-': 25808, - 'd+': 23345, - 'd': 23063, + '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 }; @@ -753,16 +798,15 @@ class EstTr { 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); + //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-( - 0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4 - ) + 1500-estglicko )*pi )/sqrt( ( @@ -1246,6 +1290,8 @@ class TetrioPlayersLeaderboard { 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)){ @@ -1257,20 +1303,6 @@ class TetrioPlayersLeaderboard { return lb; } - List getStatRankingSequel(Stats stat){ - List lb = List.from(leaderboard); - lb.sort(((a, b) { - if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){ - return -1; - }else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){ - return 0; - }else{ - return 1; - } - })); - return lb; - } - List getAverageOfRank(String rank){ // i tried to refactor it and that's was terrible if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank"); List filtredLeaderboard = List.from(leaderboard); @@ -1853,11 +1885,12 @@ class TetrioPlayersLeaderboard { "avgStride": avgStride, "avgInfDS": avgInfDS, "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].rating : lowestTR, + "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0, "entries": filtredLeaderboard }]; }else{ return [TetraLeagueAlpha(timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, rating: 0, rank: rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), - {"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0}]; + {"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0, "toEnterGlicko": 0}]; } } @@ -1926,6 +1959,26 @@ class TetrioPlayersLeaderboard { '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; diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index e453dd3..406675b 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,5 +1,4 @@ import 'dart:math'; -import 'package:vector_math/vector_math_64.dart'; import 'tetrio.dart'; diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 1bed291..eb88122 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1120 (560 per locale) +/// Strings: 1144 (572 per locale) /// -/// Built on 2024-03-24 at 14:28 UTC +/// Built on 2024-05-28 at 20:38 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -216,7 +216,12 @@ class Translations implements BaseTranslations { String verdictGeneral({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; String get verdictBetter => 'better'; String get verdictWorse => 'worse'; + String get smooth => 'Smooth'; String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; + String numOfVictories({required Object wins}) => '~${wins} victories'; + String get promotionOnNextWin => 'Promotion on next win'; + String numOfdefeats({required Object losses}) => '~${losses} defeats'; + String get demotionOnNextLoss => 'Demotion on next loss'; String get nerdStats => 'Nerd Stats'; String get playersYouTrack => 'Players you track'; String get formula => 'Formula'; @@ -298,6 +303,9 @@ class Translations implements BaseTranslations { String get calc => 'Calc'; String get calcViewNoValues => 'Enter values to calculate the stats'; String get rankAveragesViewTitle => 'Ranks cutoff and average stats'; + String get sprintAndBlitsViewTitle => '40 lines and Blitz averages'; + String sprintAndBlitsRelevance({required Object date}) => 'Relevance: ${date}'; + String get rank => 'Rank'; String get averages => 'Averages'; String get lbViewZeroEntrys => 'Empty list'; String get lbViewOneEntry => 'There is only one player'; @@ -335,6 +343,7 @@ class Translations implements BaseTranslations { String currentAxis({required Object axis}) => '${axis} axis:'; String get p1nkl0bst3rAlert => 'That data was retrived from third party API maintained by p1nkl0bst3r'; String get notForWeb => 'Function is not available for web version'; + late final _StringsGraphsEn graphs = _StringsGraphsEn._(_root); late final _StringsStatCellNumEn statCellNum = _StringsStatCellNumEn._(_root); Map get playerRole => { 'user': 'User', @@ -634,6 +643,19 @@ class _StringsNewsPartsEn { String unknownNews({required Object type}) => 'Unknown news of type ${type}'; } +// Path: graphs +class _StringsGraphsEn { + _StringsGraphsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get attack => 'Attack'; + String get speed => 'Speed'; + String get defense => 'Defense'; + String get cheese => 'Cheese'; +} + // Path: statCellNum class _StringsStatCellNumEn { _StringsStatCellNumEn._(this._root); @@ -690,7 +712,7 @@ class _StringsStatCellNumEn { String get nyaappDescription => '(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.'; String get area => 'Area'; String get areaDescription => 'How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections'; - String get estOfTR => 'Est. of TR'; + String get estOfTR => 'Estimated TR'; String get estOfTRShort => 'Est. TR'; String get accOfEst => 'Accuracy'; String get accOfEstShort => 'Acc.'; @@ -763,16 +785,16 @@ class _StringsErrorsEn { String forbiddenSub({required Object nickname}) => 'If you are using VPN or Proxy, turn it off. If this does not help, reach out to ${nickname}'; String get tooManyRequests => 'You have been rate limited.'; String get tooManyRequestsSub => 'Wait a few moments and try again'; - String get internal => 'Something happend on the tetr.io side'; + String get internal => 'Something happened on the tetr.io side'; String get internalSub => 'osk, probably, already aware about it'; - String get internalWebVersion => 'Something happend on the tetr.io side (or on oskware_bridge, idk honestly)'; + String get internalWebVersion => 'Something happened on the tetr.io side (or on oskware_bridge, idk honestly)'; String get internalWebVersionSub => 'If osk status page says that everything is ok, let dan63047 know about this issue'; - String get oskwareBridge => 'Something happend with oskware_bridge'; + String get oskwareBridge => 'Something happened with oskware_bridge'; String get oskwareBridgeSub => 'Let dan63047 know'; String get p1nkl0bst3rForbidden => 'Third party API blocked your IP address'; String get p1nkl0bst3rTooManyRequests => 'Too many requests to third party API. Try again later'; - String get p1nkl0bst3rinternal => 'Something happend on the p1nkl0bst3r side'; - String get p1nkl0bst3rinternalWebVersion => 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; + String get p1nkl0bst3rinternal => 'Something happened on the p1nkl0bst3r side'; + String get p1nkl0bst3rinternalWebVersion => 'Something happened on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; String get replayAlreadySaved => 'Replay already saved'; String get replayExpired => 'Replay expired and not available anymore'; String get replayRejected => 'Third party API blocked your IP address'; @@ -870,7 +892,12 @@ class _StringsRu implements Translations { @override String verdictGeneral({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; @override String get verdictBetter => 'Лучше'; @override String get verdictWorse => 'Хуже'; + @override String get smooth => 'Гладкий'; @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 => 'Формула'; @@ -952,6 +979,9 @@ class _StringsRu implements Translations { @override String get calc => 'Считать'; @override String get calcViewNoValues => 'Введите значения, чтобы посчитать статистику'; @override String get rankAveragesViewTitle => 'Требования рангов и средние значения'; + @override String get sprintAndBlitsViewTitle => 'Средние результаты 40 линий и блица'; + @override String sprintAndBlitsRelevance({required Object date}) => 'Актуальность: ${date}'; + @override String get rank => 'Ранг'; @override String get averages => 'Средние значения'; @override String get lbViewZeroEntrys => 'Рейтинговая таблица пуста'; @override String get lbViewOneEntry => 'В рейтинговой таблице всего один игрок'; @@ -989,6 +1019,7 @@ class _StringsRu implements Translations { @override String currentAxis({required Object axis}) => 'Ось ${axis}:'; @override String get p1nkl0bst3rAlert => 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; @override String get notForWeb => 'Функция недоступна для веб версии'; + @override late final _StringsGraphsRu graphs = _StringsGraphsRu._(_root); @override late final _StringsStatCellNumRu statCellNum = _StringsStatCellNumRu._(_root); @override Map get playerRole => { 'user': 'Пользователь', @@ -1288,6 +1319,19 @@ class _StringsNewsPartsRu implements _StringsNewsPartsEn { @override String unknownNews({required Object type}) => 'Неизвестная новость типа ${type}'; } +// Path: graphs +class _StringsGraphsRu implements _StringsGraphsEn { + _StringsGraphsRu._(this._root); + + @override final _StringsRu _root; // ignore: unused_field + + // Translations + @override String get attack => 'Атака'; + @override String get speed => 'Скорость'; + @override String get defense => 'Защита'; + @override String get cheese => 'Сыр'; +} + // Path: statCellNum class _StringsStatCellNumRu implements _StringsStatCellNumEn { _StringsStatCellNumRu._(this._root); @@ -1516,7 +1560,12 @@ extension on Translations { case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; case 'verdictBetter': return 'better'; case 'verdictWorse': return 'worse'; + case 'smooth': return 'Smooth'; case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; + case 'numOfVictories': return ({required Object wins}) => '~${wins} victories'; + case 'promotionOnNextWin': return 'Promotion on next win'; + case 'numOfdefeats': return ({required Object losses}) => '~${losses} defeats'; + case 'demotionOnNextLoss': return 'Demotion on next loss'; case 'nerdStats': return 'Nerd Stats'; case 'playersYouTrack': return 'Players you track'; case 'formula': return 'Formula'; @@ -1598,6 +1647,9 @@ extension on Translations { case 'calc': return 'Calc'; case 'calcViewNoValues': return 'Enter values to calculate the stats'; case 'rankAveragesViewTitle': return 'Ranks cutoff and average stats'; + case 'sprintAndBlitsViewTitle': return '40 lines and Blitz averages'; + case 'sprintAndBlitsRelevance': return ({required Object date}) => 'Relevance: ${date}'; + case 'rank': return 'Rank'; case 'averages': return 'Averages'; case 'lbViewZeroEntrys': return 'Empty list'; case 'lbViewOneEntry': return 'There is only one player'; @@ -1635,6 +1687,10 @@ extension on Translations { case 'currentAxis': return ({required Object axis}) => '${axis} axis:'; case 'p1nkl0bst3rAlert': return 'That data was retrived from third party API maintained by p1nkl0bst3r'; case 'notForWeb': return 'Function is not available for web version'; + case 'graphs.attack': return 'Attack'; + case 'graphs.speed': return 'Speed'; + case 'graphs.defense': return 'Defense'; + case 'graphs.cheese': return 'Cheese'; case 'statCellNum.xpLevel': return 'XP Level'; case 'statCellNum.xpProgress': return 'Progress to next level'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Progress from 0 XP to level ${n}'; @@ -1684,7 +1740,7 @@ extension on Translations { case 'statCellNum.nyaappDescription': return '(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.'; case 'statCellNum.area': return 'Area'; case 'statCellNum.areaDescription': return 'How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections'; - case 'statCellNum.estOfTR': return 'Est. of TR'; + case 'statCellNum.estOfTR': return 'Estimated TR'; case 'statCellNum.estOfTRShort': return 'Est. TR'; case 'statCellNum.accOfEst': return 'Accuracy'; case 'statCellNum.accOfEstShort': return 'Acc.'; @@ -1738,16 +1794,16 @@ extension on Translations { case 'errors.forbiddenSub': return ({required Object nickname}) => 'If you are using VPN or Proxy, turn it off. If this does not help, reach out to ${nickname}'; case 'errors.tooManyRequests': return 'You have been rate limited.'; case 'errors.tooManyRequestsSub': return 'Wait a few moments and try again'; - case 'errors.internal': return 'Something happend on the tetr.io side'; + case 'errors.internal': return 'Something happened on the tetr.io side'; case 'errors.internalSub': return 'osk, probably, already aware about it'; - case 'errors.internalWebVersion': return 'Something happend on the tetr.io side (or on oskware_bridge, idk honestly)'; + case 'errors.internalWebVersion': return 'Something happened on the tetr.io side (or on oskware_bridge, idk honestly)'; case 'errors.internalWebVersionSub': return 'If osk status page says that everything is ok, let dan63047 know about this issue'; - case 'errors.oskwareBridge': return 'Something happend with oskware_bridge'; + case 'errors.oskwareBridge': return 'Something happened with oskware_bridge'; case 'errors.oskwareBridgeSub': return 'Let dan63047 know'; case 'errors.p1nkl0bst3rForbidden': return 'Third party API blocked your IP address'; case 'errors.p1nkl0bst3rTooManyRequests': return 'Too many requests to third party API. Try again later'; - case 'errors.p1nkl0bst3rinternal': return 'Something happend on the p1nkl0bst3r side'; - case 'errors.p1nkl0bst3rinternalWebVersion': return 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; + case 'errors.p1nkl0bst3rinternal': return 'Something happened on the p1nkl0bst3r side'; + case 'errors.p1nkl0bst3rinternalWebVersion': return 'Something happened on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; case 'errors.replayAlreadySaved': return 'Replay already saved'; case 'errors.replayExpired': return 'Replay expired and not available anymore'; case 'errors.replayRejected': return 'Third party API blocked your IP address'; @@ -2096,7 +2152,12 @@ extension on _StringsRu { case 'verdictGeneral': return ({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; case 'verdictBetter': return 'Лучше'; case 'verdictWorse': return 'Хуже'; + case 'smooth': return 'Гладкий'; 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 'Формула'; @@ -2178,6 +2239,9 @@ extension on _StringsRu { case 'calc': return 'Считать'; case 'calcViewNoValues': return 'Введите значения, чтобы посчитать статистику'; case 'rankAveragesViewTitle': return 'Требования рангов и средние значения'; + case 'sprintAndBlitsViewTitle': return 'Средние результаты 40 линий и блица'; + case 'sprintAndBlitsRelevance': return ({required Object date}) => 'Актуальность: ${date}'; + case 'rank': return 'Ранг'; case 'averages': return 'Средние значения'; case 'lbViewZeroEntrys': return 'Рейтинговая таблица пуста'; case 'lbViewOneEntry': return 'В рейтинговой таблице всего один игрок'; @@ -2215,6 +2279,10 @@ extension on _StringsRu { case 'currentAxis': return ({required Object axis}) => 'Ось ${axis}:'; case 'p1nkl0bst3rAlert': return 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; case 'notForWeb': return 'Функция недоступна для веб версии'; + case 'graphs.attack': return 'Атака'; + case 'graphs.speed': return 'Скорость'; + case 'graphs.defense': return 'Защита'; + case 'graphs.cheese': return 'Сыр'; case 'statCellNum.xpLevel': return 'Уровень\nопыта'; case 'statCellNum.xpProgress': return 'Прогресс до следующего уровня'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Прогресс от 0 XP до ${n} уровня'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index a78532f..8c118ae 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -70,6 +70,7 @@ class TetrioService extends DB { // I'm trying to send as less requests, as possible, so i'm caching the results of those requests. // Usually those maps looks like this: {"cached_until_unix_milliseconds": Object} + // TODO: Make a proper caching system final Map _playersCache = {}; final Map> _recordsCache = {}; final Map _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]} @@ -77,6 +78,8 @@ class TetrioService extends DB { final Map _lbPositions = {}; final Map> _newsCache = {}; final Map> _topTRcache = {}; + final Map>> _cutoffsCache = {}; + final Map _topOneFromLB = {}; final Map _tlStreamsCache = {}; /// Thing, that sends every request to the API endpoints final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); @@ -297,6 +300,119 @@ class TetrioService extends DB { // Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above, // so i'm going to document only unique differences between them + Future>> fetchCutoffs() async { + try{ + var cached = _cutoffsCache.entries.first; + if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired + developer.log("fetchCutoffs: Cutoffs retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); + return cached.value; + }else{ // if cache expired + _topTRcache.remove(cached.key); + developer.log("fetchCutoffs: Cutoffs expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); + } + }catch(e){ // actually going to obtain + developer.log("fetchCutoffs: Trying to retrieve Cutoffs", name: "services/tetrio_crud"); + } + + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLCutoffs"}); + } else { + url = Uri.https('api.p1nkl0bst3r.xyz', 'rankcutoff', {"users": null}); + } + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + Map rawData = jsonDecode(response.body); + Map data = rawData["cutoffs"] as Map; + Map trCutoffs = {}; + Map glickoCutoffs = {}; + for (String rank in data.keys){ + trCutoffs[rank] = data[rank]["rating"]; + glickoCutoffs[rank] = data[rank]["glicko"]; + } + _cutoffsCache[(rawData["ts"] + 300000).toString()] = [trCutoffs, glickoCutoffs]; + return [trCutoffs, glickoCutoffs]; + case 404: + developer.log("fetchCutoffs: 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: + throw P1nkl0bst3rInternalProblem(); + default: + developer.log("fetchCutoffs: 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 { + try{ + var cached = _topOneFromLB.entries.first; + if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired + developer.log("fetchTopOneFromTheLeaderboard: Leader retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); + return cached.value; + }else{ // if cache expired + _topTRcache.remove(cached.key); + developer.log("fetchTopOneFromTheLeaderboard: Leader expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); + } + }catch(e){ // actually going to obtain + developer.log("fetchTopOneFromTheLeaderboard: Trying to retrieve leader", name: "services/tetrio_crud"); + } + + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLTopOne"}); + } else { + url = Uri.https('ch.tetr.io', 'api/users/lists/league', {"after": "25000", "limit": "1"}); + } + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + var rawJson = jsonDecode(response.body); + return TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["users"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"])); + case 404: + throw TetrioPlayerNotExist(); + // if not 200 or 404 - throw a unique for each code exception + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw P1nkl0bst3rInternalProblem(); + default: + developer.log("fetchTopOneFromTheLeaderboard: Failed to fetch top one", 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 + } + } + /// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data. Future> fetchAndsaveTLHistory(String id) async { diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index 412820d..bd6c119 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -2,6 +2,7 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; +final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2; final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart index 5688d26..45c7749 100644 --- a/lib/views/calc_view.dart +++ b/lib/views/calc_view.dart @@ -23,14 +23,12 @@ class CalcView extends StatefulWidget { } class CalcState extends State { - late ScrollController _scrollController; TextEditingController ppsController = TextEditingController(); TextEditingController apmController = TextEditingController(); TextEditingController vsController = TextEditingController(); @override void initState() { - _scrollController = ScrollController(); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); windowManager.setTitle("Tetra Stats: ${t.statsCalc}"); @@ -109,7 +107,7 @@ class CalcState extends State { ], ), ), - Divider(), + const Divider(), if (nerdStats == null) Text(t.calcViewNoValues) else Column(children: [ _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 44d0156..3129ea8 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -257,7 +257,7 @@ class CompareState extends State { backgroundColor: Colors.black, body: SingleChildScrollView( controller: _scrollController, - physics: AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 768), @@ -317,7 +317,7 @@ class CompareState extends State { ], ), ), - Divider(), + const Divider(), if (!listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])) Column( children: [ if (theGreenSide[0] != null && theRedSide[0] != null && theGreenSide[0]!.role != "banned" && theRedSide[0]!.role != "banned") @@ -816,21 +816,23 @@ class CompareThingy extends StatelessWidget { child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - stops: [ - 0.0, - higherIsBetter - ? greenSide > redSide - ? 0.6 - : 0 - : greenSide < redSide - ? 0.6 - : 0 - ], - )), + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + transform: const GradientRotation(0.6), + stops: [ + 0.0, + higherIsBetter + ? greenSide > redSide + ? 0.6 + : 0 + : greenSide < redSide + ? 0.6 + : 0 + ], + ) + ), child: Text( (prefix ?? "") + f.format(greenSide) + (postfix ?? ""), style: const TextStyle( @@ -838,7 +840,12 @@ class CompareThingy extends StatelessWidget { shadows: [ Shadow( offset: Offset(0.0, 0.0), - blurRadius: 3.0, + blurRadius: 1.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 2.0, color: Colors.black, ), Shadow( @@ -874,6 +881,7 @@ class CompareThingy extends StatelessWidget { colors: const [Colors.red, Colors.transparent], begin: Alignment.centerRight, end: Alignment.centerLeft, + transform: const GradientRotation(-0.6), stops: [ 0.0, higherIsBetter diff --git a/lib/views/customization_view.dart b/lib/views/customization_view.dart index e7c22a4..4d85148 100644 --- a/lib/views/customization_view.dart +++ b/lib/views/customization_view.dart @@ -1,10 +1,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart'; import 'package:window_manager/window_manager.dart'; late String oldWindowTitle; diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index c7e46e0..5cb42e1 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -7,10 +7,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; -import 'dart:math'; -import 'package:fl_chart/fl_chart.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/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; @@ -19,6 +18,7 @@ import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/ranks_averages_view.dart' show RankAveragesView; +import 'package:tetra_stats/views/sprint_and_blitz_averages.dart'; import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; import 'package:tetra_stats/widgets/finesse_thingy.dart'; @@ -35,6 +35,8 @@ import 'package:go_router/go_router.dart'; final TetrioService teto = TetrioService(); // thing, that manadge our local DB int _chartsIndex = 0; bool _gamesPlayedInsteadOfDateAndTime = false; +late ZoomPanBehavior _zoomPanBehavior; +bool _smooth = false; List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; late ScrollController _scrollController; final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode); @@ -81,11 +83,14 @@ class _MainState extends State with TickerProviderStateMixin { TetrioPlayersLeaderboard? everyone; PlayerLeaderboardPosition? meAmongEveryone; TetraLeagueAlpha? rankAverages; + double? thatRankCutoff; + double? nextRankCutoff; + double? thatRankGlickoCutoff; + double? nextRankGlickoCutoff; String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for - String _titleNickname = "dan63047"; + String _titleNickname = ""; /// Each dropdown menu item contains list of dots for the graph - var chartsData = >>[]; - var chartsDataGamesPlayed = >>[]; + List>> chartsData = []; //var tableData = []; final bodyGlobalKey = GlobalKey(); bool _showSearchBar = false; @@ -102,13 +107,18 @@ class _MainState extends State with TickerProviderStateMixin { _scrollController = ScrollController(); _tabController = TabController(length: 6, vsync: this); _wideScreenTabController = TabController(length: 4, vsync: this); - + _zoomPanBehavior = ZoomPanBehavior( + enablePinching: true, + enableSelectionZooming: true, + enableMouseWheelZooming : true, + enablePanning: true, + ); // We need to show something if (widget.player != null){ // if we have user input, changePlayer(widget.player!); // it's gonna be user input }else{ _getPreferences() // otherwise, checking for preferences - .then((value) => changePlayer(prefs.getString("player") ?? "dan63047")); // no preferences - loading me + .then((value) => changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc")); // no preferences - loading me } super.initState(); } @@ -166,26 +176,41 @@ class _MainState extends State with TickerProviderStateMixin { late TetraLeagueAlphaStream tlStream; late Map records; late List news; + late TetrioPlayerFromLeaderboard? topOne; late double? topTR; requests = await Future.wait([ // all at once teto.fetchTLStream(_searchFor), teto.fetchRecords(_searchFor), teto.fetchNews(_searchFor), + prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>>[]), + (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR ]); tlStream = requests[0] as TetraLeagueAlphaStream; records = requests[1] as Map; news = requests[2] as List; - topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR + topOne = requests[4] as TetrioPlayerFromLeaderboard?; + topTR = requests.elementAtOrNull(5) as double?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); - if (meAmongEveryone == null && prefs.getBool("showPositions") == true){ + if (prefs.getBool("showPositions") == true){ // Get tetra League leaderboard everyone = teto.getCachedLeaderboard(); everyone ??= await teto.fetchTLLeaderboard(); - meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); - if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); + if (meAmongEveryone == null){ + meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); + if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); + } } + Map? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as List>).elementAtOrNull(0); + Map? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as List>).elementAtOrNull(1); + + if (me.tlSeason1.gamesPlayed > 9) { + thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; + thatRankGlickoCutoff = cutoffsGlicko?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; + nextRankCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.rating??25000 : cutoffs?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; + nextRankGlickoCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; + } if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; @@ -254,59 +279,31 @@ class _MainState extends State with TickerProviderStateMixin { if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1); } // Also i need previous Tetra League State for comparison if avaliable - // tableData = [ - // TableRow(children: [ Text("Date & Time"), Text("Tr"), Text("Glicko"), Text("RD"), Text("GP"), Text("GW"), Text("APM"), Text("PPS"), Text("VS"), Text("APP"), Text("VS/APM"), Text("DS/S"), Text("DS/P"), Text("APP+DS/P"), Text("Cheese"), Text("GbE"), Text("wAPP"), Text("Area"), Text("eTR"), Text("±eTR"), Text("Opener"), Text("Plonk"), Text("Inf. DS"), Text("Stride")], - // decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.white)))), - // for (var state in states) TableRow(children: [Text(dateFormat.format(state.tlSeason1.timestamp)), Text(f4.format(state.tlSeason1.rating)), Text(f4.format(state.tlSeason1.glicko)), Text(f4.format(state.tlSeason1.rd)), Text(f0.format(state.tlSeason1.gamesPlayed)), Text(f0.format(state.tlSeason1.gamesWon)), Text(f2.format(state.tlSeason1.apm)), Text(f2.format(state.tlSeason1.pps)), Text(state.tlSeason1.vs != null ? f2.format(state.tlSeason1.vs) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.app) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.vsapm) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.dss) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.dsp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.appdsp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.cheese) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.gbe) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.nyaapp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.area) : "---"), Text(state.tlSeason1.estTr != null ? f4.format(state.tlSeason1.estTr?.esttr) : "---"), Text(state.tlSeason1.esttracc != null ? f4.format(state.tlSeason1.esttracc) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.opener) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.plonk) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.infds) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.stride) : "---")]), - // ]; if (uniqueTL.length >= 2){ compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); - chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.rating)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")), - ]; - chartsDataGamesPlayed = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.rating)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) FlSpot(tl.gamesPlayed.toDouble(), tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) FlSpot(tl.gamesPlayed.toDouble(), tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) FlSpot(tl.gamesPlayed.toDouble(), tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) FlSpot(tl.gamesPlayed.toDouble(), tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) FlSpot(tl.gamesPlayed.toDouble(), tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")), - ]; + 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.rating)], 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")), + ]; }else{ compareWith = null; chartsData = []; @@ -465,6 +462,12 @@ class _MainState extends State with TickerProviderStateMixin { topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon", + thatRankCutoff: thatRankCutoff, + thatRankCutoffGlicko: thatRankGlickoCutoff, + thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, + nextRankCutoff: nextRankCutoff, + nextRankCutoffGlicko: nextRankGlickoCutoff, + nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null, averages: rankAverages, lbPositions: meAmongEveryone ), @@ -474,7 +477,7 @@ class _MainState extends State with TickerProviderStateMixin { child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true,) ), ],), - _History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) ] : [ @@ -485,11 +488,17 @@ class _MainState extends State with TickerProviderStateMixin { topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon", + thatRankCutoff: thatRankCutoff, + thatRankCutoffGlicko: thatRankGlickoCutoff, + thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, + nextRankCutoff: nextRankCutoff, + nextRankCutoffGlicko: nextRankGlickoCutoff, + nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null, averages: rankAverages, lbPositions: meAmongEveryone ), _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched), - _History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), + _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank), _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) @@ -541,7 +550,7 @@ class _MainState extends State with TickerProviderStateMixin { Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), if (subText != null) Padding( padding: const EdgeInsets.only(top: 8.0), - child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18)), + child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), ), ], ) @@ -624,7 +633,7 @@ class _NavDrawerState extends State { leading: const Icon(Icons.home), title: Text(homePlayerNickname), onTap: () { - widget.changePlayer(prefs.getString("player") ?? "dan63047"); // changes player on main view to the one from preferences + widget.changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // changes player on main view to the one from preferences Navigator.of(context).pop(); // and then NavDrawer closes itself. }, ), @@ -657,6 +666,20 @@ class _NavDrawerState extends State { }, ), ), + SliverToBoxAdapter( + child: ListTile( // Rank averages button + leading: const Icon(Icons.bar_chart), + title: Text(t.sprintAndBlitsViewTitle), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SprintAndBlitzView(), + ), + ); + }, + ), + ), const SliverToBoxAdapter(child: Divider()) ]; }, @@ -752,8 +775,7 @@ class _TLRecords extends StatelessWidget { } class _History extends StatelessWidget{ - final List>> chartsData; - final List>> chartsDataGamesPlayed; + final List>> chartsData; final String userID; final Function update; final Function changePlayer; @@ -761,7 +783,7 @@ class _History extends StatelessWidget{ /// Widget, that can show history of some stat of the player on the graph. /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.chartsData, required this.chartsDataGamesPlayed, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); + const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); @override Widget build(BuildContext context) { @@ -776,65 +798,86 @@ class _History extends StatelessWidget{ )); } bool bigScreen = MediaQuery.of(context).size.width > 768; + //List<_HistoryChartSpot> selectedGraph = _gamesPlayedInsteadOfDateAndTime ? chartsDataGamesPlayed[_chartsIndex].value! : chartsData[_chartsIndex].value!; + List<_HistoryChartSpot> selectedGraph = chartsData[_chartsIndex].value!; return SingleChildScrollView( scrollDirection: Axis.vertical, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - primary: true, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 20, - 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) { - _gamesPlayedInsteadOfDateAndTime = value!; - update(); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: chartsData, - value: chartsData[_chartsIndex].value, - onChanged: (value) { - _chartsIndex = chartsData.indexWhere((element) => element.value == value); - update(); - } - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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) { + _gamesPlayedInsteadOfDateAndTime = value!; + update(); + } + ), ], - ), - ], - ), - if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: _gamesPlayedInsteadOfDateAndTime ? chartsDataGamesPlayed[_chartsIndex].value! : chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact()) - else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) - ], - )) - ], - ), - ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: chartsData, + value: chartsData[_chartsIndex].value, + onChanged: (value) { + _chartsIndex = chartsData.indexWhere((element) => element.value == value); + update(); + } + ), + ], + ), + if (selectedGraph.length > 300) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox(value: _smooth, + checkColor: Colors.black, + onChanged: ((value) { + _smooth = value!; + update(); + })), + Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) + ], + ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) + ], + ), + if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: selectedGraph, smooth: _smooth, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact()) + else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text(t.errors.actionSuggestion), + if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) + ], + )) + ], + ), ); } } +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 _HistoryChartThigy extends StatefulWidget{ - final List data; + final List<_HistoryChartSpot> data; + final bool smooth; final String yAxisTitle; final bool bigScreen; final double leftSpace; @@ -844,7 +887,7 @@ class _HistoryChartThigy extends StatefulWidget{ /// Implements graph for the _History widget. Requires [data] which is a list of dots for the graph. [yAxisTitle] used to keep track of changes. /// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets number format /// for left titles - const _HistoryChartThigy({required this.data, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat}); + const _HistoryChartThigy({required this.data, required this.smooth, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat}); @override State<_HistoryChartThigy> createState() => _HistoryChartThigyState(); @@ -853,292 +896,117 @@ class _HistoryChartThigy extends StatefulWidget{ class _HistoryChartThigyState extends State<_HistoryChartThigy> { late String previousAxisTitle; late bool previousGamesPlayedInsteadOfDateAndTime; - late double minX; - late double maxX; - late double minY; - late double actualMinY; - late double maxY; - late double actualMaxY; - late double xScale; - late double yScale; - String headerTooltip = t.pseudoTooltipHeaderInit; - String footerTooltip = t.pseudoTooltipFooterInit; - int hoveredPointId = -1; - double scaleFactor = 5e2; - double dragFactor = 7e2; + late TooltipBehavior _tooltipBehavior; + @override void initState(){ super.initState(); - minX = widget.data.first.x; - maxX = widget.data.last.x; - setMinMaxY(); + _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( + "${f4.format(data.stat)} ${widget.yAxisTitle}", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + ), + ), + Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : _dateFormat.format(data.timestamp)) + ], + ), + ); + } + ); previousAxisTitle = widget.yAxisTitle; previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; - actualMaxY = maxY; - actualMinY = minY; - recalculateScales(); } @override void dispose(){ super.dispose(); - actualMinY = 0; - minY = 0; } - /// Calculates and assignes maximum and minimum values in list of dots - void setMinMaxY(){ - actualMinY = widget.data.reduce((value, element){ - num n = min(value.y, element.y); - if (value.y == n) { - return value; - } else { - return element; - } - }).y; - actualMaxY = widget.data.reduce((value, element){ - num n = max(value.y, element.y); - if (value.y == n) { - return value; - } else { - return element; - } - }).y; - minY = actualMinY; - maxY = actualMaxY; - } - - /// Calculates and assignes scales, which is difference between maximum and minimum visible axis value - void recalculateScales(){ - xScale = maxX - minX; - yScale = maxY - minY; - } - - /// Accepts [dragUpdDet] and changes minX, maxX, minY, maxY based on that - void dragHandler(DragUpdateDetails dragUpdDet){ - setState(() { - // Changing min and max values according to drag delta and considering scales - minX -= (xScale / dragFactor) * dragUpdDet.delta.dx; - maxX -= (xScale / dragFactor) * dragUpdDet.delta.dx; - minY += (yScale / dragFactor) * dragUpdDet.delta.dy; - maxY += (yScale / dragFactor) * dragUpdDet.delta.dy; - - // If values are out of bounds - putting them back - if (minX < widget.data.first.x) { - minX = widget.data.first.x; - maxX = widget.data.first.x + xScale; - } - if (maxX > widget.data.last.x) { - maxX = widget.data.last.x; - minX = maxX - xScale; - } - if(minY < actualMinY){ - minY = actualMinY; - maxY = actualMinY + yScale; - } - if(maxY > actualMaxY){ - maxY = actualMaxY; - minY = actualMaxY - yScale; - } - }); - } - - /// Accepts scale [details] and changes minX, maxX, minY, maxY in a way to change xScale and yScale. - /// [graphKey] required for sizes calculations, as well, as [graphStartX] and [graphEndX]. - /// Not used yet, because GestureDetector works like shit - void scaleHandler(ScaleUpdateDetails details, GlobalKey> graphKey, double graphStartX, double graphEndX){ - RenderBox graphBox = graphKey.currentContext?.findRenderObject() as RenderBox; - - // calculating relative position of scale gesture - Offset graphPosition = graphBox.localToGlobal(Offset.zero); - // 0 - very left position of graph; 1 - very right position of graph - double gesturePosRelativeX = (details.focalPoint.dx - graphStartX) / (graphEndX - graphStartX); - // 0 - very top position of graph; 1 - very bottom position of graph - double gesturePosRelativeY = (details.focalPoint.dy - graphPosition.dy) / (graphBox.size.height - 30); // size - bottom titles height - - double newMinX, newMaxX, newMinY, newMaxY; // calcutating new values based on gesture and considering scales - newMinX = minX - (xScale / scaleFactor) * (details.horizontalScale-1) * gesturePosRelativeX; - newMaxX = maxX + (xScale / scaleFactor) * (details.horizontalScale-1) * (1-gesturePosRelativeX); - newMinY = minY - (yScale / scaleFactor) * (details.horizontalScale-1) * (1-gesturePosRelativeY); - newMaxY = maxY + (yScale / scaleFactor) * (details.horizontalScale-1) * gesturePosRelativeY; - - // cancel changes if minimum is more, than maximun - if ((newMaxX - newMinX).isNegative) return; - if ((newMaxY - newMinY).isNegative) return; - - // apply changes if everything ok + can't go past boundaries - setState(() { - minX = max(newMinX, widget.data.first.x); - maxX = min(newMaxX, widget.data.last.x); - minY = max(newMinY, actualMinY); - maxY = min(newMaxY, actualMaxY); - recalculateScales(); - }); - } - @override Widget build(BuildContext context) { - GlobalKey graphKey = GlobalKey(); if ((previousAxisTitle != widget.yAxisTitle) || (previousGamesPlayedInsteadOfDateAndTime != _gamesPlayedInsteadOfDateAndTime)) { - minX = widget.data.first.x; - maxX = widget.data.last.x; - recalculateScales(); - setMinMaxY(); previousAxisTitle = widget.yAxisTitle; previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; setState((){}); } - double xInterval = widget.bigScreen ? max(1, xScale / 8) : max(1, xScale / 4); // how far away xTitles should be between each other EdgeInsets padding = widget.bigScreen ? const EdgeInsets.fromLTRB(40, 30, 40, 30) : const EdgeInsets.fromLTRB(0, 40, 16, 48); - double graphStartX = padding.left+widget.leftSpace; - double graphEndX = MediaQuery.sizeOf(context).width - padding.right; return SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height - 104, + child: Padding( padding: padding, child: Listener( behavior: HitTestBehavior.translucent, onPointerSignal: (signal) { - if (signal is PointerScrollEvent) { - RenderBox graphBox = graphKey.currentContext?.findRenderObject() as RenderBox; - - // calculating relative position of pointer - Offset graphPosition = graphBox.localToGlobal(Offset.zero); - // 0 - very left position of graph; 1 - very right position of graph - double scrollPosRelativeX = (signal.position.dx - graphStartX) / (graphEndX - graphStartX); - // 0 - very top position of graph; 1 - very bottom position of graph - double scrollPosRelativeY = (signal.position.dy - graphPosition.dy) / (graphBox.size.height - 30); // size - bottom titles height - - double newMinX, newMaxX, newMinY, newMaxY; // calcutating new values based on pointer position and considering scales - newMinX = minX - (xScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeX; - newMaxX = maxX + (xScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeX); - newMinY = minY - (yScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeY); - newMaxY = maxY + (yScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeY; - - // cancel changes if minimum is more, than maximun - if ((newMaxX - newMinX).isNegative) return; - if ((newMaxY - newMinY).isNegative) return; - - // apply changes if everything ok + can't go past boundaries + if (signal is PointerScrollEvent) { setState(() { - minX = max(newMinX, widget.data.first.x); - maxX = min(newMaxX, widget.data.last.x); - minY = max(newMinY, actualMinY); - maxY = min(newMaxY, actualMaxY); - recalculateScales(); _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView }); } }, - child: - GestureDetector( - behavior: HitTestBehavior.translucent, - onDoubleTap: () { - setState(() { - minX = widget.data.first.x; - maxX = widget.data.last.x; - minY = actualMinY; - maxY = actualMaxY; - recalculateScales(); - }); - }, - // TODO: onScaleUpdate:(details) => scaleHandler(details, graphKey, graphStartX, graphEndX), - // TODO: Figure out wtf is going on with gestures - // TODO: Somehow highlight touched spot (handleBuiltInTouches breaks getTooltipItems and getTouchedSpotIndicator) - child: Padding( padding: padding, - child: Stack( - children: [ - LineChart( - key: graphKey, - LineChartData( - lineBarsData: [LineChartBarData(spots: widget.data)], - clipData: const FlClipData.all(), - borderData: FlBorderData(show: false), - gridData: FlGridData(verticalInterval: xInterval), - minX: minX, - maxX: maxX, - minY: minY, - maxY: maxY, - titlesData: FlTitlesData(topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){ - return value != meta.min && value != meta.max ? SideTitleWidget( - axisSide: meta.axisSide, - child: Text(widget.xFormat != null && _gamesPlayedInsteadOfDateAndTime ? widget.xFormat!.format(value.round()) : DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))), - ) : Container(); - })), - leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: widget.leftSpace, getTitlesWidget: (double value, TitleMeta meta){ - return value != meta.min && value != meta.max ? SideTitleWidget( - axisSide: meta.axisSide, - child: Text(widget.yFormat.format(value)), - ) : Container(); - }))), - lineTouchData: LineTouchData( - handleBuiltInTouches: false, - touchCallback:(touchEvent, touchResponse) { - if (touchEvent is FlPanUpdateEvent){ - dragHandler(touchEvent.details); - return; - } - if (touchEvent is FlPointerHoverEvent){ - setState(() { - if (touchResponse?.lineBarSpots?.first == null) { - hoveredPointId = -1; // not hovering over any point - } else { - hoveredPointId = touchResponse!.lineBarSpots!.first.spotIndex; - headerTooltip = "${f4.format(touchResponse.lineBarSpots!.first.y)} ${widget.yAxisTitle}"; - footerTooltip = _gamesPlayedInsteadOfDateAndTime ? "${f0.format(touchResponse.lineBarSpots!.first.x)} games played" : _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(touchResponse.lineBarSpots!.first.x.floor())); - } - }); - } - if (touchEvent is FlPointerExitEvent){ - setState(() {hoveredPointId = -1;}); - } - }, - ) - ) - ), - Padding( - padding: EdgeInsets.only(left: widget.leftSpace), - child: Column( - children: [ - AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24, color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(headerTooltip)), - AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round", color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(footerTooltip)), - ], - ), - ) + child: SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( + rangePadding: ChartRangePadding.additional, + ), + series: [ + if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( + enableTooltip: true, + // splineType: SplineType.cardinal, + // cardinalSplineTension: 0.2, + dataSource: widget.data, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (widget.data.length/175).floor(), + type: TrendlineType.movingAverage, + color: Colors.blue) + ], + ) + else StepLineSeries<_HistoryChartSpot, DateTime>( + enableTooltip: true, + // splineType: SplineType.cardinal, + // cardinalSplineTension: 0.2, + dataSource: widget.data, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (widget.data.length/175).floor(), + type: TrendlineType.movingAverage, + color: Colors.blue) ], ), - ), - ), + ], + ), + ), ) ); } } -// class _HistoryTableThingy extends StatelessWidget{ -// final List tableData; - -// const _HistoryTableThingy(this.tableData); -// // :tf: -// @override -// Widget build(BuildContext context) { -// return LayoutBuilder(builder: (context, constraints){ -// return Table( -// defaultColumnWidth: FixedColumnWidth(75), -// columnWidths: { -// 0: FixedColumnWidth(170), -// 1: FixedColumnWidth(100), -// 2: FixedColumnWidth(90), -// 18: FixedColumnWidth(100), -// 19: FixedColumnWidth(90), -// }, -// children: tableData, -// ); -// }); -// } -// } - class _TwoRecordsThingy extends StatelessWidget { final RecordSingle? sprint; final RecordSingle? blitz; @@ -1228,7 +1096,7 @@ class _TwoRecordsThingy extends StatelessWidget { ), if (sprint != null) FinesseThingy(sprint?.endContext?.finesse, sprint?.endContext?.finessePercentage), if (sprint != null) LineclearsThingy(sprint!.endContext!.clears, sprint!.endContext!.lines, sprint!.endContext!.holds, sprint!.endContext!.tSpins), - if (sprint != null) Text("${sprint!.endContext!.inputs} KP • ${f2.format(sprint!.endContext!.kps)} KpS") + if (sprint != null) Text("${sprint!.endContext!.inputs} KP • ${f2.format(sprint!.endContext!.kps)} KPS") ] ), Column( @@ -1289,7 +1157,7 @@ class _TwoRecordsThingy extends StatelessWidget { ), if (blitz != null) FinesseThingy(blitz?.endContext?.finesse, blitz?.endContext?.finessePercentage), if (blitz != null) LineclearsThingy(blitz!.endContext!.clears, blitz!.endContext!.lines, blitz!.endContext!.holds, blitz!.endContext!.tSpins), - if (blitz != null) Text("${blitz!.endContext!.piecesPlaced} P • ${blitz!.endContext!.inputs} KP • ${f2.format(blitz!.endContext!.kpp)} KpP • ${f2.format(blitz!.endContext!.kps)} KpS") + if (blitz != null) Text("${blitz!.endContext!.piecesPlaced} P • ${blitz!.endContext!.inputs} KP • ${f2.format(blitz!.endContext!.kpp)} KPP • ${f2.format(blitz!.endContext!.kps)} KPS") ], ), ]), @@ -1405,8 +1273,8 @@ class _RecordThingy extends StatelessWidget { ), FinesseThingy(record?.endContext?.finesse, record?.endContext?.finessePercentage), LineclearsThingy(record!.endContext!.clears, record!.endContext!.lines, record!.endContext!.holds, record!.endContext!.tSpins), - if (record!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KpS"), - if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KpP • ${f2.format(record!.endContext!.kps)} KpS") + if (record!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KPS"), + if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KPP • ${f2.format(record!.endContext!.kps)} KPS") ] ), ), diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 19651d5..b55fae0 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -1,6 +1,4 @@ import 'dart:io'; -import 'dart:math'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -8,13 +6,16 @@ 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/views/main_view.dart' show MainView; -import 'package:tetra_stats/utils/text_shadow.dart'; 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),)]; Stats _chartsX = Stats.tr; Stats _chartsY = Stats.apm; +late TooltipBehavior _tooltipBehavior; +late ZoomPanBehavior _zoomPanBehavior; List _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; +List<_MyScatterSpot> _spots = []; Stats _sortBy = Stats.tr; late List they; bool _reversed = false; @@ -48,7 +49,7 @@ class RankState extends State with SingleTickerProviderStateMixin { late double yScale; String headerTooltip = t.pseudoTooltipHeaderInit; String footerTooltip = t.pseudoTooltipFooterInit; - int hoveredPointId = -1; + ValueNotifier hoveredPointId = ValueNotifier(-1); double scaleFactor = 5e2; double dragFactor = 7e2; @@ -56,6 +57,37 @@ class RankState extends State with SingleTickerProviderStateMixin { void initState() { _scrollController = ScrollController(); _tabController = TabController(length: 6, vsync: this); + _zoomPanBehavior = ZoomPanBehavior( + enablePinching: true, + enableSelectionZooming: true, + enableMouseWheelZooming : true, + enablePanning: true, + ); + _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[_chartsX]}\n${_f4.format(data.y)} ${chartsShortTitles[_chartsY]}') + ], + ), + ); + } + ); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => _oldWindowTitle = value); windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}"); @@ -63,56 +95,22 @@ class RankState extends State with SingleTickerProviderStateMixin { super.initState(); previousAxisTitles = _chartsX.toString()+_chartsY.toString(); they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); - recalculateBoundaries(); - resetScale(); + createSpots(); } - void recalculateBoundaries(){ - actualMinX = (widget.rank[1]["entries"] as List).reduce((value, element) { - num n = min(value.getStatByEnum(_chartsX), element.getStatByEnum(_chartsX)); - if (value.getStatByEnum(_chartsX) == n) { - return value; - } else { - return element; - } - }).getStatByEnum(_chartsX).toDouble(); - actualMaxX = (widget.rank[1]["entries"] as List).reduce((value, element) { - num n = max(value.getStatByEnum(_chartsX), element.getStatByEnum(_chartsX)); - if (value.getStatByEnum(_chartsX) == n) { - return value; - } else { - return element; - } - }).getStatByEnum(_chartsX).toDouble(); - actualMinY = (widget.rank[1]["entries"] as List).reduce((value, element) { - num n = min(value.getStatByEnum(_chartsY), element.getStatByEnum(_chartsY)); - if (value.getStatByEnum(_chartsY) == n) { - return value; - } else { - return element; - } - }).getStatByEnum(_chartsY).toDouble(); - actualMaxY = (widget.rank[1]["entries"] as List).reduce((value, element) { - num n = max(value.getStatByEnum(_chartsY), element.getStatByEnum(_chartsY)); - if (value.getStatByEnum(_chartsY) == n) { - return value; - } else { - return element; - } - }).getStatByEnum(_chartsY).toDouble(); - } - - void resetScale(){ - maxX = actualMaxX; - minX = actualMinX; - maxY = actualMaxY; - minY = actualMinY; - recalculateScales(); - } - - void recalculateScales(){ - xScale = maxX - minX; - yScale = maxY - minY; + void createSpots(){ + _spots = [ + for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"]) + if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception + _MyScatterSpot( + entry.getStatByEnum(_chartsX).toDouble(), + entry.getStatByEnum(_chartsY).toDouble(), + entry.userId, + entry.username, + entry.rank, + rankColors[entry.rank]??Colors.white + ) + ]; } @override @@ -123,46 +121,15 @@ class RankState extends State with SingleTickerProviderStateMixin { super.dispose(); } - void dragHandler(DragUpdateDetails dragUpdDet){ - setState(() { - minX -= (xScale / dragFactor) * dragUpdDet.delta.dx; - maxX -= (xScale / dragFactor) * dragUpdDet.delta.dx; - minY += (yScale / dragFactor) * dragUpdDet.delta.dy; - maxY += (yScale / dragFactor) * dragUpdDet.delta.dy; - - if (minX < actualMinX) { - minX = actualMinX; - maxX = actualMinX + xScale; - } - if (maxX > actualMaxX) { - maxX = actualMaxX; - minX = maxX - xScale; - } - if(minY < actualMinY){ - minY = actualMinY; - maxY = actualMinY + yScale; - } - if(maxY > actualMaxY){ - maxY = actualMaxY; - minY = actualMaxY - yScale; - } - }); - } - void _justUpdate() { setState(() {}); } @override Widget build(BuildContext context) { - GlobalKey graphKey = GlobalKey(); bool bigScreen = MediaQuery.of(context).size.width > 768; - EdgeInsets padding = bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48); - double graphStartX = padding.left; - double graphEndX = MediaQuery.sizeOf(context).width - padding.right; if (previousAxisTitles != _chartsX.toString()+_chartsY.toString()){ - recalculateBoundaries(); - resetScale(); + createSpots(); previousAxisTitles = _chartsX.toString()+_chartsY.toString(); } final t = Translations.of(context); @@ -230,7 +197,8 @@ class RankState extends State with SingleTickerProviderStateMixin { Wrap( direction: Axis.horizontal, alignment: WrapAlignment.center, - spacing: 25, + crossAxisAlignment: WrapCrossAlignment.end, + spacing: 20, children: [ Column( children: [ @@ -271,113 +239,39 @@ class RankState extends State with SingleTickerProviderStateMixin { ), ], ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) ], ), if (widget.rank[1]["entries"].length > 1) SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height - 104, - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerSignal: (signal) { - if (signal is PointerScrollEvent) { - RenderBox graphBox = graphKey.currentContext?.findRenderObject() as RenderBox; - Offset graphPosition = graphBox.localToGlobal(Offset.zero); - double scrollPosRelativeX = (signal.position.dx - graphStartX) / (graphEndX - graphStartX); - double scrollPosRelativeY = (signal.position.dy - graphPosition.dy) / (graphBox.size.height - 30); // size - bottom titles height - double newMinX, newMaxX, newMinY, newMaxY; - newMinX = minX - (xScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeX; - newMaxX = maxX + (xScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeX); - newMinY = minY - (yScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeY); - newMaxY = maxY + (yScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeY; - if ((newMaxX - newMinX).isNegative) return; - if ((newMaxY - newMinY).isNegative) return; - setState(() { - minX = max(newMinX, actualMinX); - maxX = min(newMaxX, actualMaxX); - minY = max(newMinY, actualMinY); - maxY = min(newMaxY, actualMaxY); - recalculateScales(); - _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); - }); - }}, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onDoubleTap: () { + child: Padding( + padding: bigScreen ? const EdgeInsets.fromLTRB(40, 10, 40, 20) : const EdgeInsets.fromLTRB(0, 10, 16, 20), + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerSignal: (signal) { + if (signal is PointerScrollEvent) { setState(() { - minX = actualMinX; - maxX = actualMaxX; - minY = actualMinY; - maxY = actualMaxY; - recalculateScales(); + _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView }); - }, - // TODO: Figure out wtf is going on with gestures - child: Padding( - padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48), - child: Stack( - children: [ - ScatterChart( - key: graphKey, - ScatterChartData( - minX: minX, - maxX: maxX, - minY: minY, - maxY: maxY, - clipData: const FlClipData.all(), - scatterSpots: [ - for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"]) - if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception - _MyScatterSpot( - entry.getStatByEnum(_chartsX).toDouble(), - entry.getStatByEnum(_chartsY).toDouble(), - entry.userId, - entry.username, - dotPainter: FlDotCirclePainter(color: rankColors[entry.rank]??Colors.white, radius: 3)) - ], - scatterTouchData: ScatterTouchData( - handleBuiltInTouches: false, - touchCallback:(touchEvent, touchResponse) { - if (touchEvent is FlPanUpdateEvent){ - dragHandler(touchEvent.details); - return; - } - if (touchEvent is FlPointerHoverEvent){ - setState(() { - if (touchResponse?.touchedSpot == null) { - hoveredPointId = -1; - } else { - hoveredPointId = touchResponse!.touchedSpot!.spotIndex; - _MyScatterSpot castedPoint = touchResponse.touchedSpot!.spot as _MyScatterSpot; - headerTooltip = castedPoint.nickname; - footerTooltip = "${_f4.format(castedPoint.x)} ${chartsShortTitles[_chartsX]}; ${_f4.format(castedPoint.y)} ${chartsShortTitles[_chartsY]}"; - } - }); - } - if (touchEvent is FlPointerExitEvent){ - setState(() {hoveredPointId = -1;}); - } - if (touchEvent is FlTapUpEvent && touchResponse?.touchedSpot?.spot != null){ - _MyScatterSpot spot = touchResponse!.touchedSpot!.spot as _MyScatterSpot; - Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: spot.nickname), maintainState: false)); - } - }, - ), - ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional - ), - Padding( - padding: EdgeInsets.fromLTRB(graphStartX+8, padding.top/2+8, 0, 0), - child: Column( - children: [ - AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24, color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(headerTooltip)), - AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round", color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(footerTooltip)), - ], - ), - ) - ], - ), + } + }, + child: SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + //primaryXAxis: CategoryAxis(), + series: [ + ScatterSeries( + enableTooltip: true, + dataSource: _spots, + animationDuration: 0, + pointColorMapper: (data, _) => data.color, + xValueMapper: (data, _) => data.x, + yValueMapper: (data, _) => data.y, + onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: _spots[point.pointIndex!].nickname), maintainState: false)), + ) + ], ), ), )) @@ -424,7 +318,9 @@ class RankState extends State with SingleTickerProviderStateMixin { checkColor: Colors.black, onChanged: ((value) { _reversed = value!; - setState(() {}); + setState(() { + they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); + }); }), ), ), @@ -441,7 +337,9 @@ class RankState extends State with SingleTickerProviderStateMixin { value: _country, onChanged: ((value) { _country = value; - setState(() {}); + setState(() { + they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); + }); }), ), ], @@ -456,7 +354,9 @@ class RankState extends State with SingleTickerProviderStateMixin { bool bigScreen = MediaQuery.of(context).size.width > 768; return ListTile( title: Text(they[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - subtitle: Text(_sortBy == Stats.tr ? "${_f2.format(they[index].apm)} APM, ${_f2.format(they[index].pps)} PPS, ${_f2.format(they[index].vs)} VS, ${_f2.format(they[index].nerdStats.app)} APP, ${_f2.format(they[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(they[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}"), + subtitle: Text( + _sortBy == Stats.tr ? "${_f2.format(they[index].apm)} APM, ${_f2.format(they[index].pps)} PPS, ${_f2.format(they[index].vs)} VS, ${_f2.format(they[index].nerdStats.app)} APP, ${_f2.format(they[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(they[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", + style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -635,10 +535,12 @@ class _ListEntry extends StatelessWidget { } } -class _MyScatterSpot extends ScatterSpot { +class _MyScatterSpot{ + num x; + num y; String id; String nickname; - //Color color; - //FlDotPainter painter = FlDotCirclePainter(color: color, radius: 2); - _MyScatterSpot(super.x, super.y, this.id, this.nickname, {super.dotPainter}); + String rank; + Color color; + _MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); } diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index b82d8f3..e45e512 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -55,7 +55,7 @@ class RanksAverages extends State { leading: Image.asset("res/tetrio_tl_alpha_ranks/${keys[index]}.png", height: 48), title: Text(t.players(n: averages[keys[index]]?[1]["players"]), style: const TextStyle(fontFamily: "Eurostile Round Extended")), subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM", - style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), + style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), onTap: (){ if (averages[keys[index]]?[1]["players"] > 0) { diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 979293e..fae31a5 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -48,11 +48,7 @@ class SettingsState extends State { Future _getPreferences() async { prefs = await SharedPreferences.getInstance(); - if (prefs.getBool("showPositions") != null) { - showPositions = prefs.getBool("showPositions")!; - } else { - showPositions = false; - } + showPositions = prefs.getBool("showPositions") ?? false; _setDefaultNickname(prefs.getString("player")); } @@ -76,7 +72,7 @@ class SettingsState extends State { Future _removePlayer() async { await prefs.remove('player'); - await _setDefaultNickname("dan63047"); + await _setDefaultNickname("6098518e3d5155e6ec429cdc"); } @override @@ -97,7 +93,7 @@ class SettingsState extends State { children: [ ListTile( title: Text(t.exportDB), - subtitle: Text(t.exportDBDescription), + subtitle: Text(t.exportDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), onTap: () { if (kIsWeb){ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); @@ -151,7 +147,7 @@ class SettingsState extends State { ), ListTile( title: Text(t.importDB), - subtitle: Text(t.importDBDescription), + subtitle: Text(t.importDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), onTap: () { if (kIsWeb){ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); @@ -262,13 +258,13 @@ class SettingsState extends State { ), ), ListTile(title: Text(t.customization), - subtitle: Text(t.customizationDescription), + subtitle: Text(t.customizationDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: const Icon(Icons.arrow_right), onTap: () { context.go("/customization"); },), ListTile(title: Text(t.lbStats), - subtitle: Text(t.lbStatsDescription), + subtitle: Text(t.lbStatsDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: Switch(value: showPositions, onChanged: (bool value){ prefs.setBool("showPositions", value); setState(() { @@ -280,7 +276,7 @@ class SettingsState extends State { onTap: (){ launchInBrowser(Uri.https("github.com", "dan63047/TetraStats")); }, - title: Text(t.aboutApp), + title: Text(t.aboutApp, style: const TextStyle(fontWeight: FontWeight.w500),), subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)), trailing: const Icon(Icons.arrow_right) ), diff --git a/lib/views/sprint_and_blitz_averages.dart b/lib/views/sprint_and_blitz_averages.dart new file mode 100644 index 0000000..f0f99ec --- /dev/null +++ b/lib/views/sprint_and_blitz_averages.dart @@ -0,0 +1,102 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/main_view.dart'; +import 'package:window_manager/window_manager.dart'; + +late String oldWindowTitle; +final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode); + +class SprintAndBlitzView extends StatefulWidget { + const SprintAndBlitzView({super.key}); + + @override + State createState() => SprintAndBlitzState(); +} + +class SprintAndBlitzState extends State { + + @override + void initState() { + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ + windowManager.getTitle().then((value) => oldWindowTitle = value); + windowManager.setTitle("Tetra Stats: ${t.settings}"); + } + super.initState(); + } + + @override + void 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(t.sprintAndBlitsViewTitle), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + constraints: const BoxConstraints(maxWidth: 600), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all(color: Colors.grey.shade900), + columnWidths: {0: const FixedColumnWidth(48)}, + children: [ + TableRow( + children: [ + Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(t.sprint, textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(t.blitz, textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + ] + ), + for (MapEntry sprintEntry in sprintAverages.entries) TableRow( + decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[sprintEntry.key]!.withAlpha(100), rankColors[sprintEntry.key]!.withAlpha(200)])), + 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/${sprintEntry.key}.png", height: 48)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(get40lTime(sprintEntry.value.inMicroseconds), 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(NumberFormat.decimalPattern().format(blitzAverages[sprintEntry.key]), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + ] + ) + ], + ), + Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 5, 26)))) + ], + ), + ), + ), + ], + )), + ); + } +} diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index 7fb2422..ab802cc 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -75,6 +75,7 @@ class TLLeaderboardState extends State { case ConnectionState.done: final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers!.length)}"); + bool bigScreen = MediaQuery.of(context).size.width > 768; return NestedScrollView( headerSliverBuilder: (context, value) { String howManyPlayers(int numberOfPlayers) => Intl.plural( @@ -170,18 +171,26 @@ class TLLeaderboardState extends State { }, body: ListView.builder( itemCount: allPlayers!.length, + prototypeItem: ListTile( + leading: Text("0", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)), + title: Text("ehhh...", style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), + trailing: Container(height: bigScreen ? 48 : 36, width: 1,), + subtitle: Text("eh..."), + ), itemBuilder: (context, index) { - bool bigScreen = MediaQuery.of(context).size.width > 768; return ListTile( - leading: Text((index+1).toString(), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : null), - title: Text(allPlayers[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + leading: Text( + (index+1).toString(), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9) + ), + title: Text(allPlayers[index].username, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), subtitle: Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", - style: TextStyle(fontFamily: "Eurostile Round Condensed", color: _sortBy == Stats.tr ? Colors.grey : null)), + style: TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: bigScreen ? null : 12, color: _sortBy == Stats.tr ? Colors.grey : null)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - Text("${f2.format(allPlayers[index].rating)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), - Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 16), + Text("${f2.format(allPlayers[index].rating)} TR", style: TextStyle(fontSize: bigScreen ? 28 : 22)), + Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36), ], ), onTap: () { diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 202bfb0..9dc15b9 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -1,7 +1,6 @@ // ignore_for_file: use_build_context_synchronously import 'dart:io'; -import 'dart:math'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy; @@ -62,422 +61,426 @@ class TlMatchResultState extends State { super.dispose(); } - Widget buildComparison(bool bigScreen, bool showMobileSelector){ - return FutureBuilder(future: replayData, builder: (context, snapshot){ - late Duration time; - late String readableTime; - late String reason; - timeWeightedStatsAvaliable = true; - if (snapshot.connectionState != ConnectionState.done) return const LinearProgressIndicator(); - if (!snapshot.hasError){ - if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch))); - greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); - redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); - if (roundSelector.isNegative){ - time = framesToTime(snapshot.data!.totalLength); - readableTime = "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}"; + Widget buildComparison(double width, bool showMobileSelector){ + bool bigScreen = width >= 768; + return SizedBox( + width: width, + child: FutureBuilder(future: replayData, builder: (context, snapshot){ + late Duration time; + late String readableTime; + late String reason; + timeWeightedStatsAvaliable = true; + if (snapshot.connectionState != ConnectionState.done) return const LinearProgressIndicator(); + if (!snapshot.hasError){ + if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch))); + greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); + redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); + if (roundSelector.isNegative){ + time = framesToTime(snapshot.data!.totalLength); + readableTime = "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}"; + }else{ + time = framesToTime(snapshot.data!.roundLengths[roundSelector]); + readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}"; + } }else{ - time = framesToTime(snapshot.data!.roundLengths[roundSelector]); - readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}"; + switch (snapshot.error.runtimeType){ + case ReplayNotAvalable: + reason = t.matchIsTooOld; + break; + case SzyNotFound: + reason = t.matchIsTooOld; + break; + case SzyForbidden: + reason = t.errors.replayRejected; + break; + case SzyTooManyRequests: + reason = t.errors.tooManyRequests; + break; + default: + reason = snapshot.error.toString(); + break; + } } - }else{ - switch (snapshot.error.runtimeType){ - case ReplayNotAvalable: - reason = t.matchIsTooOld; - break; - case SzyNotFound: - reason = t.matchIsTooOld; - break; - case SzyForbidden: - reason = t.errors.replayRejected; - break; - case SzyTooManyRequests: - reason = t.errors.tooManyRequests; - break; - default: - reason = snapshot.error.toString(); - break; - } - } - return NestedScrollView( - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], - )), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Column(children: [ - Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: bigScreen ? const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28) : const TextStyle()), - Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 42)) - ]), + return NestedScrollView( + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], + )), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Column(children: [ + Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: bigScreen ? const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28) : const TextStyle()), + Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 42)) + ]), + ), ), ), - ), - const Padding( - padding: EdgeInsets.only(top: 16), - child: Text("VS"), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.red, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], - )), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Column(children: [ - Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: bigScreen ? const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28) : const TextStyle()), - Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 42)) - ]), + const Padding( + padding: EdgeInsets.only(top: 16), + child: Text("VS"), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], + )), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Column(children: [ + Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: bigScreen ? const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28) : const TextStyle()), + Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 42)) + ]), + ), ), ), - ), - ], + ], + ), ), ), - ), - if (showMobileSelector) SliverToBoxAdapter( - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.statsFor}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) { - roundSelector = value; - setState(() {}); - }),), - ], + if (showMobileSelector) SliverToBoxAdapter( + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("${t.statsFor}: ", + style: const TextStyle(color: Colors.white, fontSize: 25)), + DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) { + roundSelector = value; + setState(() {}); + }),), + ], + ), ), ), - ), - if (widget.record.ownId == widget.record.replayId && showMobileSelector) SliverToBoxAdapter( - child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), - ), - if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(snapshot.hasError ? reason : readableTime, textAlign: TextAlign.center))), - const SliverToBoxAdapter( - child: Divider(), - ) - ]; - }, - body: ListView( - children: [ - Column( - children: [ - CompareThingy( - label: "APM", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : - roundSelector == -1 ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps: - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "VS", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], - fractionDigits: 2, - higherIsBetter: true, - ), - if (snapshot.hasData) Column(children: [ - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, - label: "Inputs", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, - label: "Pieces Placed", higherIsBetter: true), - CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kpp : - roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kpp : - roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, - label: "KpP", higherIsBetter: false, fractionDigits: 2,), - CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kps : - roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kps : - roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, - label: "KpS", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared, - label: "Lines Cleared", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score, - label: "Score", higherIsBetter: true), - CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].spp : - roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].spp : - roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, - label: "SpP", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100, - label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike, - label: "Best Spike", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo, - label: "Best Combo", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB, - label: "Best BtB", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent, - label: "Sent", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived, - label: "Recived", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack, - label: "Attack", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared, - label: "Cleared", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears, - label: "PC", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins, - label: "T-spins", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads, - label: "Quads", higherIsBetter: true), - ],), - ], - ), - const Divider(), - Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.nerdStats, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "APP", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.app : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.app : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.vsapm : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.vsapm : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dss : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dss : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.appdsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.appdsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.cheese : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.cheese : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, - fractionDigits: 2, - higherIsBetter: false, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.gbe : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.gbe : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.nyaapp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.nyaapp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.area : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.area : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].estTr.esttr : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].estTr.esttr : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.opener : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.opener : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.plonk : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.plonk : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.stride : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.stride : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.infds : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.infds : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs( - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector] - ) - ], - ), - if (widget.record.ownId != widget.record.replayId) const Divider(), - if (widget.record.ownId != widget.record.replayId) Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Handling", - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, - label: "DAS", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, - label: "ARR", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, - label: "SDF", prefix: "x", - higherIsBetter: true), - CompareBoolThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, - label: "Safe HD", - trueIsBetter: true) - ], - ) - ], + if (widget.record.ownId == widget.record.replayId && showMobileSelector) SliverToBoxAdapter( + child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), + ), + if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(snapshot.hasError ? reason : readableTime, textAlign: TextAlign.center))), + const SliverToBoxAdapter( + child: Divider(), ) - ); - }); + ]; + }, + body: ListView( + children: [ + Column( + children: [ + CompareThingy( + label: "APM", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : + roundSelector == -1 ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "PPS", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps: + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "VS", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], + fractionDigits: 2, + higherIsBetter: true, + ), + if (snapshot.hasData) Column(children: [ + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, + label: "Inputs", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, + label: "Pieces Placed", higherIsBetter: true), + CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kpp : + roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kpp : + roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, + label: "KPP", higherIsBetter: false, fractionDigits: 2,), + CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kps : + roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kps : + roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, + label: "KPS", higherIsBetter: true, fractionDigits: 2,), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared, + label: "Lines Cleared", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score, + label: "Score", higherIsBetter: true), + CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].spp : + roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].spp : + roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, + label: "SPP", higherIsBetter: true, fractionDigits: 2,), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100, + label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike, + label: "Best Spike", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo, + label: "Best Combo", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB, + label: "Best BtB", higherIsBetter: true), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent, + label: "Sent", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived, + label: "Recived", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack, + label: "Attack", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared, + label: "Cleared", higherIsBetter: true), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears, + label: "PC", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins, + label: "T-spins", higherIsBetter: true), + CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads, + redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads, + label: "Quads", higherIsBetter: true), + ],), + ], + ), + const Divider(), + Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.nerdStats, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "APP", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.app : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.app : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.vsapm : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.vsapm : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dss : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dss : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.appdsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.appdsp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.cheese : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.cheese : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, + fractionDigits: 2, + higherIsBetter: false, + ), + CompareThingy( + label: "Gb Eff.", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.gbe : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.gbe : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "wAPP", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.nyaapp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.nyaapp : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.area : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.area : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.estOfTRShort, + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].estTr.esttr : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].estTr.esttr : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Opener", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.opener : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.opener : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Plonk", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.plonk : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.plonk : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Stride", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.stride : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.stride : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Inf. DS", + greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.infds : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds, + redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.infds : + roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds, + fractionDigits: 3, + higherIsBetter: true, + ), + VsGraphs( + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector], + (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector] + ) + ], + ), + if (widget.record.ownId != widget.record.replayId) const Divider(), + if (widget.record.ownId != widget.record.replayId) Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("Handling", + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, + label: "DAS", fractionDigits: 1, postfix: "F", + higherIsBetter: false), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, + label: "ARR", fractionDigits: 1, postfix: "F", + higherIsBetter: false), + CompareThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, + label: "SDF", prefix: "x", + higherIsBetter: true), + CompareBoolThingy( + greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, + redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, + label: "Safe HD", + trueIsBetter: true) + ], + ) + ], + ) + ); + }), + ); } Widget buildRoundSelector(double width){ @@ -684,21 +687,17 @@ class TlMatchResultState extends State { return Center( child: Container( constraints: const BoxConstraints(maxWidth: 768), - child: buildComparison(viewportWidth > 768, true) + child: buildComparison(viewportWidth, true) ), ); } else { + double comparisonWidth = viewportWidth - 450 - 16; + comparisonWidth = comparisonWidth > 768 ? 768 : comparisonWidth; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( - width: 768, - child: buildComparison(true, false) - ), - Container( - constraints: const BoxConstraints(maxWidth: 768), - child: buildRoundSelector(max(viewportWidth-768-16, 200)), - ) + buildComparison(comparisonWidth, false), + buildRoundSelector(450) ], ); } diff --git a/lib/widgets/finesse_thingy.dart b/lib/widgets/finesse_thingy.dart index e913572..bb71dc0 100644 --- a/lib/widgets/finesse_thingy.dart +++ b/lib/widgets/finesse_thingy.dart @@ -21,16 +21,17 @@ class FinesseThingy extends StatelessWidget{ return Stack( alignment: AlignmentDirectional.bottomStart, children: [ - Text("f", style: TextStyle( + const Text("f", style: TextStyle( fontStyle: FontStyle.italic, fontSize: 65, height: 1.2, )), - Positioned(child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended")), left: 25, top: 20), + const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), Positioned( + right: 0, top: 20, child: Text("${finesse != null ? finesse!.faults : "---"}F", style: TextStyle( color: getFinesseColor() - )), right: 0, top: 20), + ))), Padding( padding: const EdgeInsets.only(left: 10.0), child: Text("${finesse != null ? f2.format(finessePercentage! * 100) : "---.--"}%", style: TextStyle( diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart index a255908..2672b75 100644 --- a/lib/widgets/gauget_num.dart +++ b/lib/widgets/gauget_num.dart @@ -4,7 +4,6 @@ import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/widgets/tl_thingy.dart'; class GaugetNum extends StatelessWidget { final num playerStat; @@ -98,7 +97,7 @@ class GaugetNum extends StatelessWidget { oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent : oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent ),), - if ((oldTl != null && oldTl!.gamesPlayed > 0) && pos != null) const TextSpan(text: " • "), + if (oldPlayerStat != null && pos != null) const TextSpan(text: " • "), if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}", style: TextStyle(color: getColorOfRank(pos!.position))) ] ), diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index caf65cb..f8dd910 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; 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'; class Graphs extends StatelessWidget{ @@ -20,6 +21,10 @@ class Graphs extends StatelessWidget{ @override Widget build(BuildContext context) { + double attack = apm / 60 * 0.4; + double speed = pps / 3.75; + double defense = nerdStats.dss * 1.15; + double cheese = nerdStats.cheese / 110; return Wrap( direction: Axis.horizontal, alignment: WrapAlignment.center, @@ -27,7 +32,7 @@ class Graphs extends StatelessWidget{ crossAxisAlignment: WrapCrossAlignment.start, clipBehavior: Clip.hardEdge, children: [ - Padding( + if (true) Padding( // vs graph padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), child: SizedBox( height: 310, @@ -86,7 +91,7 @@ class Graphs extends StatelessWidget{ borderColor: Colors.transparent, dataEntries: [ const RadarEntry(value: 0), - const RadarEntry(value: 0), + const RadarEntry(value: 180), const RadarEntry(value: 0), const RadarEntry(value: 0), const RadarEntry(value: 0), @@ -104,7 +109,7 @@ class Graphs extends StatelessWidget{ ), ), ), - Padding( + Padding( // psq graph padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), child: SizedBox( height: 310, @@ -126,7 +131,7 @@ class Graphs extends StatelessWidget{ case 1: return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); case 2: - return RadarChartTitle(text: 'Inf Ds\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: 'Inf DS\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); case 3: return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); default: @@ -147,19 +152,9 @@ class Graphs extends StatelessWidget{ borderColor: Colors.transparent, dataEntries: [ const RadarEntry(value: 0), + const RadarEntry(value: 1), const RadarEntry(value: 0), const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 1), - const RadarEntry(value: 1), - const RadarEntry(value: 1), - const RadarEntry(value: 1), ], ) ], @@ -169,6 +164,59 @@ class Graphs extends StatelessWidget{ ), ), ), + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: RadarChart( + 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}\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: '${t.graphs.speed}\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: '${t.graphs.defense}\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: '${t.graphs.cheese}\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + dataEntries: [ + RadarEntry(value: attack), + RadarEntry(value: speed), + RadarEntry(value: defense), + RadarEntry(value: cheese), + ], + ), + 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), + ], + ) + ], + ) + ) + ) + ) ], ); } diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart new file mode 100644 index 0000000..9359629 --- /dev/null +++ b/lib/widgets/tl_progress_bar.dart @@ -0,0 +1,102 @@ +import 'dart:math'; + +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/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; + +class TLProgress extends StatelessWidget{ + final TetraLeagueAlpha tlData; + final String? nextRank; + final String? previousRank; + final double? nextRankTRcutoff; + final double? previousRankTRcutoff; + final double? nextRankGlickoCutoff; + final double? previousGlickoCutoff; + final double? nextRankTRcutoffTarget; + final double? previousRankTRcutoffTarget; + + const TLProgress({super.key, required this.tlData, this.nextRank, this.previousRank, this.nextRankTRcutoff, this.previousRankTRcutoff, this.nextRankGlickoCutoff, this.previousGlickoCutoff, this.nextRankTRcutoffTarget, this.previousRankTRcutoffTarget}); + + double getBarPosition(){ + return min(max(0, 1 - (tlData.standing - tlData.nextAt)/(tlData.prevAt - tlData.nextAt)), 1); + } + + double? getBarTR(double tr){ + return min(max(0, (tr - previousRankTRcutoff!)/(nextRankTRcutoff! - previousRankTRcutoff!)), 1); + } + + @override + Widget build(BuildContext context) { + if (nextRank == null && previousRank == null && nextRankTRcutoff == null && previousRankTRcutoff == null && nextRankGlickoCutoff == null && previousGlickoCutoff == null && nextRankTRcutoffTarget == null && previousRankTRcutoffTarget == null) return Container(); + final glickoForWin = rate(tlData.glicko!, tlData.rd!, 0.06, [[tlData.glicko!, tlData.rd!, 1]], {})[0]-tlData.glicko!; + return Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 48, + child: Stack( + alignment: AlignmentDirectional.bottomCenter, + fit: StackFit.expand, + children: [ + Positioned(left: 0, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + children: [ + if (tlData.prevAt > 0) TextSpan(text: "№ ${f0.format(tlData.prevAt)}"), + if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"), + if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"), + if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null)) + ] + ) + ), + ), + Positioned(right: 0, + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + children: [ + if (tlData.nextAt > 0) TextSpan(text: "№ ${f0.format(tlData.nextAt)}"), + if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), + if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), + if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) + ] + ) + ), + ) + ],), + ), + SfLinearGauge( + minimum: 0, + maximum: 1, + interval: 1, + ranges: [ + if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.rating)!, color: Colors.cyanAccent, position: LinearElementPosition.cross) + else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Colors.cyanAccent, 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) + ], + markerPointers: [ + LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : 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.rating)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) + ], + isMirrored: true, + showTicks: true, + showLabels: false + ) + ] + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index b5bb240..6900061 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -9,14 +9,11 @@ import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; +import 'package:tetra_stats/widgets/tl_progress_bar.dart'; var fDiff = NumberFormat("+#,###.###;-#,###.###"); var intFDiff = NumberFormat("+#,###;-#,###"); final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); -late RangeValues _currentRangeValues; -TetraLeagueAlpha? oldTl; -late TetraLeagueAlpha currentTl; -late List sortedStates; class TLThingy extends StatefulWidget { final TetraLeagueAlpha tl; @@ -29,10 +26,12 @@ class TLThingy extends StatefulWidget { final PlayerLeaderboardPosition? lbPositions; final TetraLeagueAlpha? averages; final double? thatRankCutoff; + final double? thatRankCutoffGlicko; final double? thatRankTarget; final double? nextRankCutoff; + final double? nextRankCutoffGlicko; final double? nextRankTarget; - const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff = 25000, this.thatRankCutoff = 0, this.nextRankTarget = 25000, this.thatRankTarget = 0}); + const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget}); @override State createState() => _TLThingyState(); @@ -40,17 +39,17 @@ class TLThingy extends StatefulWidget { class _TLThingyState extends State { late bool oskKagariGimmick; + late TetraLeagueAlpha? oldTl; + late TetraLeagueAlpha currentTl; + late RangeValues _currentRangeValues; + late List sortedStates; @override void initState() { _currentRangeValues = const RangeValues(0, 1); sortedStates = widget.states.reversed.toList(); oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; - try{ - oldTl = sortedStates[1].tlSeason1; - }on RangeError{ - oldTl = null; - } + oldTl = sortedStates.elementAtOrNull(1)?.tlSeason1; currentTl = widget.tl; super.initState(); } @@ -139,20 +138,16 @@ class _TLThingyState extends State { ), ], ), - if (currentTl.gamesPlayed >= 10 && currentTl.rd! < 100 && currentTl.nextAt >=0 && currentTl.prevAt >= 0) Padding( - padding: const EdgeInsets.all(8.0), - child: SfLinearGauge( - minimum: currentTl.nextAt.toDouble(), - maximum: currentTl.prevAt.toDouble(), - interval: currentTl.prevAt.toDouble() - currentTl.nextAt.toDouble(), - ranges: [LinearGaugeRange(startValue: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() : currentTl.prevAt.toDouble(), endValue: currentTl.prevAt.toDouble(), color: Colors.cyanAccent,)], - markerPointers: [LinearShapePointer(value: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() : currentTl.prevAt.toDouble(), position: LinearElementPosition.inside, shapeType: LinearShapePointerType.triangle, color: Colors.white, height: 20), - LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() : currentTl.prevAt.toDouble(), child: Text(NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(currentTl.standing)))], - isAxisInversed: true, - isMirrored: true, - showTicks: true, - showLabels: true - ), + if (currentTl.gamesPlayed > 9) TLProgress( + tlData: currentTl, + previousRankTRcutoff: widget.thatRankCutoff, + previousGlickoCutoff: widget.thatRankCutoffGlicko, + previousRank: widget.tl.prevRank, + previousRankTRcutoffTarget: widget.thatRankTarget, + nextRankTRcutoff: widget.nextRankCutoff, + nextRankGlickoCutoff: widget.nextRankCutoffGlicko, + nextRankTRcutoffTarget: widget.nextRankTarget, + nextRank: widget.tl.nextRank ), if (currentTl.gamesPlayed < 10) Text(t.gamesUntilRanked(left: 10 - currentTl.gamesPlayed), @@ -325,7 +320,7 @@ class _TLThingyState extends State { ),), if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.estTr!.position))), - if (widget.lbPositions?.estTr != null) const TextSpan(text: " • "), + if (widget.lbPositions?.estTr != null || oldTl?.estTr?.esttr != null) const TextSpan(text: " • "), TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") ] ), diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index a900051..c99a53e 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; @@ -52,7 +53,7 @@ class UserThingy extends StatelessWidget { alignment: Alignment.topCenter, children: [ if (player.bannerRevision != null) - Image.network("https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", + 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}", fit: BoxFit.cover, height: bannerHeight, errorBuilder: (context, error, stackTrace) { @@ -90,7 +91,7 @@ class UserThingy extends StatelessWidget { child: player.role == "banned" ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) : player.avatarRevision != null - ? Image.network("https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + ? 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}", // TODO: osk banner can cause memory leak fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace); @@ -410,7 +411,7 @@ class UserThingy extends StatelessWidget { errorBuilder: (context, error, stackTrace) { developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace); return Image.network( - "https://tetr.io/res/badges/${badge.badgeId}.png", + 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) { diff --git a/lib/widgets/vs_graphs.dart b/lib/widgets/vs_graphs.dart index d40e809..2ac5eea 100644 --- a/lib/widgets/vs_graphs.dart +++ b/lib/widgets/vs_graphs.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; class VsGraphs extends StatelessWidget{ final double greenAPM; @@ -205,6 +206,71 @@ class VsGraphs extends StatelessWidget{ ), ), ), + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: RadarChart( + 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: [ + RadarDataSet( + fillColor: const Color.fromARGB(115, 76, 175, 79), + borderColor: Colors.green, + dataEntries: [ + RadarEntry(value: greenAPM / 60 * 0.4), + RadarEntry(value: greenPPS / 3.75), + RadarEntry(value: greenNerdStats.dss * 1.15), + RadarEntry(value: greenNerdStats.cheese / 110), + ], + ), + RadarDataSet( + fillColor: const Color.fromARGB(115, 244, 67, 54), + borderColor: Colors.red, + dataEntries: [ + RadarEntry(value: redAPM / 60 * 0.4), + RadarEntry(value: redPPS / 3.75), + RadarEntry(value: redNerdStats.dss * 1.15), + RadarEntry(value: redNerdStats.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), + ], + ) + ], + ) + ) + ) + ) ], ); } diff --git a/pubspec.lock b/pubspec.lock index ff532e5..b4e8059 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -866,6 +866,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + syncfusion_flutter_charts: + dependency: "direct main" + description: + name: syncfusion_flutter_charts + sha256: ab73109c586f5ec2b01adc2672026a1fb3f93b2b5f6061ba8d7126c119061002 + url: "https://pub.dev" + source: hosted + version: "24.2.9" syncfusion_flutter_core: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index acb5c88..a0bbf67 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.5.0+16 +version: 1.5.3+19 environment: sdk: '>=3.0.0' @@ -43,6 +43,7 @@ dependencies: flutter_markdown: ^0.6.18 flutter_colorpicker: ^1.0.3 go_router: ^13.0.0 + syncfusion_flutter_charts: ^24.2.9 dev_dependencies: flutter_test: diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index ba3cb2b..b31172c 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -81,7 +81,12 @@ "verdictGeneral": "$n $verdict than $rank rank average", "verdictBetter": "better", "verdictWorse": "worse", + "smooth": "Smooth", "gamesUntilRanked": "${left} games until being ranked", + "numOfVictories": "~${wins} victories", + "promotionOnNextWin": "Promotion on next win", + "numOfdefeats": "~${losses} defeats", + "demotionOnNextLoss": "Demotion on next loss", "nerdStats": "Nerd Stats", "playersYouTrack": "Players you track", "formula": "Formula", @@ -163,6 +168,9 @@ "calc": "Calc", "calcViewNoValues": "Enter values to calculate the stats", "rankAveragesViewTitle": "Ranks cutoff and average stats", + "sprintAndBlitsViewTitle": "40 lines and Blitz averages", + "sprintAndBlitsRelevance": "Relevance: ${date}", + "rank": "Rank", "averages": "Averages", "lbViewZeroEntrys": "Empty list", "lbViewOneEntry": "There is only one player", @@ -200,6 +208,12 @@ "currentAxis": "$axis 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 Level", "xpProgress": "Progress to next level", @@ -250,7 +264,7 @@ "nyaappDescription": "(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.", "area": "Area", "areaDescription": "How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections", - "estOfTR": "Est. of TR", + "estOfTR": "Estimated TR", "estOfTRShort": "Est. TR", "accOfEst": "Accuracy", "accOfEstShort": "Acc." @@ -312,16 +326,16 @@ "forbiddenSub": "If you are using VPN or Proxy, turn it off. If this does not help, reach out to $nickname", "tooManyRequests": "You have been rate limited.", "tooManyRequestsSub": "Wait a few moments and try again", - "internal": "Something happend on the tetr.io side", + "internal": "Something happened on the tetr.io side", "internalSub": "osk, probably, already aware about it", - "internalWebVersion": "Something happend on the tetr.io side (or on oskware_bridge, idk honestly)", + "internalWebVersion": "Something happened on the tetr.io side (or on oskware_bridge, idk honestly)", "internalWebVersionSub": "If osk status page says that everything is ok, let dan63047 know about this issue", - "oskwareBridge": "Something happend with oskware_bridge", + "oskwareBridge": "Something happened with oskware_bridge", "oskwareBridgeSub": "Let dan63047 know", "p1nkl0bst3rForbidden": "Third party API blocked your IP address", "p1nkl0bst3rTooManyRequests": "Too many requests to third party API. Try again later", - "p1nkl0bst3rinternal": "Something happend on the p1nkl0bst3r side", - "p1nkl0bst3rinternalWebVersion": "Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)", + "p1nkl0bst3rinternal": "Something happened on the p1nkl0bst3r side", + "p1nkl0bst3rinternalWebVersion": "Something happened on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)", "replayAlreadySaved": "Replay already saved", "replayExpired": "Replay expired and not available anymore", "replayRejected": "Third party API blocked your IP address" diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 88dcfad..efa247f 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -81,7 +81,12 @@ "verdictGeneral": "$verdict среднего $rank ранга на $n", "verdictBetter": "Лучше", "verdictWorse": "Хуже", + "smooth": "Гладкий", "gamesUntilRanked": "${left} матчей до получения рейтинга", + "numOfVictories": "~${wins} побед", + "promotionOnNextWin": "Повышение после следующей победы", + "numOfdefeats": "~${losses} поражений", + "demotionOnNextLoss": "Понижение после следующего поражения", "nerdStats": "Для задротов", "playersYouTrack": "Отслеживаемые игроки", "formula": "Формула", @@ -163,6 +168,9 @@ "calc": "Считать", "calcViewNoValues": "Введите значения, чтобы посчитать статистику", "rankAveragesViewTitle": "Требования рангов и средние значения", + "sprintAndBlitsViewTitle": "Средние результаты 40 линий и блица", + "sprintAndBlitsRelevance": "Актуальность: ${date}", + "rank": "Ранг", "averages": "Средние значения", "lbViewZeroEntrys": "Рейтинговая таблица пуста", "lbViewOneEntry": "В рейтинговой таблице всего один игрок", @@ -200,6 +208,12 @@ "currentAxis": "Ось $axis:", "p1nkl0bst3rAlert": "Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r", "notForWeb": "Функция недоступна для веб версии", + "graphs": { + "attack": "Атака", + "speed": "Скорость", + "defense": "Защита", + "cheese": "Сыр" + }, "statCellNum": { "xpLevel": "Уровень\nопыта", "xpProgress": "Прогресс до следующего уровня", diff --git a/res/tetrio_badges/mmc_tabi_superlobby5.png b/res/tetrio_badges/mmc_tabi_superlobby5.png new file mode 100644 index 0000000..edf07dd Binary files /dev/null and b/res/tetrio_badges/mmc_tabi_superlobby5.png differ diff --git a/res/tetrio_badges/pkstarcup_1.png b/res/tetrio_badges/pkstarcup_1.png new file mode 100644 index 0000000..f91cb97 Binary files /dev/null and b/res/tetrio_badges/pkstarcup_1.png differ diff --git a/res/tetrio_badges/pkstarcup_2.png b/res/tetrio_badges/pkstarcup_2.png new file mode 100644 index 0000000..0e932d7 Binary files /dev/null and b/res/tetrio_badges/pkstarcup_2.png differ diff --git a/res/tetrio_badges/pkstarcup_3.png b/res/tetrio_badges/pkstarcup_3.png new file mode 100644 index 0000000..4004fea Binary files /dev/null and b/res/tetrio_badges/pkstarcup_3.png differ diff --git a/res/tetrio_badges/stride_superlobby.png b/res/tetrio_badges/stride_superlobby.png new file mode 100644 index 0000000..cd43105 Binary files /dev/null and b/res/tetrio_badges/stride_superlobby.png differ diff --git a/res/tetrio_badges/taws2_1.png b/res/tetrio_badges/taws2_1.png new file mode 100644 index 0000000..61ac03c Binary files /dev/null and b/res/tetrio_badges/taws2_1.png differ diff --git a/res/tetrio_badges/taws2_2.png b/res/tetrio_badges/taws2_2.png new file mode 100644 index 0000000..33939d0 Binary files /dev/null and b/res/tetrio_badges/taws2_2.png differ diff --git a/res/tetrio_badges/taws2_3.png b/res/tetrio_badges/taws2_3.png new file mode 100644 index 0000000..f80aa0f Binary files /dev/null and b/res/tetrio_badges/taws2_3.png differ diff --git a/res/tetrio_badges/ttsd_ou_1.png b/res/tetrio_badges/ttsd_ou_1.png new file mode 100644 index 0000000..c937225 Binary files /dev/null and b/res/tetrio_badges/ttsd_ou_1.png differ diff --git a/res/tetrio_badges/ttsd_ou_2.png b/res/tetrio_badges/ttsd_ou_2.png new file mode 100644 index 0000000..1891b91 Binary files /dev/null and b/res/tetrio_badges/ttsd_ou_2.png differ diff --git a/res/tetrio_badges/ttsd_ou_3.png b/res/tetrio_badges/ttsd_ou_3.png new file mode 100644 index 0000000..28ea18a Binary files /dev/null and b/res/tetrio_badges/ttsd_ou_3.png differ