Merge pull request #90 from dan63047/master

sync master with stable
This commit is contained in:
dan63047 2024-05-29 23:06:49 +03:00 committed by GitHub
commit 0cdeefe370
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1657 additions and 1144 deletions

View File

@ -0,0 +1,147 @@
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,6 +17,9 @@ const double appdspWeight = 140;
const double vsapmWeight = 60;
const double cheeseWeight = 1.25;
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 = {
"x": 0.01,
"u": 0.05,
@ -39,7 +42,7 @@ const Map<String, double> rankCutoffs = {
"": 0.5
};
const Map<String, double> rankTargets = {
"x": 24008,
"x": 24503.75, // where that comes from?
"u": 23038,
"ss": 21583,
"s+": 20128,
@ -138,45 +141,87 @@ const Map<String, Color> rankColors = { // thanks osk for const rankColors at ht
'z': Color(0xFF375433)
};
const Map<String, Duration> sprintAverages = { // based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098
'x': Duration(seconds: 25, milliseconds: 413),
'u': Duration(seconds: 34, milliseconds: 549),
'ss': Duration(seconds: 43, milliseconds: 373),
's+': Duration(seconds: 54, milliseconds: 027),
's': Duration(seconds: 60, milliseconds: 412),
's-': Duration(seconds: 67, milliseconds: 381),
'a+': Duration(seconds: 73, milliseconds: 694),
'a': Duration(seconds: 81, milliseconds: 166),
'a-': Duration(seconds: 88, milliseconds: 334),
'b+': Duration(seconds: 93, milliseconds: 741),
'b': Duration(seconds: 98, milliseconds: 354),
'b-': Duration(seconds: 109, milliseconds: 610),
'c+': Duration(seconds: 124, milliseconds: 641),
'c': Duration(seconds: 126, milliseconds: 104),
'c-': Duration(seconds: 145, milliseconds: 865),
'd+': Duration(seconds: 154, milliseconds: 338),
'd': Duration(seconds: 162, milliseconds: 063),
// const Map<String, Duration> sprintAverages = { // old data, based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098
// 'x': Duration(seconds: 25, milliseconds: 413),
// 'u': Duration(seconds: 34, milliseconds: 549),
// 'ss': Duration(seconds: 43, milliseconds: 373),
// 's+': Duration(seconds: 54, milliseconds: 027),
// 's': Duration(seconds: 60, milliseconds: 412),
// 's-': Duration(seconds: 67, milliseconds: 381),
// 'a+': Duration(seconds: 73, milliseconds: 694),
// 'a': Duration(seconds: 81, milliseconds: 166),
// 'a-': Duration(seconds: 88, milliseconds: 334),
// 'b+': Duration(seconds: 93, milliseconds: 741),
// 'b': Duration(seconds: 98, milliseconds: 354),
// 'b-': Duration(seconds: 109, milliseconds: 610),
// 'c+': Duration(seconds: 124, milliseconds: 641),
// 'c': Duration(seconds: 126, milliseconds: 104),
// 'c-': Duration(seconds: 145, milliseconds: 865),
// 'd+': Duration(seconds: 154, milliseconds: 338),
// 'd': Duration(seconds: 162, milliseconds: 063),
// //'z': Duration(seconds: 66, milliseconds: 802)
// };
// const Map<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)
};
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,
'x': 600715,
'u': 379418,
'ss': 233740,
's+': 158295,
's': 125164,
's-': 100933,
'a+': 83593,
'a': 68613,
'a-': 60219,
'b+': 51197,
'b': 44171,
'b-': 39045,
'c+': 34130,
'c': 28931,
'c-': 25095,
'd+': 22944,
'd': 20728,
//'z': 72084
};
@ -753,16 +798,15 @@ class EstTr {
srarea = (_apm * 0) + (_pps * 135) + (_vs * 0) + (_app * 290) + (_dss * 0) + (_dsp * 700) + (_gbe * 0);
statrank = 11.2 * atan((srarea - 93) / 130) + 1;
if (statrank <= 0) statrank = 0.001;
estglicko = (4.0867 * srarea + 186.68);
//estglicko = (4.0867 * srarea + 186.68);
double ntemp = _pps*(150+(((_vs/_apm) - 1.66)*35))+_app*290+_dsp*700;
estglicko = 0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4;
esttr = 25000 /
(
1 + pow(10, (
(
(
1500-(
0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4
)
1500-estglicko
)*pi
)/sqrt(
(
@ -1246,6 +1290,8 @@ class TetrioPlayersLeaderboard {
lb.removeWhere((element) => element.country != country);
}
lb.sort(((a, b) {
if (a.getStatByEnum(stat).isNaN) return 1;
if (b.getStatByEnum(stat).isNaN) return -1;
if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){
return reversed ? 1 : -1;
}else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){
@ -1257,20 +1303,6 @@ class TetrioPlayersLeaderboard {
return lb;
}
List<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
if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank");
List<TetrioPlayerFromLeaderboard> filtredLeaderboard = List.from(leaderboard);
@ -1853,11 +1885,12 @@ class TetrioPlayersLeaderboard {
"avgStride": avgStride,
"avgInfDS": avgInfDS,
"toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].rating : lowestTR,
"toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0,
"entries": filtredLeaderboard
}];
}else{
return [TetraLeagueAlpha(timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, rating: 0, rank: rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1),
{"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0}];
{"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0, "toEnterGlicko": 0}];
}
}
@ -1926,6 +1959,26 @@ class TetrioPlayersLeaderboard {
'd': getAverageOfRank("d")[1]["toEnterTR"]
};
Map<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) {
type = t;
timestamp = ts;

View File

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

View File

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

View File

@ -70,6 +70,7 @@ class TetrioService extends DB {
// I'm trying to send as less requests, as possible, so i'm caching the results of those requests.
// Usually those maps looks like this: {"cached_until_unix_milliseconds": Object}
// TODO: Make a proper caching system
final Map<String, TetrioPlayer> _playersCache = {};
final Map<String, Map<String, dynamic>> _recordsCache = {};
final Map<String, dynamic> _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]}
@ -77,6 +78,8 @@ class TetrioService extends DB {
final Map<String, PlayerLeaderboardPosition> _lbPositions = {};
final Map<String, List<News>> _newsCache = {};
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 = {};
/// Thing, that sends every request to the API endpoints
final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client());
@ -297,6 +300,119 @@ class TetrioService extends DB {
// Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above,
// so i'm going to document only unique differences between them
Future<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
/// (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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -7,10 +7,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:http/http.dart';
import 'package:intl/intl.dart';
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
@ -19,6 +18,7 @@ import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/ranks_averages_view.dart' show RankAveragesView;
import 'package:tetra_stats/views/sprint_and_blitz_averages.dart';
import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView;
import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView;
import 'package:tetra_stats/widgets/finesse_thingy.dart';
@ -35,6 +35,8 @@ import 'package:go_router/go_router.dart';
final TetrioService teto = TetrioService(); // thing, that manadge our local DB
int _chartsIndex = 0;
bool _gamesPlayedInsteadOfDateAndTime = false;
late ZoomPanBehavior _zoomPanBehavior;
bool _smooth = false;
List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"];
late ScrollController _scrollController;
final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode);
@ -81,11 +83,14 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
TetrioPlayersLeaderboard? everyone;
PlayerLeaderboardPosition? meAmongEveryone;
TetraLeagueAlpha? rankAverages;
double? thatRankCutoff;
double? nextRankCutoff;
double? thatRankGlickoCutoff;
double? nextRankGlickoCutoff;
String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for
String _titleNickname = "dan63047";
String _titleNickname = "";
/// Each dropdown menu item contains list of dots for the graph
var chartsData = <DropdownMenuItem<List<FlSpot>>>[];
var chartsDataGamesPlayed = <DropdownMenuItem<List<FlSpot>>>[];
List<DropdownMenuItem<List<_HistoryChartSpot>>> chartsData = [];
//var tableData = <TableRow>[];
final bodyGlobalKey = GlobalKey();
bool _showSearchBar = false;
@ -102,13 +107,18 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
_scrollController = ScrollController();
_tabController = TabController(length: 6, vsync: this);
_wideScreenTabController = TabController(length: 4, vsync: this);
_zoomPanBehavior = ZoomPanBehavior(
enablePinching: true,
enableSelectionZooming: true,
enableMouseWheelZooming : true,
enablePanning: true,
);
// We need to show something
if (widget.player != null){ // if we have user input,
changePlayer(widget.player!); // it's gonna be user input
}else{
_getPreferences() // otherwise, checking for preferences
.then((value) => changePlayer(prefs.getString("player") ?? "dan63047")); // no preferences - loading me
.then((value) => changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc")); // no preferences - loading me
}
super.initState();
}
@ -166,26 +176,41 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
late TetraLeagueAlphaStream tlStream;
late Map<String, dynamic> records;
late List<News> news;
late TetrioPlayerFromLeaderboard? topOne;
late double? topTR;
requests = await Future.wait([ // all at once
teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor),
prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=><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
]);
tlStream = requests[0] as TetraLeagueAlphaStream;
records = requests[1] as Map<String, dynamic>;
news = requests[2] as List<News>;
topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR
topOne = requests[4] as TetrioPlayerFromLeaderboard?;
topTR = requests.elementAtOrNull(5) as double?; // No TR - no Top TR
meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId);
if (meAmongEveryone == null && prefs.getBool("showPositions") == true){
if (prefs.getBool("showPositions") == true){
// Get tetra League leaderboard
everyone = teto.getCachedLeaderboard();
everyone ??= await teto.fetchTLLeaderboard();
if (meAmongEveryone == null){
meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me);
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];
@ -254,58 +279,30 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1);
}
// Also i need previous Tetra League State for comparison if avaliable
// tableData = [
// TableRow(children: [ Text("Date & Time"), Text("Tr"), Text("Glicko"), Text("RD"), Text("GP"), Text("GW"), Text("APM"), Text("PPS"), Text("VS"), Text("APP"), Text("VS/APM"), Text("DS/S"), Text("DS/P"), Text("APP+DS/P"), Text("Cheese"), Text("GbE"), Text("wAPP"), Text("Area"), Text("eTR"), Text("±eTR"), Text("Opener"), Text("Plonk"), Text("Inf. DS"), Text("Stride")],
// decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.white)))),
// for (var state in states) TableRow(children: [Text(dateFormat.format(state.tlSeason1.timestamp)), Text(f4.format(state.tlSeason1.rating)), Text(f4.format(state.tlSeason1.glicko)), Text(f4.format(state.tlSeason1.rd)), Text(f0.format(state.tlSeason1.gamesPlayed)), Text(f0.format(state.tlSeason1.gamesWon)), Text(f2.format(state.tlSeason1.apm)), Text(f2.format(state.tlSeason1.pps)), Text(state.tlSeason1.vs != null ? f2.format(state.tlSeason1.vs) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.app) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.vsapm) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.dss) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.dsp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.appdsp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.cheese) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.gbe) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.nyaapp) : "---"), Text(state.tlSeason1.nerdStats != null ? f4.format(state.tlSeason1.nerdStats?.area) : "---"), Text(state.tlSeason1.estTr != null ? f4.format(state.tlSeason1.estTr?.esttr) : "---"), Text(state.tlSeason1.esttracc != null ? f4.format(state.tlSeason1.esttracc) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.opener) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.plonk) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.infds) : "---"), Text(state.tlSeason1.playstyle != null ? f4.format(state.tlSeason1.playstyle?.stride) : "---")]),
// ];
if (uniqueTL.length >= 2){
compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2);
chartsData = <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.timestamp.millisecondsSinceEpoch.toDouble(), tl.rating)], child: Text(t.statCellNum.tr)),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.glicko!)], child: const Text("Glicko")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.rd!)], child: const Text("Rating Deviation")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.vsapm)], child: const Text("VS/APM")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.opener)], child: const Text("Opener")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.plonk)], child: const Text("Plonk")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.infds)], child: const Text("Inf. DS")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.playstyle!.stride)], child: const Text("Stride")),
];
chartsDataGamesPlayed = <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")),
chartsData = <DropdownMenuItem<List<_HistoryChartSpot>>>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rating)], child: Text(t.statCellNum.tr)),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")),
];
}else{
compareWith = null;
@ -465,6 +462,12 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
topTR: snapshot.data![7],
bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff,
thatRankCutoffGlicko: thatRankGlickoCutoff,
thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null,
nextRankCutoff: nextRankCutoff,
nextRankCutoffGlicko: nextRankGlickoCutoff,
nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null,
averages: rankAverages,
lbPositions: meAmongEveryone
),
@ -474,7 +477,7 @@ class _MainState extends State<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,)
),
],),
_History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,),
_OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
] : [
@ -485,11 +488,17 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
topTR: snapshot.data![7],
bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff,
thatRankCutoffGlicko: thatRankGlickoCutoff,
thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null,
nextRankCutoff: nextRankCutoff,
nextRankCutoffGlicko: nextRankGlickoCutoff,
nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null,
averages: rankAverages,
lbPositions: meAmongEveryone
),
_TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched),
_History(chartsData: chartsData, chartsDataGamesPlayed: chartsDataGamesPlayed, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank),
_RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank),
_OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
@ -541,7 +550,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
if (subText != null) Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18)),
child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center),
),
],
)
@ -624,7 +633,7 @@ class _NavDrawerState extends State<NavDrawer> {
leading: const Icon(Icons.home),
title: Text(homePlayerNickname),
onTap: () {
widget.changePlayer(prefs.getString("player") ?? "dan63047"); // changes player on main view to the one from preferences
widget.changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // changes player on main view to the one from preferences
Navigator.of(context).pop(); // and then NavDrawer closes itself.
},
),
@ -657,6 +666,20 @@ class _NavDrawerState extends State<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())
];
},
@ -752,8 +775,7 @@ class _TLRecords extends StatelessWidget {
}
class _History extends StatelessWidget{
final List<DropdownMenuItem<List<FlSpot>>> chartsData;
final List<DropdownMenuItem<List<FlSpot>>> chartsDataGamesPlayed;
final List<DropdownMenuItem<List<_HistoryChartSpot>>> chartsData;
final String userID;
final Function update;
final Function changePlayer;
@ -761,7 +783,7 @@ class _History extends StatelessWidget{
/// Widget, that can show history of some stat of the player on the graph.
/// Requires player [states], which is list of states and function [update], which rebuild widgets
const _History({required this.chartsData, required this.chartsDataGamesPlayed, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL});
const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL});
@override
Widget build(BuildContext context) {
@ -776,16 +798,16 @@ class _History extends StatelessWidget{
));
}
bool bigScreen = MediaQuery.of(context).size.width > 768;
//List<_HistoryChartSpot> selectedGraph = _gamesPlayedInsteadOfDateAndTime ? chartsDataGamesPlayed[_chartsIndex].value! : chartsData[_chartsIndex].value!;
List<_HistoryChartSpot> selectedGraph = chartsData[_chartsIndex].value!;
return SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
primary: true,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
spacing: 20,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
@ -815,9 +837,22 @@ 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))
],
),
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())
IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,)
],
),
if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: selectedGraph, smooth: _smooth, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact())
else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -828,13 +863,21 @@ 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{
final List<FlSpot> data;
final List<_HistoryChartSpot> data;
final bool smooth;
final String yAxisTitle;
final bool bigScreen;
final double leftSpace;
@ -844,7 +887,7 @@ class _HistoryChartThigy extends StatefulWidget{
/// Implements graph for the _History widget. Requires [data] which is a list of dots for the graph. [yAxisTitle] used to keep track of changes.
/// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets number format
/// for left titles
const _HistoryChartThigy({required this.data, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat});
const _HistoryChartThigy({required this.data, required this.smooth, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat});
@override
State<_HistoryChartThigy> createState() => _HistoryChartThigyState();
@ -853,292 +896,117 @@ class _HistoryChartThigy extends StatefulWidget{
class _HistoryChartThigyState extends State<_HistoryChartThigy> {
late String previousAxisTitle;
late bool previousGamesPlayedInsteadOfDateAndTime;
late double minX;
late double maxX;
late double minY;
late double actualMinY;
late double maxY;
late double actualMaxY;
late double xScale;
late double yScale;
String headerTooltip = t.pseudoTooltipHeaderInit;
String footerTooltip = t.pseudoTooltipFooterInit;
int hoveredPointId = -1;
double scaleFactor = 5e2;
double dragFactor = 7e2;
late TooltipBehavior _tooltipBehavior;
@override
void initState(){
super.initState();
minX = widget.data.first.x;
maxX = widget.data.last.x;
setMinMaxY();
_tooltipBehavior = TooltipBehavior(
color: Colors.black,
borderColor: Colors.white,
enable: true,
animationDuration: 0,
builder: (dynamic data, dynamic point, dynamic series,
int pointIndex, int seriesIndex) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"${f4.format(data.stat)} ${widget.yAxisTitle}",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20),
),
),
Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : _dateFormat.format(data.timestamp))
],
),
);
}
);
previousAxisTitle = widget.yAxisTitle;
previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime;
actualMaxY = maxY;
actualMinY = minY;
recalculateScales();
}
@override
void dispose(){
super.dispose();
actualMinY = 0;
minY = 0;
}
/// Calculates and assignes maximum and minimum values in list of dots
void setMinMaxY(){
actualMinY = widget.data.reduce((value, element){
num n = min(value.y, element.y);
if (value.y == n) {
return value;
} else {
return element;
}
}).y;
actualMaxY = widget.data.reduce((value, element){
num n = max(value.y, element.y);
if (value.y == n) {
return value;
} else {
return element;
}
}).y;
minY = actualMinY;
maxY = actualMaxY;
}
/// Calculates and assignes scales, which is difference between maximum and minimum visible axis value
void recalculateScales(){
xScale = maxX - minX;
yScale = maxY - minY;
}
/// Accepts [dragUpdDet] and changes minX, maxX, minY, maxY based on that
void dragHandler(DragUpdateDetails dragUpdDet){
setState(() {
// Changing min and max values according to drag delta and considering scales
minX -= (xScale / dragFactor) * dragUpdDet.delta.dx;
maxX -= (xScale / dragFactor) * dragUpdDet.delta.dx;
minY += (yScale / dragFactor) * dragUpdDet.delta.dy;
maxY += (yScale / dragFactor) * dragUpdDet.delta.dy;
// If values are out of bounds - putting them back
if (minX < widget.data.first.x) {
minX = widget.data.first.x;
maxX = widget.data.first.x + xScale;
}
if (maxX > widget.data.last.x) {
maxX = widget.data.last.x;
minX = maxX - xScale;
}
if(minY < actualMinY){
minY = actualMinY;
maxY = actualMinY + yScale;
}
if(maxY > actualMaxY){
maxY = actualMaxY;
minY = actualMaxY - yScale;
}
});
}
/// Accepts scale [details] and changes minX, maxX, minY, maxY in a way to change xScale and yScale.
/// [graphKey] required for sizes calculations, as well, as [graphStartX] and [graphEndX].
/// Not used yet, because GestureDetector works like shit
void scaleHandler(ScaleUpdateDetails details, GlobalKey<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
Widget build(BuildContext context) {
GlobalKey graphKey = GlobalKey();
if ((previousAxisTitle != widget.yAxisTitle) || (previousGamesPlayedInsteadOfDateAndTime != _gamesPlayedInsteadOfDateAndTime)) {
minX = widget.data.first.x;
maxX = widget.data.last.x;
recalculateScales();
setMinMaxY();
previousAxisTitle = widget.yAxisTitle;
previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime;
setState((){});
}
double xInterval = widget.bigScreen ? max(1, xScale / 8) : max(1, xScale / 4); // how far away xTitles should be between each other
EdgeInsets padding = widget.bigScreen ? const EdgeInsets.fromLTRB(40, 30, 40, 30) : const EdgeInsets.fromLTRB(0, 40, 16, 48);
double graphStartX = padding.left+widget.leftSpace;
double graphEndX = MediaQuery.sizeOf(context).width - padding.right;
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 104,
child: Padding( padding: padding,
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerSignal: (signal) {
if (signal is PointerScrollEvent) {
RenderBox graphBox = graphKey.currentContext?.findRenderObject() as RenderBox;
// calculating relative position of pointer
Offset graphPosition = graphBox.localToGlobal(Offset.zero);
// 0 - very left position of graph; 1 - very right position of graph
double scrollPosRelativeX = (signal.position.dx - graphStartX) / (graphEndX - graphStartX);
// 0 - very top position of graph; 1 - very bottom position of graph
double scrollPosRelativeY = (signal.position.dy - graphPosition.dy) / (graphBox.size.height - 30); // size - bottom titles height
double newMinX, newMaxX, newMinY, newMaxY; // calcutating new values based on pointer position and considering scales
newMinX = minX - (xScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeX;
newMaxX = maxX + (xScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeX);
newMinY = minY - (yScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeY);
newMaxY = maxY + (yScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeY;
// cancel changes if minimum is more, than maximun
if ((newMaxX - newMinX).isNegative) return;
if ((newMaxY - newMinY).isNegative) return;
// apply changes if everything ok + can't go past boundaries
setState(() {
minX = max(newMinX, widget.data.first.x);
maxX = min(newMaxX, widget.data.last.x);
minY = max(newMinY, actualMinY);
maxY = min(newMaxY, actualMaxY);
recalculateScales();
_scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView
});
}
},
child:
GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTap: () {
setState(() {
minX = widget.data.first.x;
maxX = widget.data.last.x;
minY = actualMinY;
maxY = actualMaxY;
recalculateScales();
});
},
// TODO: onScaleUpdate:(details) => scaleHandler(details, graphKey, graphStartX, graphEndX),
// TODO: Figure out wtf is going on with gestures
// TODO: Somehow highlight touched spot (handleBuiltInTouches breaks getTooltipItems and getTouchedSpotIndicator)
child: Padding( padding: padding,
child: Stack(
children: [
LineChart(
key: graphKey,
LineChartData(
lineBarsData: [LineChartBarData(spots: widget.data)],
clipData: const FlClipData.all(),
borderData: FlBorderData(show: false),
gridData: FlGridData(verticalInterval: xInterval),
minX: minX,
maxX: maxX,
minY: minY,
maxY: maxY,
titlesData: FlTitlesData(topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){
return value != meta.min && value != meta.max ? SideTitleWidget(
axisSide: meta.axisSide,
child: Text(widget.xFormat != null && _gamesPlayedInsteadOfDateAndTime ? widget.xFormat!.format(value.round()) : DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))),
) : Container();
})),
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: widget.leftSpace, getTitlesWidget: (double value, TitleMeta meta){
return value != meta.min && value != meta.max ? SideTitleWidget(
axisSide: meta.axisSide,
child: Text(widget.yFormat.format(value)),
) : Container();
}))),
lineTouchData: LineTouchData(
handleBuiltInTouches: false,
touchCallback:(touchEvent, touchResponse) {
if (touchEvent is FlPanUpdateEvent){
dragHandler(touchEvent.details);
return;
}
if (touchEvent is FlPointerHoverEvent){
setState(() {
if (touchResponse?.lineBarSpots?.first == null) {
hoveredPointId = -1; // not hovering over any point
} else {
hoveredPointId = touchResponse!.lineBarSpots!.first.spotIndex;
headerTooltip = "${f4.format(touchResponse.lineBarSpots!.first.y)} ${widget.yAxisTitle}";
footerTooltip = _gamesPlayedInsteadOfDateAndTime ? "${f0.format(touchResponse.lineBarSpots!.first.x)} games played" : _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(touchResponse.lineBarSpots!.first.x.floor()));
}
});
}
if (touchEvent is FlPointerExitEvent){
setState(() {hoveredPointId = -1;});
}
},
)
)
child: SfCartesianChart(
tooltipBehavior: _tooltipBehavior,
zoomPanBehavior: _zoomPanBehavior,
primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(),
primaryYAxis: const NumericAxis(
rangePadding: ChartRangePadding.additional,
),
Padding(
padding: EdgeInsets.only(left: widget.leftSpace),
child: Column(
children: [
AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24, color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(headerTooltip)),
AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round", color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(footerTooltip)),
series: <CartesianSeries>[
if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>(
enableTooltip: true,
// splineType: SplineType.cardinal,
// cardinalSplineTension: 0.2,
dataSource: widget.data,
animationDuration: 0,
opacity: _smooth ? 0 : 1,
xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed,
yValueMapper: (_HistoryChartSpot data, _) => data.stat,
trendlines:<Trendline>[
Trendline(
isVisible: _smooth,
period: (widget.data.length/175).floor(),
type: TrendlineType.movingAverage,
color: Colors.blue)
],
)
else StepLineSeries<_HistoryChartSpot, DateTime>(
enableTooltip: true,
// splineType: SplineType.cardinal,
// cardinalSplineTension: 0.2,
dataSource: widget.data,
animationDuration: 0,
opacity: _smooth ? 0 : 1,
xValueMapper: (_HistoryChartSpot data, _) => data.timestamp,
yValueMapper: (_HistoryChartSpot data, _) => data.stat,
trendlines:<Trendline>[
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 {
final RecordSingle? sprint;
final RecordSingle? blitz;
@ -1228,7 +1096,7 @@ class _TwoRecordsThingy extends StatelessWidget {
),
if (sprint != null) FinesseThingy(sprint?.endContext?.finesse, sprint?.endContext?.finessePercentage),
if (sprint != null) LineclearsThingy(sprint!.endContext!.clears, sprint!.endContext!.lines, sprint!.endContext!.holds, sprint!.endContext!.tSpins),
if (sprint != null) Text("${sprint!.endContext!.inputs} KP • ${f2.format(sprint!.endContext!.kps)} KpS")
if (sprint != null) Text("${sprint!.endContext!.inputs} KP • ${f2.format(sprint!.endContext!.kps)} KPS")
]
),
Column(
@ -1289,7 +1157,7 @@ class _TwoRecordsThingy extends StatelessWidget {
),
if (blitz != null) FinesseThingy(blitz?.endContext?.finesse, blitz?.endContext?.finessePercentage),
if (blitz != null) LineclearsThingy(blitz!.endContext!.clears, blitz!.endContext!.lines, blitz!.endContext!.holds, blitz!.endContext!.tSpins),
if (blitz != null) Text("${blitz!.endContext!.piecesPlaced} P • ${blitz!.endContext!.inputs} KP • ${f2.format(blitz!.endContext!.kpp)} KpP • ${f2.format(blitz!.endContext!.kps)} KpS")
if (blitz != null) Text("${blitz!.endContext!.piecesPlaced} P • ${blitz!.endContext!.inputs} KP • ${f2.format(blitz!.endContext!.kpp)} KPP • ${f2.format(blitz!.endContext!.kps)} KPS")
],
),
]),
@ -1405,8 +1273,8 @@ class _RecordThingy extends StatelessWidget {
),
FinesseThingy(record?.endContext?.finesse, record?.endContext?.finessePercentage),
LineclearsThingy(record!.endContext!.clears, record!.endContext!.lines, record!.endContext!.holds, record!.endContext!.tSpins),
if (record!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KpS"),
if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KpP • ${f2.format(record!.endContext!.kps)} KpS")
if (record!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KPS"),
if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KPP • ${f2.format(record!.endContext!.kps)} KPS")
]
),
),

View File

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

View File

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

View File

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

View File

@ -0,0 +1,102 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/main_view.dart';
import 'package:window_manager/window_manager.dart';
late String oldWindowTitle;
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode);
class SprintAndBlitzView extends StatefulWidget {
const SprintAndBlitzView({super.key});
@override
State<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,6 +75,7 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
case ConnectionState.done:
final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country);
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers!.length)}");
bool bigScreen = MediaQuery.of(context).size.width > 768;
return NestedScrollView(
headerSliverBuilder: (context, value) {
String howManyPlayers(int numberOfPlayers) => Intl.plural(
@ -170,18 +171,26 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
},
body: ListView.builder(
itemCount: allPlayers!.length,
prototypeItem: ListTile(
leading: Text("0", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)),
title: Text("ehhh...", style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)),
trailing: Container(height: bigScreen ? 48 : 36, width: 1,),
subtitle: Text("eh..."),
),
itemBuilder: (context, index) {
bool bigScreen = MediaQuery.of(context).size.width > 768;
return ListTile(
leading: Text((index+1).toString(), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : null),
title: Text(allPlayers[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
leading: Text(
(index+1).toString(),
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)
),
title: Text(allPlayers[index].username, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)),
subtitle: Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}",
style: TextStyle(fontFamily: "Eurostile Round Condensed", color: _sortBy == Stats.tr ? Colors.grey : null)),
style: TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: bigScreen ? null : 12, color: _sortBy == Stats.tr ? Colors.grey : null)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("${f2.format(allPlayers[index].rating)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null),
Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 16),
Text("${f2.format(allPlayers[index].rating)} TR", style: TextStyle(fontSize: bigScreen ? 28 : 22)),
Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36),
],
),
onTap: () {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
class Graphs extends StatelessWidget{
@ -20,6 +21,10 @@ class Graphs extends StatelessWidget{
@override
Widget build(BuildContext context) {
double attack = apm / 60 * 0.4;
double speed = pps / 3.75;
double defense = nerdStats.dss * 1.15;
double cheese = nerdStats.cheese / 110;
return Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.center,
@ -27,7 +32,7 @@ class Graphs extends StatelessWidget{
crossAxisAlignment: WrapCrossAlignment.start,
clipBehavior: Clip.hardEdge,
children: [
Padding(
if (true) Padding( // vs graph
padding: const EdgeInsets.fromLTRB(18, 0, 18, 44),
child: SizedBox(
height: 310,
@ -86,7 +91,7 @@ class Graphs extends StatelessWidget{
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 180),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
@ -104,7 +109,7 @@ class Graphs extends StatelessWidget{
),
),
),
Padding(
Padding( // psq graph
padding: const EdgeInsets.fromLTRB(18, 0, 18, 44),
child: SizedBox(
height: 310,
@ -126,7 +131,7 @@ class Graphs extends StatelessWidget{
case 1:
return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05);
case 2:
return RadarChartTitle(text: 'Inf Ds\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05);
return RadarChartTitle(text: 'Inf DS\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05);
case 3:
return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05);
default:
@ -147,19 +152,9 @@ class Graphs extends StatelessWidget{
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 0),
const RadarEntry(value: 1),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
],
),
RadarDataSet(
fillColor: Colors.transparent,
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 1),
const RadarEntry(value: 1),
const RadarEntry(value: 1),
const RadarEntry(value: 1),
],
)
],
@ -169,6 +164,59 @@ class Graphs extends StatelessWidget{
),
),
),
Padding( // sq graph
padding: const EdgeInsets.fromLTRB(18, 0, 18, 44),
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10),
radarBorderData: const BorderSide(color: Colors.transparent, width: 1),
gridBorderData: const BorderSide(color: Colors.white24, width: 1),
tickBorderData: const BorderSide(color: Colors.transparent, width: 1),
titleTextStyle: const TextStyle(height: 1.1),
radarTouchData: RadarTouchData(),
getTitle: (index, angle) {
switch (index) {
case 0:
return RadarChartTitle(text: '${t.graphs.attack}\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05);
case 1:
return RadarChartTitle(text: '${t.graphs.speed}\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05);
case 2:
return RadarChartTitle(text: '${t.graphs.defense}\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05);
case 3:
return RadarChartTitle(text: '${t.graphs.cheese}\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05);
default:
return const RadarChartTitle(text: '');
}
},
dataSets: [
RadarDataSet(
dataEntries: [
RadarEntry(value: attack),
RadarEntry(value: speed),
RadarEntry(value: defense),
RadarEntry(value: cheese),
],
),
RadarDataSet(
fillColor: Colors.transparent,
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 0),
const RadarEntry(value: 1.2),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
],
)
],
)
)
)
)
],
);
}

View File

@ -0,0 +1,102 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/data_objects/glicko.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
class TLProgress extends StatelessWidget{
final TetraLeagueAlpha tlData;
final String? nextRank;
final String? previousRank;
final double? nextRankTRcutoff;
final double? previousRankTRcutoff;
final double? nextRankGlickoCutoff;
final double? previousGlickoCutoff;
final double? nextRankTRcutoffTarget;
final double? previousRankTRcutoffTarget;
const TLProgress({super.key, required this.tlData, this.nextRank, this.previousRank, this.nextRankTRcutoff, this.previousRankTRcutoff, this.nextRankGlickoCutoff, this.previousGlickoCutoff, this.nextRankTRcutoffTarget, this.previousRankTRcutoffTarget});
double getBarPosition(){
return min(max(0, 1 - (tlData.standing - tlData.nextAt)/(tlData.prevAt - tlData.nextAt)), 1);
}
double? getBarTR(double tr){
return min(max(0, (tr - previousRankTRcutoff!)/(nextRankTRcutoff! - previousRankTRcutoff!)), 1);
}
@override
Widget build(BuildContext context) {
if (nextRank == null && previousRank == null && nextRankTRcutoff == null && previousRankTRcutoff == null && nextRankGlickoCutoff == null && previousGlickoCutoff == null && nextRankTRcutoffTarget == null && previousRankTRcutoffTarget == null) return Container();
final glickoForWin = rate(tlData.glicko!, tlData.rd!, 0.06, [[tlData.glicko!, tlData.rd!, 1]], {})[0]-tlData.glicko!;
return Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 48,
child: Stack(
alignment: AlignmentDirectional.bottomCenter,
fit: StackFit.expand,
children: [
Positioned(left: 0,
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12),
children: [
if (tlData.prevAt > 0) TextSpan(text: "${f0.format(tlData.prevAt)}"),
if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"),
if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"),
if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"),
if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null))
]
)
),
),
Positioned(right: 0,
child: RichText(
textAlign: TextAlign.right,
text: TextSpan(
style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12),
children: [
if (tlData.nextAt > 0) TextSpan(text: "${f0.format(tlData.nextAt)}"),
if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"),
if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"),
if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"),
if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null))
]
)
),
)
],),
),
SfLinearGauge(
minimum: 0,
maximum: 1,
interval: 1,
ranges: [
if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.rating)!, color: Colors.cyanAccent, position: LinearElementPosition.cross)
else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Colors.cyanAccent, position: LinearElementPosition.cross),
if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent, position: LinearElementPosition.inside),
if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside)
],
markerPointers: [
LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20),
if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), child: Text("${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),))
],
isMirrored: true,
showTicks: true,
showLabels: false
)
]
),
);
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
class VsGraphs extends StatelessWidget{
final double greenAPM;
@ -205,6 +206,71 @@ class VsGraphs extends StatelessWidget{
),
),
),
Padding( // sq graph
padding: const EdgeInsets.fromLTRB(18, 0, 18, 44),
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10),
radarBorderData: const BorderSide(color: Colors.transparent, width: 1),
gridBorderData: const BorderSide(color: Colors.white24, width: 1),
tickBorderData: const BorderSide(color: Colors.transparent, width: 1),
titleTextStyle: const TextStyle(height: 1.1),
radarTouchData: RadarTouchData(),
getTitle: (index, angle) {
switch (index) {
case 0:
return RadarChartTitle(text: t.graphs.attack, angle: 0, positionPercentageOffset: 0.05);
case 1:
return RadarChartTitle(text: t.graphs.speed, angle: 0, positionPercentageOffset: 0.05);
case 2:
return RadarChartTitle(text: t.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05);
case 3:
return RadarChartTitle(text: t.graphs.cheese, angle: 0, positionPercentageOffset: 0.05);
default:
return const RadarChartTitle(text: '');
}
},
dataSets: [
RadarDataSet(
fillColor: const Color.fromARGB(115, 76, 175, 79),
borderColor: Colors.green,
dataEntries: [
RadarEntry(value: greenAPM / 60 * 0.4),
RadarEntry(value: greenPPS / 3.75),
RadarEntry(value: greenNerdStats.dss * 1.15),
RadarEntry(value: greenNerdStats.cheese / 110),
],
),
RadarDataSet(
fillColor: const Color.fromARGB(115, 244, 67, 54),
borderColor: Colors.red,
dataEntries: [
RadarEntry(value: redAPM / 60 * 0.4),
RadarEntry(value: redPPS / 3.75),
RadarEntry(value: redNerdStats.dss * 1.15),
RadarEntry(value: redNerdStats.cheese / 110),
],
),
RadarDataSet(
fillColor: Colors.transparent,
borderColor: Colors.transparent,
dataEntries: [
const RadarEntry(value: 0),
const RadarEntry(value: 1.2),
const RadarEntry(value: 0),
const RadarEntry(value: 0),
],
)
],
)
)
)
)
],
);
}

View File

@ -866,6 +866,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
syncfusion_flutter_charts:
dependency: "direct main"
description:
name: syncfusion_flutter_charts
sha256: ab73109c586f5ec2b01adc2672026a1fb3f93b2b5f6061ba8d7126c119061002
url: "https://pub.dev"
source: hosted
version: "24.2.9"
syncfusion_flutter_core:
dependency: transitive
description:

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB