Compare commits

..

No commits in common. "0cdeefe3709c25911aeee61e6b33c9bde1f1b9cd" and "395405bfa3664fba600a44b299e289b47aca05b9" have entirely different histories.

38 changed files with 1148 additions and 1661 deletions

View File

@ -1,147 +0,0 @@
import 'dart:math';
// I reimplemented kenany/glicko2-lite in dart lol
// Don't look here lol
List<double> 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<Map<String, double>> scaleOpponents(double mu, List<List<double>> 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<Map<String, double>> 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<Map<String, double>> 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<String, double> 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<double> unscale(mup, phip, options) {
double rating = 173.7178 * mup + options["rating"];
double rd = 173.7178 * phip;
return [ rating, rd ];
}
List<double> rate(double rating, double rd, double sigma, List<List<double>> opponents, Map<String, double> options) {
Map<String, double> opts = { "rating": options["rating"]??1500, "tau": options["tau"]??0.5 };
// Step 2
List<double> scaled = scale(rating, rd, opts["rating"]!);
List<Map<String, double>> 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<String, double> updated = newRating(phis, scaled[0], v, scaledOpponents);
return unscale(updated['mu'], updated['phi'], opts)..add(sigmap);
}

View File

@ -17,9 +17,6 @@ const double appdspWeight = 140;
const double vsapmWeight = 60; const double vsapmWeight = 60;
const double cheeseWeight = 1.25; const double cheeseWeight = 1.25;
const double gbeWeight = 315; const double gbeWeight = 315;
const List<String> ranks = [
"d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x"
];
const Map<String, double> rankCutoffs = { const Map<String, double> rankCutoffs = {
"x": 0.01, "x": 0.01,
"u": 0.05, "u": 0.05,
@ -42,7 +39,7 @@ const Map<String, double> rankCutoffs = {
"": 0.5 "": 0.5
}; };
const Map<String, double> rankTargets = { const Map<String, double> rankTargets = {
"x": 24503.75, // where that comes from? "x": 24008,
"u": 23038, "u": 23038,
"ss": 21583, "ss": 21583,
"s+": 20128, "s+": 20128,
@ -141,87 +138,45 @@ const Map<String, Color> rankColors = { // thanks osk for const rankColors at ht
'z': Color(0xFF375433) 'z': Color(0xFF375433)
}; };
// const Map<String, Duration> sprintAverages = { // old data, based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098 const Map<String, Duration> sprintAverages = { // based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098
// 'x': Duration(seconds: 25, milliseconds: 413), 'x': Duration(seconds: 25, milliseconds: 413),
// 'u': Duration(seconds: 34, milliseconds: 549), 'u': Duration(seconds: 34, milliseconds: 549),
// 'ss': Duration(seconds: 43, milliseconds: 373), 'ss': Duration(seconds: 43, milliseconds: 373),
// 's+': Duration(seconds: 54, milliseconds: 027), 's+': Duration(seconds: 54, milliseconds: 027),
// 's': Duration(seconds: 60, milliseconds: 412), 's': Duration(seconds: 60, milliseconds: 412),
// 's-': Duration(seconds: 67, milliseconds: 381), 's-': Duration(seconds: 67, milliseconds: 381),
// 'a+': Duration(seconds: 73, milliseconds: 694), 'a+': Duration(seconds: 73, milliseconds: 694),
// 'a': Duration(seconds: 81, milliseconds: 166), 'a': Duration(seconds: 81, milliseconds: 166),
// 'a-': Duration(seconds: 88, milliseconds: 334), 'a-': Duration(seconds: 88, milliseconds: 334),
// 'b+': Duration(seconds: 93, milliseconds: 741), 'b+': Duration(seconds: 93, milliseconds: 741),
// 'b': Duration(seconds: 98, milliseconds: 354), 'b': Duration(seconds: 98, milliseconds: 354),
// 'b-': Duration(seconds: 109, milliseconds: 610), 'b-': Duration(seconds: 109, milliseconds: 610),
// 'c+': Duration(seconds: 124, milliseconds: 641), 'c+': Duration(seconds: 124, milliseconds: 641),
// 'c': Duration(seconds: 126, milliseconds: 104), 'c': Duration(seconds: 126, milliseconds: 104),
// 'c-': Duration(seconds: 145, milliseconds: 865), 'c-': Duration(seconds: 145, milliseconds: 865),
// 'd+': Duration(seconds: 154, milliseconds: 338), 'd+': Duration(seconds: 154, milliseconds: 338),
// 'd': Duration(seconds: 162, milliseconds: 063), 'd': Duration(seconds: 162, milliseconds: 063),
// //'z': Duration(seconds: 66, milliseconds: 802)
// };
// const Map<String, int> 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<String, Duration> 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) //'z': Duration(seconds: 66, milliseconds: 802)
}; };
const Map<String, int> blitzAverages = { const Map<String, int> blitzAverages = {
'x': 600715, 'x': 626494,
'u': 379418, 'u': 406059,
'ss': 233740, 'ss': 243166,
's+': 158295, 's+': 168636,
's': 125164, 's': 121594,
's-': 100933, 's-': 107845,
'a+': 83593, 'a+': 87142,
'a': 68613, 'a': 73413,
'a-': 60219, 'a-': 60799,
'b+': 51197, 'b+': 55417,
'b': 44171, 'b': 47608,
'b-': 39045, 'b-': 40534,
'c+': 34130, 'c+': 34200,
'c': 28931, 'c': 32535,
'c-': 25095, 'c-': 25808,
'd+': 22944, 'd+': 23345,
'd': 20728, 'd': 23063,
//'z': 72084 //'z': 72084
}; };
@ -798,15 +753,16 @@ class EstTr {
srarea = (_apm * 0) + (_pps * 135) + (_vs * 0) + (_app * 290) + (_dss * 0) + (_dsp * 700) + (_gbe * 0); srarea = (_apm * 0) + (_pps * 135) + (_vs * 0) + (_app * 290) + (_dss * 0) + (_dsp * 700) + (_gbe * 0);
statrank = 11.2 * atan((srarea - 93) / 130) + 1; statrank = 11.2 * atan((srarea - 93) / 130) + 1;
if (statrank <= 0) statrank = 0.001; 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; 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 / esttr = 25000 /
( (
1 + pow(10, ( 1 + pow(10, (
( (
( (
1500-estglicko 1500-(
0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4
)
)*pi )*pi
)/sqrt( )/sqrt(
( (
@ -1290,8 +1246,6 @@ class TetrioPlayersLeaderboard {
lb.removeWhere((element) => element.country != country); lb.removeWhere((element) => element.country != country);
} }
lb.sort(((a, b) { 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)){ if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){
return reversed ? 1 : -1; return reversed ? 1 : -1;
}else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){ }else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){
@ -1303,6 +1257,20 @@ class TetrioPlayersLeaderboard {
return lb; return lb;
} }
List<TetrioPlayerFromLeaderboard> getStatRankingSequel(Stats stat){
List<TetrioPlayerFromLeaderboard> 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<dynamic> getAverageOfRank(String rank){ // i tried to refactor it and that's was terrible List<dynamic> getAverageOfRank(String rank){ // i tried to refactor it and that's was terrible
if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank"); if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank");
List<TetrioPlayerFromLeaderboard> filtredLeaderboard = List.from(leaderboard); List<TetrioPlayerFromLeaderboard> filtredLeaderboard = List.from(leaderboard);
@ -1885,12 +1853,11 @@ class TetrioPlayersLeaderboard {
"avgStride": avgStride, "avgStride": avgStride,
"avgInfDS": avgInfDS, "avgInfDS": avgInfDS,
"toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].rating : lowestTR, "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 "entries": filtredLeaderboard
}]; }];
}else{ }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), 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, "toEnterGlicko": 0}]; {"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0}];
} }
} }
@ -1959,26 +1926,6 @@ class TetrioPlayersLeaderboard {
'd': getAverageOfRank("d")[1]["toEnterTR"] 'd': getAverageOfRank("d")[1]["toEnterTR"]
}; };
Map<String, double> 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<dynamic> json, String t, DateTime ts) { TetrioPlayersLeaderboard.fromJson(List<dynamic> json, String t, DateTime ts) {
type = t; type = t;
timestamp = ts; timestamp = ts;

View File

@ -1,4 +1,5 @@
import 'dart:math'; import 'dart:math';
import 'package:vector_math/vector_math_64.dart';
import 'tetrio.dart'; import 'tetrio.dart';

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 2 /// Locales: 2
/// Strings: 1144 (572 per locale) /// Strings: 1120 (560 per locale)
/// ///
/// Built on 2024-05-28 at 20:38 UTC /// Built on 2024-03-24 at 14:28 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
@ -216,12 +216,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String verdictGeneral({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; String verdictGeneral({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average';
String get verdictBetter => 'better'; String get verdictBetter => 'better';
String get verdictWorse => 'worse'; String get verdictWorse => 'worse';
String get smooth => 'Smooth';
String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; 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 nerdStats => 'Nerd Stats';
String get playersYouTrack => 'Players you track'; String get playersYouTrack => 'Players you track';
String get formula => 'Formula'; String get formula => 'Formula';
@ -303,9 +298,6 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get calc => 'Calc'; String get calc => 'Calc';
String get calcViewNoValues => 'Enter values to calculate the stats'; String get calcViewNoValues => 'Enter values to calculate the stats';
String get rankAveragesViewTitle => 'Ranks cutoff and average 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 averages => 'Averages';
String get lbViewZeroEntrys => 'Empty list'; String get lbViewZeroEntrys => 'Empty list';
String get lbViewOneEntry => 'There is only one player'; String get lbViewOneEntry => 'There is only one player';
@ -343,7 +335,6 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String currentAxis({required Object axis}) => '${axis} axis:'; String currentAxis({required Object axis}) => '${axis} axis:';
String get p1nkl0bst3rAlert => 'That data was retrived from third party API maintained by p1nkl0bst3r'; String get p1nkl0bst3rAlert => 'That data was retrived from third party API maintained by p1nkl0bst3r';
String get notForWeb => 'Function is not available for web version'; String get notForWeb => 'Function is not available for web version';
late final _StringsGraphsEn graphs = _StringsGraphsEn._(_root);
late final _StringsStatCellNumEn statCellNum = _StringsStatCellNumEn._(_root); late final _StringsStatCellNumEn statCellNum = _StringsStatCellNumEn._(_root);
Map<String, String> get playerRole => { Map<String, String> get playerRole => {
'user': 'User', 'user': 'User',
@ -643,19 +634,6 @@ class _StringsNewsPartsEn {
String unknownNews({required Object type}) => 'Unknown news of type ${type}'; 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 // Path: statCellNum
class _StringsStatCellNumEn { class _StringsStatCellNumEn {
_StringsStatCellNumEn._(this._root); _StringsStatCellNumEn._(this._root);
@ -712,7 +690,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 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 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 areaDescription => 'How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections';
String get estOfTR => 'Estimated TR'; String get estOfTR => 'Est. of TR';
String get estOfTRShort => 'Est. TR'; String get estOfTRShort => 'Est. TR';
String get accOfEst => 'Accuracy'; String get accOfEst => 'Accuracy';
String get accOfEstShort => 'Acc.'; String get accOfEstShort => 'Acc.';
@ -785,16 +763,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 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 tooManyRequests => 'You have been rate limited.';
String get tooManyRequestsSub => 'Wait a few moments and try again'; String get tooManyRequestsSub => 'Wait a few moments and try again';
String get internal => 'Something happened on the tetr.io side'; String get internal => 'Something happend on the tetr.io side';
String get internalSub => 'osk, probably, already aware about it'; String get internalSub => 'osk, probably, already aware about it';
String get internalWebVersion => 'Something happened on the tetr.io side (or on oskware_bridge, idk honestly)'; String get internalWebVersion => 'Something happend 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 internalWebVersionSub => 'If osk status page says that everything is ok, let dan63047 know about this issue';
String get oskwareBridge => 'Something happened with oskware_bridge'; String get oskwareBridge => 'Something happend with oskware_bridge';
String get oskwareBridgeSub => 'Let dan63047 know'; String get oskwareBridgeSub => 'Let dan63047 know';
String get p1nkl0bst3rForbidden => 'Third party API blocked your IP address'; 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 p1nkl0bst3rTooManyRequests => 'Too many requests to third party API. Try again later';
String get p1nkl0bst3rinternal => 'Something happened on the p1nkl0bst3r side'; String get p1nkl0bst3rinternal => 'Something happend on the p1nkl0bst3r side';
String get p1nkl0bst3rinternalWebVersion => 'Something happened on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; String get p1nkl0bst3rinternalWebVersion => 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)';
String get replayAlreadySaved => 'Replay already saved'; String get replayAlreadySaved => 'Replay already saved';
String get replayExpired => 'Replay expired and not available anymore'; String get replayExpired => 'Replay expired and not available anymore';
String get replayRejected => 'Third party API blocked your IP address'; String get replayRejected => 'Third party API blocked your IP address';
@ -892,12 +870,7 @@ class _StringsRu implements Translations {
@override String verdictGeneral({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; @override String verdictGeneral({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}';
@override String get verdictBetter => 'Лучше'; @override String get verdictBetter => 'Лучше';
@override String get verdictWorse => 'Хуже'; @override String get verdictWorse => 'Хуже';
@override String get smooth => 'Гладкий';
@override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; @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 nerdStats => 'Для задротов';
@override String get playersYouTrack => 'Отслеживаемые игроки'; @override String get playersYouTrack => 'Отслеживаемые игроки';
@override String get formula => 'Формула'; @override String get formula => 'Формула';
@ -979,9 +952,6 @@ class _StringsRu implements Translations {
@override String get calc => 'Считать'; @override String get calc => 'Считать';
@override String get calcViewNoValues => 'Введите значения, чтобы посчитать статистику'; @override String get calcViewNoValues => 'Введите значения, чтобы посчитать статистику';
@override String get rankAveragesViewTitle => 'Требования рангов и средние значения'; @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 averages => 'Средние значения';
@override String get lbViewZeroEntrys => 'Рейтинговая таблица пуста'; @override String get lbViewZeroEntrys => 'Рейтинговая таблица пуста';
@override String get lbViewOneEntry => 'В рейтинговой таблице всего один игрок'; @override String get lbViewOneEntry => 'В рейтинговой таблице всего один игрок';
@ -1019,7 +989,6 @@ class _StringsRu implements Translations {
@override String currentAxis({required Object axis}) => 'Ось ${axis}:'; @override String currentAxis({required Object axis}) => 'Ось ${axis}:';
@override String get p1nkl0bst3rAlert => 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; @override String get p1nkl0bst3rAlert => 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r';
@override String get notForWeb => 'Функция недоступна для веб версии'; @override String get notForWeb => 'Функция недоступна для веб версии';
@override late final _StringsGraphsRu graphs = _StringsGraphsRu._(_root);
@override late final _StringsStatCellNumRu statCellNum = _StringsStatCellNumRu._(_root); @override late final _StringsStatCellNumRu statCellNum = _StringsStatCellNumRu._(_root);
@override Map<String, String> get playerRole => { @override Map<String, String> get playerRole => {
'user': 'Пользователь', 'user': 'Пользователь',
@ -1319,19 +1288,6 @@ class _StringsNewsPartsRu implements _StringsNewsPartsEn {
@override String unknownNews({required Object type}) => 'Неизвестная новость типа ${type}'; @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 // Path: statCellNum
class _StringsStatCellNumRu implements _StringsStatCellNumEn { class _StringsStatCellNumRu implements _StringsStatCellNumEn {
_StringsStatCellNumRu._(this._root); _StringsStatCellNumRu._(this._root);
@ -1560,12 +1516,7 @@ extension on Translations {
case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average';
case 'verdictBetter': return 'better'; case 'verdictBetter': return 'better';
case 'verdictWorse': return 'worse'; case 'verdictWorse': return 'worse';
case 'smooth': return 'Smooth';
case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; 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 'nerdStats': return 'Nerd Stats';
case 'playersYouTrack': return 'Players you track'; case 'playersYouTrack': return 'Players you track';
case 'formula': return 'Formula'; case 'formula': return 'Formula';
@ -1647,9 +1598,6 @@ extension on Translations {
case 'calc': return 'Calc'; case 'calc': return 'Calc';
case 'calcViewNoValues': return 'Enter values to calculate the stats'; case 'calcViewNoValues': return 'Enter values to calculate the stats';
case 'rankAveragesViewTitle': return 'Ranks cutoff and average 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 'averages': return 'Averages';
case 'lbViewZeroEntrys': return 'Empty list'; case 'lbViewZeroEntrys': return 'Empty list';
case 'lbViewOneEntry': return 'There is only one player'; case 'lbViewOneEntry': return 'There is only one player';
@ -1687,10 +1635,6 @@ extension on Translations {
case 'currentAxis': return ({required Object axis}) => '${axis} axis:'; case 'currentAxis': return ({required Object axis}) => '${axis} axis:';
case 'p1nkl0bst3rAlert': return 'That data was retrived from third party API maintained by p1nkl0bst3r'; 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 '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.xpLevel': return 'XP Level';
case 'statCellNum.xpProgress': return 'Progress to next level'; case 'statCellNum.xpProgress': return 'Progress to next level';
case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Progress from 0 XP to level ${n}'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Progress from 0 XP to level ${n}';
@ -1740,7 +1684,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.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.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.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 'Estimated TR'; case 'statCellNum.estOfTR': return 'Est. of TR';
case 'statCellNum.estOfTRShort': return 'Est. TR'; case 'statCellNum.estOfTRShort': return 'Est. TR';
case 'statCellNum.accOfEst': return 'Accuracy'; case 'statCellNum.accOfEst': return 'Accuracy';
case 'statCellNum.accOfEstShort': return 'Acc.'; case 'statCellNum.accOfEstShort': return 'Acc.';
@ -1794,16 +1738,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.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.tooManyRequests': return 'You have been rate limited.';
case 'errors.tooManyRequestsSub': return 'Wait a few moments and try again'; case 'errors.tooManyRequestsSub': return 'Wait a few moments and try again';
case 'errors.internal': return 'Something happened on the tetr.io side'; case 'errors.internal': return 'Something happend on the tetr.io side';
case 'errors.internalSub': return 'osk, probably, already aware about it'; case 'errors.internalSub': return 'osk, probably, already aware about it';
case 'errors.internalWebVersion': return 'Something happened on the tetr.io side (or on oskware_bridge, idk honestly)'; case 'errors.internalWebVersion': return 'Something happend 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.internalWebVersionSub': return 'If osk status page says that everything is ok, let dan63047 know about this issue';
case 'errors.oskwareBridge': return 'Something happened with oskware_bridge'; case 'errors.oskwareBridge': return 'Something happend with oskware_bridge';
case 'errors.oskwareBridgeSub': return 'Let dan63047 know'; case 'errors.oskwareBridgeSub': return 'Let dan63047 know';
case 'errors.p1nkl0bst3rForbidden': return 'Third party API blocked your IP address'; 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.p1nkl0bst3rTooManyRequests': return 'Too many requests to third party API. Try again later';
case 'errors.p1nkl0bst3rinternal': return 'Something happened on the p1nkl0bst3r side'; case 'errors.p1nkl0bst3rinternal': return 'Something happend on the p1nkl0bst3r side';
case 'errors.p1nkl0bst3rinternalWebVersion': return 'Something happened on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; case 'errors.p1nkl0bst3rinternalWebVersion': return 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)';
case 'errors.replayAlreadySaved': return 'Replay already saved'; case 'errors.replayAlreadySaved': return 'Replay already saved';
case 'errors.replayExpired': return 'Replay expired and not available anymore'; case 'errors.replayExpired': return 'Replay expired and not available anymore';
case 'errors.replayRejected': return 'Third party API blocked your IP address'; case 'errors.replayRejected': return 'Third party API blocked your IP address';
@ -2152,12 +2096,7 @@ extension on _StringsRu {
case 'verdictGeneral': return ({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; case 'verdictGeneral': return ({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}';
case 'verdictBetter': return 'Лучше'; case 'verdictBetter': return 'Лучше';
case 'verdictWorse': return 'Хуже'; case 'verdictWorse': return 'Хуже';
case 'smooth': return 'Гладкий';
case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; 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 'nerdStats': return 'Для задротов';
case 'playersYouTrack': return 'Отслеживаемые игроки'; case 'playersYouTrack': return 'Отслеживаемые игроки';
case 'formula': return 'Формула'; case 'formula': return 'Формула';
@ -2239,9 +2178,6 @@ extension on _StringsRu {
case 'calc': return 'Считать'; case 'calc': return 'Считать';
case 'calcViewNoValues': return 'Введите значения, чтобы посчитать статистику'; case 'calcViewNoValues': return 'Введите значения, чтобы посчитать статистику';
case 'rankAveragesViewTitle': return 'Требования рангов и средние значения'; case 'rankAveragesViewTitle': return 'Требования рангов и средние значения';
case 'sprintAndBlitsViewTitle': return 'Средние результаты 40 линий и блица';
case 'sprintAndBlitsRelevance': return ({required Object date}) => 'Актуальность: ${date}';
case 'rank': return 'Ранг';
case 'averages': return 'Средние значения'; case 'averages': return 'Средние значения';
case 'lbViewZeroEntrys': return 'Рейтинговая таблица пуста'; case 'lbViewZeroEntrys': return 'Рейтинговая таблица пуста';
case 'lbViewOneEntry': return 'В рейтинговой таблице всего один игрок'; case 'lbViewOneEntry': return 'В рейтинговой таблице всего один игрок';
@ -2279,10 +2215,6 @@ extension on _StringsRu {
case 'currentAxis': return ({required Object axis}) => 'Ось ${axis}:'; case 'currentAxis': return ({required Object axis}) => 'Ось ${axis}:';
case 'p1nkl0bst3rAlert': return 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; case 'p1nkl0bst3rAlert': return 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r';
case 'notForWeb': return 'Функция недоступна для веб версии'; 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.xpLevel': return 'Уровень\nопыта';
case 'statCellNum.xpProgress': return 'Прогресс до следующего уровня'; case 'statCellNum.xpProgress': return 'Прогресс до следующего уровня';
case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Прогресс от 0 XP до ${n} уровня'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Прогресс от 0 XP до ${n} уровня';

View File

@ -70,7 +70,6 @@ class TetrioService extends DB {
// I'm trying to send as less requests, as possible, so i'm caching the results of those requests. // 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} // Usually those maps looks like this: {"cached_until_unix_milliseconds": Object}
// TODO: Make a proper caching system
final Map<String, TetrioPlayer> _playersCache = {}; final Map<String, TetrioPlayer> _playersCache = {};
final Map<String, Map<String, dynamic>> _recordsCache = {}; final Map<String, Map<String, dynamic>> _recordsCache = {};
final Map<String, dynamic> _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]} final Map<String, dynamic> _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]}
@ -78,8 +77,6 @@ class TetrioService extends DB {
final Map<String, PlayerLeaderboardPosition> _lbPositions = {}; final Map<String, PlayerLeaderboardPosition> _lbPositions = {};
final Map<String, List<News>> _newsCache = {}; final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {}; final Map<String, Map<String, double?>> _topTRcache = {};
final Map<String, List<Map<String, double>>> _cutoffsCache = {};
final Map<String, TetrioPlayerFromLeaderboard> _topOneFromLB = {};
final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {}; final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {};
/// Thing, that sends every request to the API endpoints /// 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()); 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());
@ -300,119 +297,6 @@ class TetrioService extends DB {
// Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above, // 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 // so i'm going to document only unique differences between them
Future<List<Map<String, double>>> 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<String, dynamic> rawData = jsonDecode(response.body);
Map<String, dynamic> data = rawData["cutoffs"] as Map<String, dynamic>;
Map<String, double> trCutoffs = {};
Map<String, double> 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<TetrioPlayerFromLeaderboard> 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 /// 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. /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data.
Future<List<TetrioPlayer>> fetchAndsaveTLHistory(String id) async { Future<List<TetrioPlayer>> fetchAndsaveTLHistory(String id) async {

View File

@ -2,7 +2,6 @@ import 'package:intl/intl.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; 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 intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0);
final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4);
final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3);

View File

@ -23,12 +23,14 @@ class CalcView extends StatefulWidget {
} }
class CalcState extends State<CalcView> { class CalcState extends State<CalcView> {
late ScrollController _scrollController;
TextEditingController ppsController = TextEditingController(); TextEditingController ppsController = TextEditingController();
TextEditingController apmController = TextEditingController(); TextEditingController apmController = TextEditingController();
TextEditingController vsController = TextEditingController(); TextEditingController vsController = TextEditingController();
@override @override
void initState() { void initState() {
_scrollController = ScrollController();
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value); windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.statsCalc}"); windowManager.setTitle("Tetra Stats: ${t.statsCalc}");
@ -107,7 +109,7 @@ class CalcState extends State<CalcView> {
], ],
), ),
), ),
const Divider(), Divider(),
if (nerdStats == null) Text(t.calcViewNoValues) if (nerdStats == null) Text(t.calcViewNoValues)
else Column(children: [ else Column(children: [
_ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),

View File

@ -257,7 +257,7 @@ class CompareState extends State<CompareView> {
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: SingleChildScrollView( body: SingleChildScrollView(
controller: _scrollController, controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
child: Center( child: Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 768), constraints: const BoxConstraints(maxWidth: 768),
@ -317,7 +317,7 @@ class CompareState extends State<CompareView> {
], ],
), ),
), ),
const Divider(), Divider(),
if (!listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])) Column( if (!listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])) Column(
children: [ children: [
if (theGreenSide[0] != null && theRedSide[0] != null && theGreenSide[0]!.role != "banned" && theRedSide[0]!.role != "banned") if (theGreenSide[0] != null && theRedSide[0] != null && theGreenSide[0]!.role != "banned" && theRedSide[0]!.role != "banned")
@ -820,7 +820,6 @@ class CompareThingy extends StatelessWidget {
colors: const [Colors.green, Colors.transparent], colors: const [Colors.green, Colors.transparent],
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, end: Alignment.centerRight,
transform: const GradientRotation(0.6),
stops: [ stops: [
0.0, 0.0,
higherIsBetter higherIsBetter
@ -831,8 +830,7 @@ class CompareThingy extends StatelessWidget {
? 0.6 ? 0.6
: 0 : 0
], ],
) )),
),
child: Text( child: Text(
(prefix ?? "") + f.format(greenSide) + (postfix ?? ""), (prefix ?? "") + f.format(greenSide) + (postfix ?? ""),
style: const TextStyle( style: const TextStyle(
@ -840,12 +838,7 @@ class CompareThingy extends StatelessWidget {
shadows: <Shadow>[ shadows: <Shadow>[
Shadow( Shadow(
offset: Offset(0.0, 0.0), offset: Offset(0.0, 0.0),
blurRadius: 1.0, blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(0.0, 0.0),
blurRadius: 2.0,
color: Colors.black, color: Colors.black,
), ),
Shadow( Shadow(
@ -881,7 +874,6 @@ class CompareThingy extends StatelessWidget {
colors: const [Colors.red, Colors.transparent], colors: const [Colors.red, Colors.transparent],
begin: Alignment.centerRight, begin: Alignment.centerRight,
end: Alignment.centerLeft, end: Alignment.centerLeft,
transform: const GradientRotation(-0.6),
stops: [ stops: [
0.0, 0.0,
higherIsBetter higherIsBetter

View File

@ -1,8 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
late String oldWindowTitle; late String oldWindowTitle;

View File

@ -7,9 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:intl/intl.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:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.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/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/tetrio_crud.dart'; import 'package:tetra_stats/services/tetrio_crud.dart';
@ -18,7 +19,6 @@ import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/text_shadow.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/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_leaderboard_view.dart' show TLLeaderboardView;
import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView;
import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/finesse_thingy.dart';
@ -35,8 +35,6 @@ import 'package:go_router/go_router.dart';
final TetrioService teto = TetrioService(); // thing, that manadge our local DB final TetrioService teto = TetrioService(); // thing, that manadge our local DB
int _chartsIndex = 0; int _chartsIndex = 0;
bool _gamesPlayedInsteadOfDateAndTime = false; 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"]; 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; late ScrollController _scrollController;
final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode); final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode);
@ -83,14 +81,11 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
TetrioPlayersLeaderboard? everyone; TetrioPlayersLeaderboard? everyone;
PlayerLeaderboardPosition? meAmongEveryone; PlayerLeaderboardPosition? meAmongEveryone;
TetraLeagueAlpha? rankAverages; TetraLeagueAlpha? rankAverages;
double? thatRankCutoff;
double? nextRankCutoff;
double? thatRankGlickoCutoff;
double? nextRankGlickoCutoff;
String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for
String _titleNickname = ""; String _titleNickname = "dan63047";
/// Each dropdown menu item contains list of dots for the graph /// Each dropdown menu item contains list of dots for the graph
List<DropdownMenuItem<List<_HistoryChartSpot>>> chartsData = []; var chartsData = <DropdownMenuItem<List<FlSpot>>>[];
var chartsDataGamesPlayed = <DropdownMenuItem<List<FlSpot>>>[];
//var tableData = <TableRow>[]; //var tableData = <TableRow>[];
final bodyGlobalKey = GlobalKey(); final bodyGlobalKey = GlobalKey();
bool _showSearchBar = false; bool _showSearchBar = false;
@ -107,18 +102,13 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
_scrollController = ScrollController(); _scrollController = ScrollController();
_tabController = TabController(length: 6, vsync: this); _tabController = TabController(length: 6, vsync: this);
_wideScreenTabController = TabController(length: 4, vsync: this); _wideScreenTabController = TabController(length: 4, vsync: this);
_zoomPanBehavior = ZoomPanBehavior(
enablePinching: true,
enableSelectionZooming: true,
enableMouseWheelZooming : true,
enablePanning: true,
);
// We need to show something // We need to show something
if (widget.player != null){ // if we have user input, if (widget.player != null){ // if we have user input,
changePlayer(widget.player!); // it's gonna be user input changePlayer(widget.player!); // it's gonna be user input
}else{ }else{
_getPreferences() // otherwise, checking for preferences _getPreferences() // otherwise, checking for preferences
.then((value) => changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc")); // no preferences - loading me .then((value) => changePlayer(prefs.getString("player") ?? "dan63047")); // no preferences - loading me
} }
super.initState(); super.initState();
} }
@ -176,41 +166,26 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
late TetraLeagueAlphaStream tlStream; late TetraLeagueAlphaStream tlStream;
late Map<String, dynamic> records; late Map<String, dynamic> records;
late List<News> news; late List<News> news;
late TetrioPlayerFromLeaderboard? topOne;
late double? topTR; late double? topTR;
requests = await Future.wait([ // all at once requests = await Future.wait([ // all at once
teto.fetchTLStream(_searchFor), teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor), teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor), teto.fetchNews(_searchFor),
prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=><Map<String, double>>[]),
(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 if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR
]); ]);
tlStream = requests[0] as TetraLeagueAlphaStream; tlStream = requests[0] as TetraLeagueAlphaStream;
records = requests[1] as Map<String, dynamic>; records = requests[1] as Map<String, dynamic>;
news = requests[2] as List<News>; news = requests[2] as List<News>;
topOne = requests[4] as TetrioPlayerFromLeaderboard?; topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR
topTR = requests.elementAtOrNull(5) as double?; // No TR - no Top TR
meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId);
if (prefs.getBool("showPositions") == true){ if (meAmongEveryone == null && prefs.getBool("showPositions") == true){
// Get tetra League leaderboard // Get tetra League leaderboard
everyone = teto.getCachedLeaderboard(); everyone = teto.getCachedLeaderboard();
everyone ??= await teto.fetchTLLeaderboard(); everyone ??= await teto.fetchTLLeaderboard();
if (meAmongEveryone == null){
meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me);
if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!);
} }
}
Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as List<Map<String, double>>).elementAtOrNull(0);
Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as List<Map<String, double>>).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]; if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0];
@ -279,30 +254,58 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1); if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1);
} }
// Also i need previous Tetra League State for comparison if avaliable // 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){ if (uniqueTL.length >= 2){
compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2);
chartsData = <DropdownMenuItem<List<_HistoryChartSpot>>>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid chartsData = <DropdownMenuItem<List<FlSpot>>>[ // 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) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), 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) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), 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.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) _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.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) _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.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) _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.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) _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) 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) _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) 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) _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) 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) _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) 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) _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) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), 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) 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) _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) 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) _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) 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) _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.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) _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.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) _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.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) _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) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), 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) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), 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) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), 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")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")),
];
chartsDataGamesPlayed = <DropdownMenuItem<List<FlSpot>>>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.rating)], child: Text(t.statCellNum.tr)),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.glicko!)], child: const Text("Glicko")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.gamesPlayed.toDouble(), tl.rd!)], child: const Text("Rating Deviation")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) FlSpot(tl.gamesPlayed.toDouble(), tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) FlSpot(tl.gamesPlayed.toDouble(), tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) FlSpot(tl.gamesPlayed.toDouble(), tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.vsapm)], child: const Text("VS/APM")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.gamesPlayed.toDouble(), tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) FlSpot(tl.gamesPlayed.toDouble(), tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) FlSpot(tl.gamesPlayed.toDouble(), tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.opener)], child: const Text("Opener")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.plonk)], child: const Text("Plonk")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.infds)], child: const Text("Inf. DS")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.gamesPlayed.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")),
]; ];
}else{ }else{
compareWith = null; compareWith = null;
@ -462,12 +465,6 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
topTR: snapshot.data![7], topTR: snapshot.data![7],
bot: snapshot.data![0].role == "bot", bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon", 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, averages: rankAverages,
lbPositions: meAmongEveryone lbPositions: meAmongEveryone
), ),
@ -477,7 +474,7 @@ class _MainState extends State<MainView> 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,) child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true,)
), ),
],), ],),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,), _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],) _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
] : [ ] : [
@ -488,17 +485,11 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
topTR: snapshot.data![7], topTR: snapshot.data![7],
bot: snapshot.data![0].role == "bot", bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon", 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, averages: rankAverages,
lbPositions: meAmongEveryone lbPositions: meAmongEveryone
), ),
_TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched), _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank), _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank),
_RecordThingy(record: snapshot.data![1]['blitz'], 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],) _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
@ -550,7 +541,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
if (subText != null) Padding( if (subText != null) Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18)),
), ),
], ],
) )
@ -633,7 +624,7 @@ class _NavDrawerState extends State<NavDrawer> {
leading: const Icon(Icons.home), leading: const Icon(Icons.home),
title: Text(homePlayerNickname), title: Text(homePlayerNickname),
onTap: () { onTap: () {
widget.changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // changes player on main view to the one from preferences widget.changePlayer(prefs.getString("player") ?? "dan63047"); // changes player on main view to the one from preferences
Navigator.of(context).pop(); // and then NavDrawer closes itself. Navigator.of(context).pop(); // and then NavDrawer closes itself.
}, },
), ),
@ -666,20 +657,6 @@ class _NavDrawerState extends State<NavDrawer> {
}, },
), ),
), ),
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()) const SliverToBoxAdapter(child: Divider())
]; ];
}, },
@ -775,7 +752,8 @@ class _TLRecords extends StatelessWidget {
} }
class _History extends StatelessWidget{ class _History extends StatelessWidget{
final List<DropdownMenuItem<List<_HistoryChartSpot>>> chartsData; final List<DropdownMenuItem<List<FlSpot>>> chartsData;
final List<DropdownMenuItem<List<FlSpot>>> chartsDataGamesPlayed;
final String userID; final String userID;
final Function update; final Function update;
final Function changePlayer; final Function changePlayer;
@ -783,7 +761,7 @@ class _History extends StatelessWidget{
/// Widget, that can show history of some stat of the player on the graph. /// 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 /// Requires player [states], which is list of states and function [update], which rebuild widgets
const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); const _History({required this.chartsData, required this.chartsDataGamesPlayed, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -798,16 +776,16 @@ class _History extends StatelessWidget{
)); ));
} }
bool bigScreen = MediaQuery.of(context).size.width > 768; 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( return SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
primary: true,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Wrap( Wrap(
spacing: 20, spacing: 20,
crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -837,22 +815,9 @@ class _History extends StatelessWidget{
), ),
], ],
), ),
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: _gamesPlayedInsteadOfDateAndTime ? chartsDataGamesPlayed[_chartsIndex].value! : chartsData[_chartsIndex].value!, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact())
],
),
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( else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -863,21 +828,13 @@ class _History extends StatelessWidget{
)) ))
], ],
), ),
),
); );
} }
} }
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{ class _HistoryChartThigy extends StatefulWidget{
final List<_HistoryChartSpot> data; final List<FlSpot> data;
final bool smooth;
final String yAxisTitle; final String yAxisTitle;
final bool bigScreen; final bool bigScreen;
final double leftSpace; final double leftSpace;
@ -887,7 +844,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. /// 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 /// [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 /// for left titles
const _HistoryChartThigy({required this.data, required this.smooth, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat}); const _HistoryChartThigy({required this.data, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat});
@override @override
State<_HistoryChartThigy> createState() => _HistoryChartThigyState(); State<_HistoryChartThigy> createState() => _HistoryChartThigyState();
@ -896,117 +853,292 @@ class _HistoryChartThigy extends StatefulWidget{
class _HistoryChartThigyState extends State<_HistoryChartThigy> { class _HistoryChartThigyState extends State<_HistoryChartThigy> {
late String previousAxisTitle; late String previousAxisTitle;
late bool previousGamesPlayedInsteadOfDateAndTime; late bool previousGamesPlayedInsteadOfDateAndTime;
late TooltipBehavior _tooltipBehavior; 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;
@override @override
void initState(){ void initState(){
super.initState(); super.initState();
_tooltipBehavior = TooltipBehavior( minX = widget.data.first.x;
color: Colors.black, maxX = widget.data.last.x;
borderColor: Colors.white, setMinMaxY();
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; previousAxisTitle = widget.yAxisTitle;
previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime;
actualMaxY = maxY;
actualMinY = minY;
recalculateScales();
} }
@override @override
void dispose(){ void dispose(){
super.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<State<StatefulWidget>> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
GlobalKey graphKey = GlobalKey();
if ((previousAxisTitle != widget.yAxisTitle) || (previousGamesPlayedInsteadOfDateAndTime != _gamesPlayedInsteadOfDateAndTime)) { if ((previousAxisTitle != widget.yAxisTitle) || (previousGamesPlayedInsteadOfDateAndTime != _gamesPlayedInsteadOfDateAndTime)) {
minX = widget.data.first.x;
maxX = widget.data.last.x;
recalculateScales();
setMinMaxY();
previousAxisTitle = widget.yAxisTitle; previousAxisTitle = widget.yAxisTitle;
previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime;
setState((){}); 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); 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( return SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 104, height: MediaQuery.of(context).size.height - 104,
child: Padding( padding: padding,
child: Listener( child: Listener(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onPointerSignal: (signal) { onPointerSignal: (signal) {
if (signal is PointerScrollEvent) { 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
setState(() { 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 _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView
}); });
} }
}, },
child: SfCartesianChart( child:
tooltipBehavior: _tooltipBehavior, GestureDetector(
zoomPanBehavior: _zoomPanBehavior, behavior: HitTestBehavior.translucent,
primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), onDoubleTap: () {
primaryYAxis: const NumericAxis( setState(() {
rangePadding: ChartRangePadding.additional, minX = widget.data.first.x;
), maxX = widget.data.last.x;
series: <CartesianSeries>[ minY = actualMinY;
if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( maxY = actualMaxY;
enableTooltip: true, recalculateScales();
// splineType: SplineType.cardinal, });
// cardinalSplineTension: 0.2, },
dataSource: widget.data, // TODO: onScaleUpdate:(details) => scaleHandler(details, graphKey, graphStartX, graphEndX),
animationDuration: 0, // TODO: Figure out wtf is going on with gestures
opacity: _smooth ? 0 : 1, // TODO: Somehow highlight touched spot (handleBuiltInTouches breaks getTooltipItems and getTouchedSpotIndicator)
xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, child: Padding( padding: padding,
yValueMapper: (_HistoryChartSpot data, _) => data.stat, child: Stack(
trendlines:<Trendline>[ children: [
Trendline( LineChart(
isVisible: _smooth, key: graphKey,
period: (widget.data.length/175).floor(), LineChartData(
type: TrendlineType.movingAverage, lineBarsData: [LineChartBarData(spots: widget.data)],
color: Colors.blue) 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;});
}
},
) )
else StepLineSeries<_HistoryChartSpot, DateTime>( )
enableTooltip: true, ),
// splineType: SplineType.cardinal, Padding(
// cardinalSplineTension: 0.2, padding: EdgeInsets.only(left: widget.leftSpace),
dataSource: widget.data, child: Column(
animationDuration: 0, children: [
opacity: _smooth ? 0 : 1, 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)),
xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, 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)),
yValueMapper: (_HistoryChartSpot data, _) => data.stat,
trendlines:<Trendline>[
Trendline(
isVisible: _smooth,
period: (widget.data.length/175).floor(),
type: TrendlineType.movingAverage,
color: Colors.blue)
], ],
), ),
)
], ],
), ),
), ),
),
) )
); );
} }
} }
// class _HistoryTableThingy extends StatelessWidget{
// final List<TableRow> 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 { class _TwoRecordsThingy extends StatelessWidget {
final RecordSingle? sprint; final RecordSingle? sprint;
final RecordSingle? blitz; final RecordSingle? blitz;
@ -1096,7 +1228,7 @@ class _TwoRecordsThingy extends StatelessWidget {
), ),
if (sprint != null) FinesseThingy(sprint?.endContext?.finesse, sprint?.endContext?.finessePercentage), 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) 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( Column(
@ -1157,7 +1289,7 @@ class _TwoRecordsThingy extends StatelessWidget {
), ),
if (blitz != null) FinesseThingy(blitz?.endContext?.finesse, blitz?.endContext?.finessePercentage), 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) 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")
], ],
), ),
]), ]),
@ -1273,8 +1405,8 @@ class _RecordThingy extends StatelessWidget {
), ),
FinesseThingy(record?.endContext?.finesse, record?.endContext?.finessePercentage), FinesseThingy(record?.endContext?.finesse, record?.endContext?.finessePercentage),
LineclearsThingy(record!.endContext!.clears, record!.endContext!.lines, record!.endContext!.holds, record!.endContext!.tSpins), 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("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("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KpP • ${f2.format(record!.endContext!.kps)} KpS")
] ]
), ),
), ),

View File

@ -1,4 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -6,16 +8,13 @@ import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/main_view.dart' show MainView; 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:window_manager/window_manager.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
var _chartsShortTitlesDropdowns = <DropdownMenuItem>[for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)]; var _chartsShortTitlesDropdowns = <DropdownMenuItem>[for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)];
Stats _chartsX = Stats.tr; Stats _chartsX = Stats.tr;
Stats _chartsY = Stats.apm; Stats _chartsY = Stats.apm;
late TooltipBehavior _tooltipBehavior;
late ZoomPanBehavior _zoomPanBehavior;
List<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; List<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
List<_MyScatterSpot> _spots = [];
Stats _sortBy = Stats.tr; Stats _sortBy = Stats.tr;
late List<TetrioPlayerFromLeaderboard> they; late List<TetrioPlayerFromLeaderboard> they;
bool _reversed = false; bool _reversed = false;
@ -49,7 +48,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
late double yScale; late double yScale;
String headerTooltip = t.pseudoTooltipHeaderInit; String headerTooltip = t.pseudoTooltipHeaderInit;
String footerTooltip = t.pseudoTooltipFooterInit; String footerTooltip = t.pseudoTooltipFooterInit;
ValueNotifier<int> hoveredPointId = ValueNotifier<int>(-1); int hoveredPointId = -1;
double scaleFactor = 5e2; double scaleFactor = 5e2;
double dragFactor = 7e2; double dragFactor = 7e2;
@ -57,37 +56,6 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
void initState() { void initState() {
_scrollController = ScrollController(); _scrollController = ScrollController();
_tabController = TabController(length: 6, vsync: this); _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){ if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => _oldWindowTitle = value); windowManager.getTitle().then((value) => _oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}"); windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}");
@ -95,22 +63,56 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
super.initState(); super.initState();
previousAxisTitles = _chartsX.toString()+_chartsY.toString(); previousAxisTitles = _chartsX.toString()+_chartsY.toString();
they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
createSpots(); recalculateBoundaries();
resetScale();
} }
void createSpots(){ void recalculateBoundaries(){
_spots = [ actualMinX = (widget.rank[1]["entries"] as List<TetrioPlayerFromLeaderboard>).reduce((value, element) {
for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"]) num n = min(value.getStatByEnum(_chartsX), element.getStatByEnum(_chartsX));
if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception if (value.getStatByEnum(_chartsX) == n) {
_MyScatterSpot( return value;
entry.getStatByEnum(_chartsX).toDouble(), } else {
entry.getStatByEnum(_chartsY).toDouble(), return element;
entry.userId, }
entry.username, }).getStatByEnum(_chartsX).toDouble();
entry.rank, actualMaxX = (widget.rank[1]["entries"] as List<TetrioPlayerFromLeaderboard>).reduce((value, element) {
rankColors[entry.rank]??Colors.white 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<TetrioPlayerFromLeaderboard>).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<TetrioPlayerFromLeaderboard>).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;
} }
@override @override
@ -121,15 +123,46 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
super.dispose(); 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() { void _justUpdate() {
setState(() {}); setState(() {});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
GlobalKey graphKey = GlobalKey();
bool bigScreen = MediaQuery.of(context).size.width > 768; 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()){ if (previousAxisTitles != _chartsX.toString()+_chartsY.toString()){
createSpots(); recalculateBoundaries();
resetScale();
previousAxisTitles = _chartsX.toString()+_chartsY.toString(); previousAxisTitles = _chartsX.toString()+_chartsY.toString();
} }
final t = Translations.of(context); final t = Translations.of(context);
@ -197,8 +230,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
Wrap( Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.end, spacing: 25,
spacing: 20,
children: [ children: [
Column( Column(
children: [ children: [
@ -239,41 +271,115 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
), ),
], ],
), ),
IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,)
], ],
), ),
if (widget.rank[1]["entries"].length > 1) if (widget.rank[1]["entries"].length > 1)
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 104, height: MediaQuery.of(context).size.height - 104,
child: Padding(
padding: bigScreen ? const EdgeInsets.fromLTRB(40, 10, 40, 20) : const EdgeInsets.fromLTRB(0, 10, 16, 20),
child: Listener( child: Listener(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onPointerSignal: (signal) { onPointerSignal: (signal) {
if (signal is PointerScrollEvent) { 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(() { setState(() {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView 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: () {
setState(() {
minX = actualMinX;
maxX = actualMaxX;
minY = actualMinY;
maxY = actualMaxY;
recalculateScales();
});
},
// 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));
}
}, },
child: SfCartesianChart( ),
tooltipBehavior: _tooltipBehavior, ),
zoomPanBehavior: _zoomPanBehavior, swapAnimationDuration: const Duration(milliseconds: 150), // Optional
//primaryXAxis: CategoryAxis(), swapAnimationCurve: Curves.linear, // Optional
series: [ ),
ScatterSeries( Padding(
enableTooltip: true, padding: EdgeInsets.fromLTRB(graphStartX+8, padding.top/2+8, 0, 0),
dataSource: _spots, child: Column(
animationDuration: 0, children: [
pointColorMapper: (data, _) => data.color, 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)),
xValueMapper: (data, _) => data.x, 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)),
yValueMapper: (data, _) => data.y, ],
onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: _spots[point.pointIndex!].nickname), maintainState: false)), ),
) )
], ],
), ),
), ),
),
)) ))
else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))
], ],
@ -318,9 +424,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
checkColor: Colors.black, checkColor: Colors.black,
onChanged: ((value) { onChanged: ((value) {
_reversed = value!; _reversed = value!;
setState(() { setState(() {});
they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
});
}), }),
), ),
), ),
@ -337,9 +441,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
value: _country, value: _country,
onChanged: ((value) { onChanged: ((value) {
_country = value; _country = value;
setState(() { setState(() {});
they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
});
}), }),
), ),
], ],
@ -354,9 +456,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
bool bigScreen = MediaQuery.of(context).size.width > 768; bool bigScreen = MediaQuery.of(context).size.width > 768;
return ListTile( return ListTile(
title: Text(they[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")), title: Text(they[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
subtitle: Text( 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]}"),
_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( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -535,12 +635,10 @@ class _ListEntry extends StatelessWidget {
} }
} }
class _MyScatterSpot{ class _MyScatterSpot extends ScatterSpot {
num x;
num y;
String id; String id;
String nickname; String nickname;
String rank; //Color color;
Color color; //FlDotPainter painter = FlDotCirclePainter(color: color, radius: 2);
_MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); _MyScatterSpot(super.x, super.y, this.id, this.nickname, {super.dotPainter});
} }

View File

@ -55,7 +55,7 @@ class RanksAverages extends State<RankAveragesView> {
leading: Image.asset("res/tetrio_tl_alpha_ranks/${keys[index]}.png", height: 48), 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")), title: Text(t.players(n: averages[keys[index]]?[1]["players"]), style: const TextStyle(fontFamily: "Eurostile Round Extended")),
subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM", subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM",
style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null),
onTap: (){ onTap: (){
if (averages[keys[index]]?[1]["players"] > 0) { if (averages[keys[index]]?[1]["players"] > 0) {

View File

@ -48,7 +48,11 @@ class SettingsState extends State<SettingsView> {
Future<void> _getPreferences() async { Future<void> _getPreferences() async {
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
showPositions = prefs.getBool("showPositions") ?? false; if (prefs.getBool("showPositions") != null) {
showPositions = prefs.getBool("showPositions")!;
} else {
showPositions = false;
}
_setDefaultNickname(prefs.getString("player")); _setDefaultNickname(prefs.getString("player"));
} }
@ -72,7 +76,7 @@ class SettingsState extends State<SettingsView> {
Future<void> _removePlayer() async { Future<void> _removePlayer() async {
await prefs.remove('player'); await prefs.remove('player');
await _setDefaultNickname("6098518e3d5155e6ec429cdc"); await _setDefaultNickname("dan63047");
} }
@override @override
@ -93,7 +97,7 @@ class SettingsState extends State<SettingsView> {
children: [ children: [
ListTile( ListTile(
title: Text(t.exportDB), title: Text(t.exportDB),
subtitle: Text(t.exportDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), subtitle: Text(t.exportDBDescription),
onTap: () { onTap: () {
if (kIsWeb){ if (kIsWeb){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb)));
@ -147,7 +151,7 @@ class SettingsState extends State<SettingsView> {
), ),
ListTile( ListTile(
title: Text(t.importDB), title: Text(t.importDB),
subtitle: Text(t.importDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), subtitle: Text(t.importDBDescription),
onTap: () { onTap: () {
if (kIsWeb){ if (kIsWeb){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb)));
@ -258,13 +262,13 @@ class SettingsState extends State<SettingsView> {
), ),
), ),
ListTile(title: Text(t.customization), ListTile(title: Text(t.customization),
subtitle: Text(t.customizationDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), subtitle: Text(t.customizationDescription),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
context.go("/customization"); context.go("/customization");
},), },),
ListTile(title: Text(t.lbStats), ListTile(title: Text(t.lbStats),
subtitle: Text(t.lbStatsDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), subtitle: Text(t.lbStatsDescription),
trailing: Switch(value: showPositions, onChanged: (bool value){ trailing: Switch(value: showPositions, onChanged: (bool value){
prefs.setBool("showPositions", value); prefs.setBool("showPositions", value);
setState(() { setState(() {
@ -276,7 +280,7 @@ class SettingsState extends State<SettingsView> {
onTap: (){ onTap: (){
launchInBrowser(Uri.https("github.com", "dan63047/TetraStats")); launchInBrowser(Uri.https("github.com", "dan63047/TetraStats"));
}, },
title: Text(t.aboutApp, style: const TextStyle(fontWeight: FontWeight.w500),), title: Text(t.aboutApp),
subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)), subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)),
trailing: const Icon(Icons.arrow_right) trailing: const Icon(Icons.arrow_right)
), ),

View File

@ -1,102 +0,0 @@
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<StatefulWidget> createState() => SprintAndBlitzState();
}
class SprintAndBlitzState extends State<SprintAndBlitzView> {
@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<String, Duration> 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))))
],
),
),
),
],
)),
);
}
}

View File

@ -75,7 +75,6 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
case ConnectionState.done: case ConnectionState.done:
final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); 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)}"); 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( return NestedScrollView(
headerSliverBuilder: (context, value) { headerSliverBuilder: (context, value) {
String howManyPlayers(int numberOfPlayers) => Intl.plural( String howManyPlayers(int numberOfPlayers) => Intl.plural(
@ -171,26 +170,18 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
}, },
body: ListView.builder( body: ListView.builder(
itemCount: allPlayers!.length, 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) { itemBuilder: (context, index) {
bool bigScreen = MediaQuery.of(context).size.width > 768;
return ListTile( return ListTile(
leading: Text( leading: Text((index+1).toString(), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : null),
(index+1).toString(), title: Text(allPlayers[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
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]}", 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", fontSize: bigScreen ? null : 12, color: _sortBy == Stats.tr ? Colors.grey : null)), style: TextStyle(fontFamily: "Eurostile Round Condensed", color: _sortBy == Stats.tr ? Colors.grey : null)),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text("${f2.format(allPlayers[index].rating)} TR", style: TextStyle(fontSize: bigScreen ? 28 : 22)), 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 : 36), Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 16),
], ],
), ),
onTap: () { onTap: () {

View File

@ -1,6 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy; import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy;
@ -61,11 +62,8 @@ class TlMatchResultState extends State<TlMatchResultView> {
super.dispose(); super.dispose();
} }
Widget buildComparison(double width, bool showMobileSelector){ Widget buildComparison(bool bigScreen, bool showMobileSelector){
bool bigScreen = width >= 768; return FutureBuilder(future: replayData, builder: (context, snapshot){
return SizedBox(
width: width,
child: FutureBuilder(future: replayData, builder: (context, snapshot){
late Duration time; late Duration time;
late String readableTime; late String readableTime;
late String reason; late String reason;
@ -231,12 +229,12 @@ class TlMatchResultState extends State<TlMatchResultView> {
roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][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 : redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kpp :
roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp,
label: "KPP", higherIsBetter: false, fractionDigits: 2,), label: "KpP", higherIsBetter: false, fractionDigits: 2,),
CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kps : CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kps :
roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][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 : redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kps :
roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps,
label: "KPS", higherIsBetter: true, fractionDigits: 2,), label: "KpS", higherIsBetter: true, fractionDigits: 2,),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, 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, redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared,
label: "Lines Cleared", higherIsBetter: true), label: "Lines Cleared", higherIsBetter: true),
@ -247,7 +245,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][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 : redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].spp :
roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp,
label: "SPP", higherIsBetter: true, fractionDigits: 2,), label: "SpP", higherIsBetter: true, fractionDigits: 2,),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, 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, redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100,
label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true),
@ -479,8 +477,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
], ],
) )
); );
}), });
);
} }
Widget buildRoundSelector(double width){ Widget buildRoundSelector(double width){
@ -687,17 +684,21 @@ class TlMatchResultState extends State<TlMatchResultView> {
return Center( return Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 768), constraints: const BoxConstraints(maxWidth: 768),
child: buildComparison(viewportWidth, true) child: buildComparison(viewportWidth > 768, true)
), ),
); );
} else { } else {
double comparisonWidth = viewportWidth - 450 - 16;
comparisonWidth = comparisonWidth > 768 ? 768 : comparisonWidth;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
buildComparison(comparisonWidth, false), SizedBox(
buildRoundSelector(450) width: 768,
child: buildComparison(true, false)
),
Container(
constraints: const BoxConstraints(maxWidth: 768),
child: buildRoundSelector(max(viewportWidth-768-16, 200)),
)
], ],
); );
} }

View File

@ -21,17 +21,16 @@ class FinesseThingy extends StatelessWidget{
return Stack( return Stack(
alignment: AlignmentDirectional.bottomStart, alignment: AlignmentDirectional.bottomStart,
children: [ children: [
const Text("f", style: TextStyle( Text("f", style: TextStyle(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
fontSize: 65, fontSize: 65,
height: 1.2, height: 1.2,
)), )),
const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), Positioned(child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended")), left: 25, top: 20),
Positioned( Positioned(
right: 0, top: 20,
child: Text("${finesse != null ? finesse!.faults : "---"}F", style: TextStyle( child: Text("${finesse != null ? finesse!.faults : "---"}F", style: TextStyle(
color: getFinesseColor() color: getFinesseColor()
))), )), right: 0, top: 20),
Padding( Padding(
padding: const EdgeInsets.only(left: 10.0), padding: const EdgeInsets.only(left: 10.0),
child: Text("${finesse != null ? f2.format(finessePercentage! * 100) : "---.--"}%", style: TextStyle( child: Text("${finesse != null ? f2.format(finessePercentage! * 100) : "---.--"}%", style: TextStyle(

View File

@ -4,6 +4,7 @@ import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/colors_functions.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/widgets/tl_thingy.dart';
class GaugetNum extends StatelessWidget { class GaugetNum extends StatelessWidget {
final num playerStat; final num playerStat;
@ -97,7 +98,7 @@ class GaugetNum extends StatelessWidget {
oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent : oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent :
oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent
),), ),),
if (oldPlayerStat != null && pos != null) const TextSpan(text: ""), if ((oldTl != null && oldTl!.gamesPlayed > 0) && pos != null) const TextSpan(text: ""),
if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "${pos!.position}", style: TextStyle(color: getColorOfRank(pos!.position))) if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "${pos!.position}", style: TextStyle(color: getColorOfRank(pos!.position)))
] ]
), ),

View File

@ -1,7 +1,6 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
class Graphs extends StatelessWidget{ class Graphs extends StatelessWidget{
@ -21,10 +20,6 @@ class Graphs extends StatelessWidget{
@override @override
Widget build(BuildContext context) { 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( return Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
@ -32,7 +27,7 @@ class Graphs extends StatelessWidget{
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
children: [ children: [
if (true) Padding( // vs graph Padding(
padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), padding: const EdgeInsets.fromLTRB(18, 0, 18, 44),
child: SizedBox( child: SizedBox(
height: 310, height: 310,
@ -91,7 +86,7 @@ class Graphs extends StatelessWidget{
borderColor: Colors.transparent, borderColor: Colors.transparent,
dataEntries: [ 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),
const RadarEntry(value: 0), const RadarEntry(value: 0),
const RadarEntry(value: 0), const RadarEntry(value: 0),
@ -109,7 +104,7 @@ class Graphs extends StatelessWidget{
), ),
), ),
), ),
Padding( // psq graph Padding(
padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), padding: const EdgeInsets.fromLTRB(18, 0, 18, 44),
child: SizedBox( child: SizedBox(
height: 310, height: 310,
@ -131,7 +126,7 @@ class Graphs extends StatelessWidget{
case 1: case 1:
return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05);
case 2: 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: case 3:
return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05);
default: default:
@ -152,9 +147,19 @@ class Graphs extends StatelessWidget{
borderColor: Colors.transparent, borderColor: Colors.transparent,
dataEntries: [ dataEntries: [
const RadarEntry(value: 0), const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
],
),
RadarDataSet(
fillColor: Colors.transparent,
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 1),
const RadarEntry(value: 1),
const RadarEntry(value: 1),
const RadarEntry(value: 1), const RadarEntry(value: 1),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
], ],
) )
], ],
@ -164,59 +169,6 @@ 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),
],
)
],
)
)
)
)
], ],
); );
} }

View File

@ -1,102 +0,0 @@
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
)
]
),
);
}
}

View File

@ -9,11 +9,14 @@ import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/gauget_num.dart';
import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/graphs.dart';
import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart';
import 'package:tetra_stats/widgets/tl_progress_bar.dart';
var fDiff = NumberFormat("+#,###.###;-#,###.###"); var fDiff = NumberFormat("+#,###.###;-#,###.###");
var intFDiff = NumberFormat("+#,###;-#,###"); var intFDiff = NumberFormat("+#,###;-#,###");
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
late RangeValues _currentRangeValues;
TetraLeagueAlpha? oldTl;
late TetraLeagueAlpha currentTl;
late List<TetrioPlayer> sortedStates;
class TLThingy extends StatefulWidget { class TLThingy extends StatefulWidget {
final TetraLeagueAlpha tl; final TetraLeagueAlpha tl;
@ -26,12 +29,10 @@ class TLThingy extends StatefulWidget {
final PlayerLeaderboardPosition? lbPositions; final PlayerLeaderboardPosition? lbPositions;
final TetraLeagueAlpha? averages; final TetraLeagueAlpha? averages;
final double? thatRankCutoff; final double? thatRankCutoff;
final double? thatRankCutoffGlicko;
final double? thatRankTarget; final double? thatRankTarget;
final double? nextRankCutoff; final double? nextRankCutoff;
final double? nextRankCutoffGlicko;
final double? nextRankTarget; 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, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget}); const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff = 25000, this.thatRankCutoff = 0, this.nextRankTarget = 25000, this.thatRankTarget = 0});
@override @override
State<TLThingy> createState() => _TLThingyState(); State<TLThingy> createState() => _TLThingyState();
@ -39,17 +40,17 @@ class TLThingy extends StatefulWidget {
class _TLThingyState extends State<TLThingy> { class _TLThingyState extends State<TLThingy> {
late bool oskKagariGimmick; late bool oskKagariGimmick;
late TetraLeagueAlpha? oldTl;
late TetraLeagueAlpha currentTl;
late RangeValues _currentRangeValues;
late List<TetrioPlayer> sortedStates;
@override @override
void initState() { void initState() {
_currentRangeValues = const RangeValues(0, 1); _currentRangeValues = const RangeValues(0, 1);
sortedStates = widget.states.reversed.toList(); sortedStates = widget.states.reversed.toList();
oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true;
oldTl = sortedStates.elementAtOrNull(1)?.tlSeason1; try{
oldTl = sortedStates[1].tlSeason1;
}on RangeError{
oldTl = null;
}
currentTl = widget.tl; currentTl = widget.tl;
super.initState(); super.initState();
} }
@ -138,16 +139,20 @@ class _TLThingyState extends State<TLThingy> {
), ),
], ],
), ),
if (currentTl.gamesPlayed > 9) TLProgress( if (currentTl.gamesPlayed >= 10 && currentTl.rd! < 100 && currentTl.nextAt >=0 && currentTl.prevAt >= 0) Padding(
tlData: currentTl, padding: const EdgeInsets.all(8.0),
previousRankTRcutoff: widget.thatRankCutoff, child: SfLinearGauge(
previousGlickoCutoff: widget.thatRankCutoffGlicko, minimum: currentTl.nextAt.toDouble(),
previousRank: widget.tl.prevRank, maximum: currentTl.prevAt.toDouble(),
previousRankTRcutoffTarget: widget.thatRankTarget, interval: currentTl.prevAt.toDouble() - currentTl.nextAt.toDouble(),
nextRankTRcutoff: widget.nextRankCutoff, ranges: [LinearGaugeRange(startValue: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() : currentTl.prevAt.toDouble(), endValue: currentTl.prevAt.toDouble(), color: Colors.cyanAccent,)],
nextRankGlickoCutoff: widget.nextRankCutoffGlicko, 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),
nextRankTRcutoffTarget: widget.nextRankTarget, 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)))],
nextRank: widget.tl.nextRank isAxisInversed: true,
isMirrored: true,
showTicks: true,
showLabels: true
),
), ),
if (currentTl.gamesPlayed < 10) if (currentTl.gamesPlayed < 10)
Text(t.gamesUntilRanked(left: 10 - currentTl.gamesPlayed), Text(t.gamesUntilRanked(left: 10 - currentTl.gamesPlayed),
@ -320,7 +325,7 @@ class _TLThingyState extends State<TLThingy> {
),), ),),
if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: ""), 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) 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 || oldTl?.estTr?.esttr != null) const TextSpan(text: ""), if (widget.lbPositions?.estTr != null) const TextSpan(text: ""),
TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}")
] ]
), ),

View File

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart';
@ -53,7 +52,7 @@ class UserThingy extends StatelessWidget {
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
children: [ children: [
if (player.bannerRevision != null) if (player.bannerRevision != null)
Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", Image.network("https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}",
fit: BoxFit.cover, fit: BoxFit.cover,
height: bannerHeight, height: bannerHeight,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
@ -91,7 +90,7 @@ class UserThingy extends StatelessWidget {
child: player.role == "banned" child: player.role == "banned"
? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,)
: player.avatarRevision != null : player.avatarRevision != null
? Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", ? Image.network("https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}",
// TODO: osk banner can cause memory leak // TODO: osk banner can cause memory leak
fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) {
developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace); developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace);
@ -411,7 +410,7 @@ class UserThingy extends StatelessWidget {
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace); developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace);
return Image.network( return Image.network(
kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png", "https://tetr.io/res/badges/${badge.badgeId}.png",
height: 32, height: 32,
width: 32, width: 32,
errorBuilder:(context, error, stackTrace) { errorBuilder:(context, error, stackTrace) {

View File

@ -1,7 +1,6 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
class VsGraphs extends StatelessWidget{ class VsGraphs extends StatelessWidget{
final double greenAPM; final double greenAPM;
@ -206,71 +205,6 @@ 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),
],
)
],
)
)
)
)
], ],
); );
} }

View File

@ -866,14 +866,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" 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: syncfusion_flutter_core:
dependency: transitive dependency: transitive
description: description:

View File

@ -2,7 +2,7 @@ name: tetra_stats
description: Track your and other player stats in TETR.IO description: Track your and other player stats in TETR.IO
publish_to: 'none' publish_to: 'none'
version: 1.5.3+19 version: 1.5.0+16
environment: environment:
sdk: '>=3.0.0' sdk: '>=3.0.0'
@ -43,7 +43,6 @@ dependencies:
flutter_markdown: ^0.6.18 flutter_markdown: ^0.6.18
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
go_router: ^13.0.0 go_router: ^13.0.0
syncfusion_flutter_charts: ^24.2.9
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -81,12 +81,7 @@
"verdictGeneral": "$n $verdict than $rank rank average", "verdictGeneral": "$n $verdict than $rank rank average",
"verdictBetter": "better", "verdictBetter": "better",
"verdictWorse": "worse", "verdictWorse": "worse",
"smooth": "Smooth",
"gamesUntilRanked": "${left} games until being ranked", "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", "nerdStats": "Nerd Stats",
"playersYouTrack": "Players you track", "playersYouTrack": "Players you track",
"formula": "Formula", "formula": "Formula",
@ -168,9 +163,6 @@
"calc": "Calc", "calc": "Calc",
"calcViewNoValues": "Enter values to calculate the stats", "calcViewNoValues": "Enter values to calculate the stats",
"rankAveragesViewTitle": "Ranks cutoff and average stats", "rankAveragesViewTitle": "Ranks cutoff and average stats",
"sprintAndBlitsViewTitle": "40 lines and Blitz averages",
"sprintAndBlitsRelevance": "Relevance: ${date}",
"rank": "Rank",
"averages": "Averages", "averages": "Averages",
"lbViewZeroEntrys": "Empty list", "lbViewZeroEntrys": "Empty list",
"lbViewOneEntry": "There is only one player", "lbViewOneEntry": "There is only one player",
@ -208,12 +200,6 @@
"currentAxis": "$axis axis:", "currentAxis": "$axis axis:",
"p1nkl0bst3rAlert": "That data was retrived from third party API maintained by p1nkl0bst3r", "p1nkl0bst3rAlert": "That data was retrived from third party API maintained by p1nkl0bst3r",
"notForWeb": "Function is not available for web version", "notForWeb": "Function is not available for web version",
"graphs": {
"attack": "Attack",
"speed": "Speed",
"defense": "Defense",
"cheese": "Cheese"
},
"statCellNum":{ "statCellNum":{
"xpLevel": "XP Level", "xpLevel": "XP Level",
"xpProgress": "Progress to next level", "xpProgress": "Progress to next level",
@ -264,7 +250,7 @@
"nyaappDescription": "(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.", "nyaappDescription": "(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.",
"area": "Area", "area": "Area",
"areaDescription": "How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections", "areaDescription": "How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections",
"estOfTR": "Estimated TR", "estOfTR": "Est. of TR",
"estOfTRShort": "Est. TR", "estOfTRShort": "Est. TR",
"accOfEst": "Accuracy", "accOfEst": "Accuracy",
"accOfEstShort": "Acc." "accOfEstShort": "Acc."
@ -326,16 +312,16 @@
"forbiddenSub": "If you are using VPN or Proxy, turn it off. If this does not help, reach out to $nickname", "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.", "tooManyRequests": "You have been rate limited.",
"tooManyRequestsSub": "Wait a few moments and try again", "tooManyRequestsSub": "Wait a few moments and try again",
"internal": "Something happened on the tetr.io side", "internal": "Something happend on the tetr.io side",
"internalSub": "osk, probably, already aware about it", "internalSub": "osk, probably, already aware about it",
"internalWebVersion": "Something happened on the tetr.io side (or on oskware_bridge, idk honestly)", "internalWebVersion": "Something happend 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", "internalWebVersionSub": "If osk status page says that everything is ok, let dan63047 know about this issue",
"oskwareBridge": "Something happened with oskware_bridge", "oskwareBridge": "Something happend with oskware_bridge",
"oskwareBridgeSub": "Let dan63047 know", "oskwareBridgeSub": "Let dan63047 know",
"p1nkl0bst3rForbidden": "Third party API blocked your IP address", "p1nkl0bst3rForbidden": "Third party API blocked your IP address",
"p1nkl0bst3rTooManyRequests": "Too many requests to third party API. Try again later", "p1nkl0bst3rTooManyRequests": "Too many requests to third party API. Try again later",
"p1nkl0bst3rinternal": "Something happened on the p1nkl0bst3r side", "p1nkl0bst3rinternal": "Something happend on the p1nkl0bst3r side",
"p1nkl0bst3rinternalWebVersion": "Something happened on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)", "p1nkl0bst3rinternalWebVersion": "Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)",
"replayAlreadySaved": "Replay already saved", "replayAlreadySaved": "Replay already saved",
"replayExpired": "Replay expired and not available anymore", "replayExpired": "Replay expired and not available anymore",
"replayRejected": "Third party API blocked your IP address" "replayRejected": "Third party API blocked your IP address"

View File

@ -81,12 +81,7 @@
"verdictGeneral": "$verdict среднего $rank ранга на $n", "verdictGeneral": "$verdict среднего $rank ранга на $n",
"verdictBetter": "Лучше", "verdictBetter": "Лучше",
"verdictWorse": "Хуже", "verdictWorse": "Хуже",
"smooth": "Гладкий",
"gamesUntilRanked": "${left} матчей до получения рейтинга", "gamesUntilRanked": "${left} матчей до получения рейтинга",
"numOfVictories": "~${wins} побед",
"promotionOnNextWin": "Повышение после следующей победы",
"numOfdefeats": "~${losses} поражений",
"demotionOnNextLoss": "Понижение после следующего поражения",
"nerdStats": "Для задротов", "nerdStats": "Для задротов",
"playersYouTrack": "Отслеживаемые игроки", "playersYouTrack": "Отслеживаемые игроки",
"formula": "Формула", "formula": "Формула",
@ -168,9 +163,6 @@
"calc": "Считать", "calc": "Считать",
"calcViewNoValues": "Введите значения, чтобы посчитать статистику", "calcViewNoValues": "Введите значения, чтобы посчитать статистику",
"rankAveragesViewTitle": "Требования рангов и средние значения", "rankAveragesViewTitle": "Требования рангов и средние значения",
"sprintAndBlitsViewTitle": "Средние результаты 40 линий и блица",
"sprintAndBlitsRelevance": "Актуальность: ${date}",
"rank": "Ранг",
"averages": "Средние значения", "averages": "Средние значения",
"lbViewZeroEntrys": "Рейтинговая таблица пуста", "lbViewZeroEntrys": "Рейтинговая таблица пуста",
"lbViewOneEntry": "В рейтинговой таблице всего один игрок", "lbViewOneEntry": "В рейтинговой таблице всего один игрок",
@ -208,12 +200,6 @@
"currentAxis": "Ось $axis:", "currentAxis": "Ось $axis:",
"p1nkl0bst3rAlert": "Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r", "p1nkl0bst3rAlert": "Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r",
"notForWeb": "Функция недоступна для веб версии", "notForWeb": "Функция недоступна для веб версии",
"graphs": {
"attack": "Атака",
"speed": "Скорость",
"defense": "Защита",
"cheese": "Сыр"
},
"statCellNum": { "statCellNum": {
"xpLevel": "Уровень\nопыта", "xpLevel": "Уровень\nопыта",
"xpProgress": "Прогресс до следующего уровня", "xpProgress": "Прогресс до следующего уровня",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB