swamp water

This commit is contained in:
dan63047 2024-07-27 22:10:45 +03:00
parent 8f5b7c018d
commit b623693148
17 changed files with 1365 additions and 297 deletions

View File

@ -60,7 +60,8 @@ const Map<String, double> rankTargets = {
"d+": 606, "d+": 606,
"d": 0, "d": 0,
}; };
DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15); DateTime seasonStart = DateTime.utc(2024, 08, 16, 18);
//DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15);
enum Stats { enum Stats {
tr, tr,
glicko, glicko,
@ -261,9 +262,7 @@ class TetrioPlayer {
bool? badstanding; bool? badstanding;
String? botmaster; String? botmaster;
Connections? connections; Connections? connections;
late TetraLeagueAlpha tlSeason1; TetraLeagueAlpha? tlSeason1;
List<RecordSingle?> sprint = [];
List<RecordSingle?> blitz = [];
TetrioZen? zen; TetrioZen? zen;
Distinguishment? distinguishment; Distinguishment? distinguishment;
DateTime? cachedUntil; DateTime? cachedUntil;
@ -290,8 +289,6 @@ class TetrioPlayer {
this.botmaster, this.botmaster,
required this.connections, required this.connections,
required this.tlSeason1, required this.tlSeason1,
required this.sprint,
required this.blitz,
this.zen, this.zen,
this.distinguishment, this.distinguishment,
this.cachedUntil this.cachedUntil
@ -318,7 +315,7 @@ class TetrioPlayer {
country = json['country']; country = json['country'];
supporterTier = json['supporter_tier'] ?? 0; supporterTier = json['supporter_tier'] ?? 0;
verified = json['verified'] ?? false; verified = json['verified'] ?? false;
tlSeason1 = TetraLeagueAlpha.fromJson(json['league'], stateTime); tlSeason1 = json['league'] != null ? TetraLeagueAlpha.fromJson(json['league'], stateTime) : null;
avatarRevision = json['avatar_revision']; avatarRevision = json['avatar_revision'];
bannerRevision = json['banner_revision']; bannerRevision = json['banner_revision'];
bio = json['bio']; bio = json['bio'];
@ -344,7 +341,7 @@ class TetrioPlayer {
if (country != null) data['country'] = country; if (country != null) data['country'] = country;
if (supporterTier > 0) data['supporter_tier'] = supporterTier; if (supporterTier > 0) data['supporter_tier'] = supporterTier;
if (verified) data['verified'] = verified; if (verified) data['verified'] = verified;
data['league'] = tlSeason1.toJson(); data['league'] = tlSeason1?.toJson();
if (distinguishment != null) data['distinguishment'] = distinguishment?.toJson(); if (distinguishment != null) data['distinguishment'] = distinguishment?.toJson();
if (avatarRevision != null) data['avatar_revision'] = avatarRevision; if (avatarRevision != null) data['avatar_revision'] = avatarRevision;
if (bannerRevision != null) data['banner_revision'] = bannerRevision; if (bannerRevision != null) data['banner_revision'] = bannerRevision;
@ -380,12 +377,12 @@ class TetrioPlayer {
} }
bool checkForRetrivedHistory(covariant TetrioPlayer other) { bool checkForRetrivedHistory(covariant TetrioPlayer other) {
return tlSeason1.lessStrictCheck(other.tlSeason1); return tlSeason1!.lessStrictCheck(other.tlSeason1!);
} }
TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard() => TetrioPlayerFromLeaderboard( TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard() => TetrioPlayerFromLeaderboard(
userId, username, role, xp, country, supporterTier > 0, verified, state, gamesPlayed, gamesWon, userId, username, role, xp, country, verified, state, gamesPlayed, gamesWon,
tlSeason1.rating, tlSeason1.glicko??0, tlSeason1.rd??noTrRd, tlSeason1.rank, tlSeason1.bestRank, tlSeason1.apm??0, tlSeason1.pps??0, tlSeason1.vs??0, tlSeason1.decaying); tlSeason1!.rating, tlSeason1!.glicko??0, tlSeason1!.rd??noTrRd, tlSeason1!.rank, tlSeason1!.bestRank, tlSeason1!.apm??0, tlSeason1!.pps??0, tlSeason1!.vs??0, tlSeason1!.decaying);
@override @override
String toString() { String toString() {
@ -395,59 +392,59 @@ class TetrioPlayer {
num? getStatByEnum(Stats stat){ num? getStatByEnum(Stats stat){
switch (stat) { switch (stat) {
case Stats.tr: case Stats.tr:
return tlSeason1.rating; return tlSeason1?.rating;
case Stats.glicko: case Stats.glicko:
return tlSeason1.glicko; return tlSeason1?.glicko;
case Stats.rd: case Stats.rd:
return tlSeason1.rd; return tlSeason1?.rd;
case Stats.gp: case Stats.gp:
return tlSeason1.gamesPlayed; return tlSeason1?.gamesPlayed;
case Stats.gw: case Stats.gw:
return tlSeason1.gamesWon; return tlSeason1?.gamesWon;
case Stats.wr: case Stats.wr:
return tlSeason1.winrate; return tlSeason1?.winrate;
case Stats.apm: case Stats.apm:
return tlSeason1.apm; return tlSeason1?.apm;
case Stats.pps: case Stats.pps:
return tlSeason1.pps; return tlSeason1?.pps;
case Stats.vs: case Stats.vs:
return tlSeason1.vs; return tlSeason1?.vs;
case Stats.app: case Stats.app:
return tlSeason1.nerdStats?.app; return tlSeason1?.nerdStats?.app;
case Stats.dss: case Stats.dss:
return tlSeason1.nerdStats?.dss; return tlSeason1?.nerdStats?.dss;
case Stats.dsp: case Stats.dsp:
return tlSeason1.nerdStats?.dsp; return tlSeason1?.nerdStats?.dsp;
case Stats.appdsp: case Stats.appdsp:
return tlSeason1.nerdStats?.appdsp; return tlSeason1?.nerdStats?.appdsp;
case Stats.vsapm: case Stats.vsapm:
return tlSeason1.nerdStats?.vsapm; return tlSeason1?.nerdStats?.vsapm;
case Stats.cheese: case Stats.cheese:
return tlSeason1.nerdStats?.cheese; return tlSeason1?.nerdStats?.cheese;
case Stats.gbe: case Stats.gbe:
return tlSeason1.nerdStats?.gbe; return tlSeason1?.nerdStats?.gbe;
case Stats.nyaapp: case Stats.nyaapp:
return tlSeason1.nerdStats?.nyaapp; return tlSeason1?.nerdStats?.nyaapp;
case Stats.area: case Stats.area:
return tlSeason1.nerdStats?.area; return tlSeason1?.nerdStats?.area;
case Stats.eTR: case Stats.eTR:
return tlSeason1.estTr?.esttr; return tlSeason1?.estTr?.esttr;
case Stats.acceTR: case Stats.acceTR:
return tlSeason1.esttracc; return tlSeason1?.esttracc;
case Stats.acceTRabs: case Stats.acceTRabs:
return tlSeason1.esttracc?.abs(); return tlSeason1?.esttracc?.abs();
case Stats.opener: case Stats.opener:
return tlSeason1.playstyle?.opener; return tlSeason1?.playstyle?.opener;
case Stats.plonk: case Stats.plonk:
return tlSeason1.playstyle?.plonk; return tlSeason1?.playstyle?.plonk;
case Stats.infDS: case Stats.infDS:
return tlSeason1.playstyle?.infds; return tlSeason1?.playstyle?.infds;
case Stats.stride: case Stats.stride:
return tlSeason1.playstyle?.stride; return tlSeason1?.playstyle?.stride;
case Stats.stridemMinusPlonk: case Stats.stridemMinusPlonk:
return tlSeason1.playstyle != null ? tlSeason1.playstyle!.stride - tlSeason1.playstyle!.plonk : null; return tlSeason1?.playstyle != null ? tlSeason1!.playstyle!.stride - tlSeason1!.playstyle!.plonk : null;
case Stats.openerMinusInfDS: case Stats.openerMinusInfDS:
return tlSeason1.playstyle != null ? tlSeason1.playstyle!.opener - tlSeason1.playstyle!.infds : null; return tlSeason1?.playstyle != null ? tlSeason1!.playstyle!.opener - tlSeason1!.playstyle!.infds : null;
} }
} }
@ -458,6 +455,24 @@ class TetrioPlayer {
bool operator ==(covariant TetrioPlayer other) => isSameState(other) && state.isAtSameMomentAs(other.state); bool operator ==(covariant TetrioPlayer other) => isSameState(other) && state.isAtSameMomentAs(other.state);
} }
class Summaries{
late String id;
late RecordSingle sprint;
late RecordSingle blitz;
late TetraLeagueAlpha league;
late TetrioZen zen;
Summaries(this.id, this.league, this.zen);
Summaries.fromJson(Map<String, dynamic> json, String i){
id = i;
sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank']);
blitz = RecordSingle.fromJson(json['blitz']['record'], json['blitz']['rank']);
league = TetraLeagueAlpha.fromJson(json['league'], DateTime.now());
zen = TetrioZen.fromJson(json['zen']);
}
}
class Badge { class Badge {
late String badgeId; late String badgeId;
late String label; late String label;
@ -652,8 +667,7 @@ class Finesse {
} }
} }
class EndContextSingle { class ResultsStats {
late String gameType;
late int topBtB; late int topBtB;
late int topCombo; late int topCombo;
late int holds; late int holds;
@ -662,7 +676,7 @@ class EndContextSingle {
late int piecesPlaced; late int piecesPlaced;
late int lines; late int lines;
late int score; late int score;
late double seed; late int seed;
late Duration finalTime; late Duration finalTime;
late int tSpins; late int tSpins;
late Clears clears; late Clears clears;
@ -674,8 +688,8 @@ class EndContextSingle {
double get kps => inputs / (finalTime.inMicroseconds / 1000000); double get kps => inputs / (finalTime.inMicroseconds / 1000000);
double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0; double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0;
EndContextSingle( ResultsStats(
{required this.gameType, {
required this.topBtB, required this.topBtB,
required this.topCombo, required this.topCombo,
required this.holds, required this.holds,
@ -690,12 +704,12 @@ class EndContextSingle {
required this.clears, required this.clears,
required this.finesse}); required this.finesse});
EndContextSingle.fromJson(Map<String, dynamic> json) { ResultsStats.fromJson(Map<String, dynamic> json) {
seed = json['seed'].toDouble(); seed = json['seed'];
lines = json['lines']; lines = json['lines'];
inputs = json['inputs']; inputs = json['inputs'];
holds = json['holds'] ?? 0; holds = json['holds'] ?? 0;
finalTime = doubleMillisecondsToDuration(json['finalTime'].toDouble()); finalTime = doubleMillisecondsToDuration(json['finaltime'].toDouble());
score = json['score']; score = json['score'];
level = json['level']; level = json['level'];
topCombo = json['topcombo']; topCombo = json['topcombo'];
@ -704,7 +718,6 @@ class EndContextSingle {
piecesPlaced = json['piecesplaced']; piecesPlaced = json['piecesplaced'];
clears = Clears.fromJson(json['clears']); clears = Clears.fromJson(json['clears']);
finesse = json.containsKey("finesse") ? Finesse.fromJson(json['finesse']) : null; finesse = json.containsKey("finesse") ? Finesse.fromJson(json['finesse']) : null;
gameType = json['gametype'];
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -722,7 +735,6 @@ class EndContextSingle {
data['clears'] = clears.toJson(); data['clears'] = clears.toJson();
if (finesse != null) data['finesse'] = finesse!.toJson(); if (finesse != null) data['finesse'] = finesse!.toJson();
data['finalTime'] = finalTime; data['finalTime'] = finalTime;
data['gametype'] = gameType;
return data; return data;
} }
} }
@ -873,6 +885,119 @@ class TetraLeagueAlphaStream{
} }
} }
class TetraLeagueBetaStream{
late String id;
List<BetaRecord> records = [];
TetraLeagueBetaStream({required this.id, required this.records});
TetraLeagueBetaStream.fromJson(List<dynamic> json, String userID) {
id = userID;
for (var entry in json) records.add(BetaRecord.fromJson(entry));
}
addFromAlphaStream(TetraLeagueAlphaStream oldStream){
for (var entry in oldStream.records) {
records.add(
BetaRecord(
id: entry.ownId,
replayID: entry.replayId,
ts: entry.timestamp,
enemyID: entry.endContext[1].userId,
enemyUsername: entry.endContext[1].username,
gamemode: "oldleague",
results: BetaLeagueResults(
leaderboard: [
BetaLeagueLeaderboardEntry(
id: entry.endContext[0].userId,
username: entry.endContext[0].username,
naturalorder: entry.endContext[0].naturalOrder,
wins: entry.endContext[0].points,
stats: BetaLeagueStats(
apm: entry.endContext[0].secondary,
pps: entry.endContext[0].tertiary,
vs: entry.endContext[0].extra,
garbageSent: -1,
garbageReceived: -1,
kills: entry.endContext[0].points,
altitude: 0.0,
rank: -1,
nerdStats: entry.endContext[0].nerdStats,
playstyle: entry.endContext[0].playstyle,
estTr: entry.endContext[0].estTr,
)
),
BetaLeagueLeaderboardEntry(
id: entry.endContext[1].userId,
username: entry.endContext[1].username,
naturalorder: entry.endContext[1].naturalOrder,
wins: entry.endContext[1].points,
stats: BetaLeagueStats(
apm: entry.endContext[1].secondary,
pps: entry.endContext[1].tertiary,
vs: entry.endContext[1].extra,
garbageSent: -1,
garbageReceived: -1,
kills: entry.endContext[1].points,
altitude: 0.0,
rank: -1,
nerdStats: entry.endContext[1].nerdStats,
playstyle: entry.endContext[1].playstyle,
estTr: entry.endContext[1].estTr,
)
)
],
rounds: [
for (int i=0; i<entry.endContext[0].secondaryTracking.length; i++)
[BetaLeagueRound(
id: entry.endContext[0].userId,
username: entry.endContext[0].username,
naturalorder: entry.endContext[0].naturalOrder,
active: false,
alive: false,
lifetime: Duration(milliseconds: -1),
stats: BetaLeagueStats(
apm: entry.endContext[0].secondaryTracking[i],
pps: entry.endContext[0].tertiaryTracking[i],
vs: entry.endContext[0].extraTracking[i],
garbageSent: -1,
garbageReceived: -1,
kills: 0,
altitude: 0.0,
rank: -1,
nerdStats: entry.endContext[0].nerdStatsTracking[i],
playstyle: entry.endContext[0].playstyleTracking[i],
estTr: entry.endContext[0].estTrTracking[i],
)
),BetaLeagueRound(
id: entry.endContext[1].userId,
username: entry.endContext[1].username,
naturalorder: entry.endContext[1].naturalOrder,
active: false,
alive: false,
lifetime: Duration(milliseconds: -1),
stats: BetaLeagueStats(
apm: entry.endContext[1].secondaryTracking[i],
pps: entry.endContext[1].tertiaryTracking[i],
vs: entry.endContext[1].extraTracking[i],
garbageSent: -1,
garbageReceived: -1,
kills: 0,
altitude: 0.0,
rank: -1,
nerdStats: entry.endContext[1].nerdStatsTracking[i],
playstyle: entry.endContext[1].playstyleTracking[i],
estTr: entry.endContext[1].estTrTracking[i],
)
)]
]
)
)
);
}
}
}
class SingleplayerStream{ class SingleplayerStream{
late String userId; late String userId;
late String type; late String type;
@ -888,6 +1013,118 @@ class SingleplayerStream{
} }
} }
class BetaRecord{
late String id;
late String replayID;
late String gamemode;
late DateTime ts;
late String enemyUsername;
late String enemyID;
late BetaLeagueResults results;
BetaRecord({required this.id, required this.replayID, required this.gamemode, required this.ts, required this.enemyUsername, required this.enemyID, required this.results});
BetaRecord.fromJson(Map<String, dynamic> json){
id = json['_id'];
replayID = json['replayid'];
gamemode = json['gamemode'];
ts = DateTime.parse(json['ts']);
enemyUsername = json['otherusers'][0]['username'];
enemyID = json['otherusers'][0]['id'];
results = BetaLeagueResults.fromJson(json['results']);
}
}
class BetaLeagueResults{
List<BetaLeagueLeaderboardEntry> leaderboard = [];
List<List<BetaLeagueRound>> rounds = [];
BetaLeagueResults({required this.leaderboard, required this.rounds});
BetaLeagueResults.fromJson(Map<String, dynamic> json){
for (var lbEntry in json['leaderboard']) leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry));
for (var roundEntry in json['rounds']){
List<BetaLeagueRound> round = [];
for (var r in roundEntry) round.add(BetaLeagueRound.fromJson(r));
rounds.add(round);
}
}
}
class BetaLeagueLeaderboardEntry{
late String id;
late String username;
late int naturalorder;
late int wins;
late BetaLeagueStats stats;
BetaLeagueLeaderboardEntry({required this.id, required this.username, required this.naturalorder, required this.wins, required this.stats});
BetaLeagueLeaderboardEntry.fromJson(Map<String, dynamic> json){
id = json['id'];
username = json['username'];
naturalorder = json['naturalorder'];
wins = json['wins'];
stats = BetaLeagueStats.fromJson(json['stats']);
}
}
class BetaLeagueStats{
late double apm;
late double pps;
late double vs;
late int garbageSent;
late int garbageReceived;
late int kills;
late double altitude;
late int rank;
int? targetingFactor;
int? targetingRace;
late NerdStats nerdStats;
late EstTr estTr;
late Playstyle playstyle;
BetaLeagueStats({required this.apm, required this.pps, required this.vs, required this.garbageSent, required this.garbageReceived, required this.kills, required this.altitude, required this.rank, required this.nerdStats, required this.estTr, required this.playstyle});
BetaLeagueStats.fromJson(Map<String, dynamic> json){
apm = json['apm'].toDouble();
pps = json['pps'].toDouble();
vs = json['vsscore'].toDouble();
garbageSent = json['garbagesent'];
garbageReceived = json['garbagereceived'];
kills = json['kills'];
altitude = json['altitude'].toDouble();
rank = json['rank'];
targetingFactor = json['targetingfactor'];
targetingRace = json['targetinggrace'];
nerdStats = NerdStats(apm, pps, vs);
estTr = EstTr(apm, pps, vs, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe);
playstyle = Playstyle(apm, pps, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank);
}
}
class BetaLeagueRound{
late String id;
late String username;
late bool active;
late int naturalorder;
late bool alive;
late Duration lifetime;
late BetaLeagueStats stats;
BetaLeagueRound({required this.id, required this.username, required this.active, required this.naturalorder, required this.alive, required this.lifetime, required this.stats});
BetaLeagueRound.fromJson(Map<String, dynamic> json){
id = json['id'];
username = json['username'];
active = json['active'];
naturalorder = json['naturalorder'];
alive = json['alive'];
lifetime = Duration(milliseconds: json['lifetime']);
stats = BetaLeagueStats.fromJson(json['stats']);
}
}
class TetraLeagueAlphaRecord{ class TetraLeagueAlphaRecord{
late String replayId; late String replayId;
late String ownId; late String ownId;
@ -1132,26 +1369,28 @@ class RecordSingle {
late String userId; late String userId;
late String replayId; late String replayId;
late String ownId; late String ownId;
late String gamemode;
late DateTime timestamp; late DateTime timestamp;
late EndContextSingle endContext; late ResultsStats stats;
int? rank; int? rank;
RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.endContext, this.rank}); RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, this.rank});
RecordSingle.fromJson(Map<String, dynamic> json, int? ran) { RecordSingle.fromJson(Map<String, dynamic> json, int? ran) {
//developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio"); //developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio");
ownId = json['_id']; ownId = json['_id'];
endContext = EndContextSingle.fromJson(json['endcontext']); gamemode = json['gamemode'];
stats = ResultsStats.fromJson(json['results']['stats']);
replayId = json['replayid']; replayId = json['replayid'];
timestamp = DateTime.parse(json['ts']); timestamp = DateTime.parse(json['ts']);
userId = json['user']['_id']; userId = json['user']['id'];
rank = ran; rank = ran;
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data['_id'] = ownId; data['_id'] = ownId;
data['endcontext'] = endContext.toJson(); data['results']['stats'] = stats.toJson();
data['ismulti'] = false; data['ismulti'] = false;
data['replayid'] = replayId; data['replayid'] = replayId;
data['ts'] = timestamp; data['ts'] = timestamp;
@ -1470,8 +1709,8 @@ class TetrioPlayersLeaderboard {
avgPPS += entry.pps; avgPPS += entry.pps;
avgVS += entry.vs; avgVS += entry.vs;
avgTR += entry.rating; avgTR += entry.rating;
avgGlicko += entry.glicko; if (entry.glicko != null) avgGlicko += entry.glicko!;
avgRD += entry.rd; if (entry.rd != null) avgRD += entry.rd!;
avgAPP += entry.nerdStats.app; avgAPP += entry.nerdStats.app;
avgVSAPM += entry.nerdStats.vsapm; avgVSAPM += entry.nerdStats.vsapm;
avgDSS += entry.nerdStats.dss; avgDSS += entry.nerdStats.dss;
@ -1494,13 +1733,13 @@ class TetrioPlayersLeaderboard {
lowestTRid = entry.userId; lowestTRid = entry.userId;
lowestTRnick = entry.username; lowestTRnick = entry.username;
} }
if (entry.glicko < lowestGlicko){ if (entry.glicko != null && entry.glicko! < lowestGlicko){
lowestGlicko = entry.glicko; lowestGlicko = entry.glicko!;
lowestGlickoID = entry.userId; lowestGlickoID = entry.userId;
lowestGlickoNick = entry.username; lowestGlickoNick = entry.username;
} }
if (entry.rd < lowestRD){ if (entry.rd != null && entry.rd! < lowestRD){
lowestRD = entry.rd; lowestRD = entry.rd!;
lowestRdID = entry.userId; lowestRdID = entry.userId;
lowestRdNick = entry.username; lowestRdNick = entry.username;
} }
@ -1614,13 +1853,13 @@ class TetrioPlayersLeaderboard {
highestTRid = entry.userId; highestTRid = entry.userId;
highestTRnick = entry.username; highestTRnick = entry.username;
} }
if (entry.glicko > highestGlicko){ if (entry.glicko != null && entry.glicko! > highestGlicko){
highestGlicko = entry.glicko; highestGlicko = entry.glicko!;
highestGlickoID = entry.userId; highestGlickoID = entry.userId;
highestGlickoNick = entry.username; highestGlickoNick = entry.username;
} }
if (entry.rd > highestRD){ if (entry.rd != null && entry.rd! > highestRD){
highestRD = entry.rd; highestRD = entry.rd!;
highestRdID = entry.userId; highestRdID = entry.userId;
highestRdNick = entry.username; highestRdNick = entry.username;
} }
@ -1929,7 +2168,7 @@ class TetrioPlayersLeaderboard {
} }
PlayerLeaderboardPosition? getLeaderboardPosition(TetrioPlayer user) { PlayerLeaderboardPosition? getLeaderboardPosition(TetrioPlayer user) {
if (user.tlSeason1.gamesPlayed == 0) return null; if (user.tlSeason1?.gamesPlayed == 0) return null;
bool fakePositions = false; bool fakePositions = false;
late List<TetrioPlayerFromLeaderboard> copyOfLeaderboard; late List<TetrioPlayerFromLeaderboard> copyOfLeaderboard;
if (leaderboard.indexWhere((element) => element.userId == user.userId) == -1){ if (leaderboard.indexWhere((element) => element.userId == user.userId) == -1){
@ -2029,16 +2268,15 @@ class TetrioPlayerFromLeaderboard {
late String role; late String role;
late double xp; late double xp;
String? country; String? country;
late bool supporter;
late bool verified; late bool verified;
late DateTime timestamp; late DateTime timestamp;
late int gamesPlayed; late int gamesPlayed;
late int gamesWon; late int gamesWon;
late double rating; late double rating;
late double glicko; late double? glicko;
late double rd; late double? rd;
late String rank; late String rank;
late String bestRank; late String? bestRank;
late double apm; late double apm;
late double pps; late double pps;
late double vs; late double vs;
@ -2053,7 +2291,6 @@ class TetrioPlayerFromLeaderboard {
this.role, this.role,
this.xp, this.xp,
this.country, this.country,
this.supporter,
this.verified, this.verified,
this.timestamp, this.timestamp,
this.gamesPlayed, this.gamesPlayed,
@ -2081,14 +2318,13 @@ class TetrioPlayerFromLeaderboard {
role = json['role']; role = json['role'];
xp = json['xp'].toDouble(); xp = json['xp'].toDouble();
country = json['country']; country = json['country'];
supporter = json['supporter'];
verified = json['verified']; verified = json['verified'];
timestamp = ts; timestamp = ts;
gamesPlayed = json['league']['gamesplayed']; gamesPlayed = json['league']['gamesplayed'] as int;
gamesWon = json['league']['gameswon']; gamesWon = json['league']['gameswon'] as int;
rating = json['league']['rating'].toDouble(); rating = json['league']['rating'] != null ? json['league']['rating'].toDouble() : 0;
glicko = json['league']['glicko'].toDouble(); glicko = json['league']['glicko'] != null ? json['league']['glicko'].toDouble() : null;
rd = json['league']['rd'].toDouble(); rd = json['league']['rd'] != null ? json['league']['rd'].toDouble() : null;
rank = json['league']['rank']; rank = json['league']['rank'];
bestRank = json['league']['bestrank']; bestRank = json['league']['bestrank'];
apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00; apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00;
@ -2105,9 +2341,9 @@ class TetrioPlayerFromLeaderboard {
case Stats.tr: case Stats.tr:
return rating; return rating;
case Stats.glicko: case Stats.glicko:
return glicko; return glicko??-1;
case Stats.rd: case Stats.rd:
return rd; return rd??-1;
case Stats.gp: case Stats.gp:
return gamesPlayed; return gamesPlayed;
case Stats.gw: case Stats.gw:

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 2 /// Locales: 2
/// Strings: 1186 (593 per locale) /// Strings: 1198 (599 per locale)
/// ///
/// Built on 2024-07-20 at 13:24 UTC /// Built on 2024-07-27 at 18:54 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
@ -222,6 +222,12 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get verdictBetter => 'better'; String get verdictBetter => 'better';
String get verdictWorse => 'worse'; String get verdictWorse => 'worse';
String get smooth => 'Smooth'; String get smooth => 'Smooth';
String get postSeason => 'Off-season';
String get seasonStarts => 'Season starts in:';
String get myMessadgeHeader => 'A messadge from dan63';
String get myMessadgeBody => 'TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.';
String preSeasonMessage({required Object n}) => 'Right now you can play unranked FT3 matches against absolutely random player.\nSeason ${n} rules applied';
String get nanow => 'Not avaliable for now...';
String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}'; String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}';
String get seasonEnded => 'Season has ended'; String get seasonEnded => 'Season has ended';
String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; String gamesUntilRanked({required Object left}) => '${left} games until being ranked';
@ -919,6 +925,12 @@ class _StringsRu implements Translations {
@override String get verdictBetter => 'Лучше'; @override String get verdictBetter => 'Лучше';
@override String get verdictWorse => 'Хуже'; @override String get verdictWorse => 'Хуже';
@override String get smooth => 'Гладкий'; @override String get smooth => 'Гладкий';
@override String get postSeason => 'Внесезонье';
@override String get seasonStarts => 'Сезон начнётся через:';
@override String get myMessadgeHeader => 'Сообщение от dan63';
@override String get myMessadgeBody => 'TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.';
@override String preSeasonMessage({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона';
@override String get nanow => 'Пока недоступно...';
@override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}'; @override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}';
@override String get seasonEnded => 'Сезон закончился'; @override String get seasonEnded => 'Сезон закончился';
@override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга';
@ -1608,6 +1620,12 @@ extension on Translations {
case 'verdictBetter': return 'better'; case 'verdictBetter': return 'better';
case 'verdictWorse': return 'worse'; case 'verdictWorse': return 'worse';
case 'smooth': return 'Smooth'; case 'smooth': return 'Smooth';
case 'postSeason': return 'Off-season';
case 'seasonStarts': return 'Season starts in:';
case 'myMessadgeHeader': return 'A messadge from dan63';
case 'myMessadgeBody': return 'TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.';
case 'preSeasonMessage': return ({required Object n}) => 'Right now you can play unranked FT3 matches against absolutely random player.\nSeason ${n} rules applied';
case 'nanow': return 'Not avaliable for now...';
case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}'; case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}';
case 'seasonEnded': return 'Season has ended'; case 'seasonEnded': return 'Season has ended';
case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked';
@ -2221,6 +2239,12 @@ extension on _StringsRu {
case 'verdictBetter': return 'Лучше'; case 'verdictBetter': return 'Лучше';
case 'verdictWorse': return 'Хуже'; case 'verdictWorse': return 'Хуже';
case 'smooth': return 'Гладкий'; case 'smooth': return 'Гладкий';
case 'postSeason': return 'Внесезонье';
case 'seasonStarts': return 'Сезон начнётся через:';
case 'myMessadgeHeader': return 'Сообщение от dan63';
case 'myMessadgeBody': return 'TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.';
case 'preSeasonMessage': return ({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона';
case 'nanow': return 'Пока недоступно...';
case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}'; case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}';
case 'seasonEnded': return 'Сезон закончился'; case 'seasonEnded': return 'Сезон закончился';
case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга';

View File

@ -25,39 +25,15 @@ import 'package:go_router/go_router.dart';
late final PackageInfo packageInfo; late final PackageInfo packageInfo;
late SharedPreferences prefs; late SharedPreferences prefs;
late TetrioService teto; late TetrioService teto;
ThemeData theme = ThemeData(fontFamily: 'Eurostile Round', colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white), scaffoldBackgroundColor: Colors.black); ThemeData theme = ThemeData(
fontFamily: 'Eurostile Round',
// Future<dynamic> computeIsolate(Future Function() function) async { colorScheme: const ColorScheme.dark(
// final receivePort = ReceivePort(); primary: Colors.cyanAccent,
// var rootToken = RootIsolateToken.instance!; surface: Color.fromARGB(255, 10, 10, 10),
// await Isolate.spawn<_IsolateData>( secondary: Colors.white
// _isolateEntry, ),
// _IsolateData( scaffoldBackgroundColor: Colors.black
// token: rootToken, );
// function: function,
// answerPort: receivePort.sendPort,
// ),
// );
// return await receivePort.first;
// }
// void _isolateEntry(_IsolateData isolateData) async {
// BackgroundIsolateBinaryMessenger.ensureInitialized(isolateData.token);
// final answer = await isolateData.function();
// isolateData.answerPort.send(answer);
// }
// class _IsolateData {
// final RootIsolateToken token;
// final Function function;
// final SendPort answerPort;
// _IsolateData({
// required this.token,
// required this.function,
// required this.answerPort,
// });
// }
final router = GoRouter( final router = GoRouter(
initialLocation: "/", initialLocation: "/",
@ -189,4 +165,4 @@ class MyAppState extends State<MyApp> {
theme: theme theme: theme
); );
} }
} }

View File

@ -557,8 +557,6 @@ class TetrioService extends DB {
nextAt: -1, nextAt: -1,
prevAt: -1 prevAt: -1
), ),
sprint: [],
blitz: []
); );
history.add(state); history.add(state);
} }
@ -601,7 +599,7 @@ class TetrioService extends DB {
} }
/// Docs later /// Docs later
Future<List<TetraLeagueAlphaRecord>> fetchAndSaveOldTLmatches(String userID) async { Future<TetraLeagueAlphaStream> fetchAndSaveOldTLmatches(String userID) async {
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID});
@ -616,7 +614,7 @@ class TetrioService extends DB {
case 200: case 200:
TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID); TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID);
saveTLMatchesFromStream(stream); saveTLMatchesFromStream(stream);
return stream.records; return stream;
case 404: case 404:
developer.log("fetchAndSaveOldTLmatches: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); developer.log("fetchAndSaveOldTLmatches: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode);
throw TetrioHistoryNotExist(); throw TetrioHistoryNotExist();
@ -650,7 +648,7 @@ class TetrioService extends DB {
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"});
} else { } else {
url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); url = Uri.https('ch.tetr.io', 'api/users/by/league');
} }
try{ try{
final response = await client.get(url); final response = await client.get(url);
@ -660,7 +658,7 @@ class TetrioService extends DB {
_lbPositions.clear(); _lbPositions.clear();
var rawJson = jsonDecode(response.body); var rawJson = jsonDecode(response.body);
if (rawJson['success']) { // if api confirmed that everything ok if (rawJson['success']) { // if api confirmed that everything ok
TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at'])); TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['entries'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at']));
developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud");
//_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard;
_cache.store(leaderboard, rawJson['cache']['cached_until']); _cache.store(leaderboard, rawJson['cache']['cached_until']);
@ -758,15 +756,15 @@ class TetrioService extends DB {
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream). /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve. /// Throws an exception if fails to retrieve.
Future<TetraLeagueAlphaStream> fetchTLStream(String userID) async { Future<TetraLeagueBetaStream> fetchTLStream(String userID) async {
TetraLeagueAlphaStream? cached = _cache.get(userID, TetraLeagueAlphaStream); TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueAlphaStream);
if (cached != null) return cached; if (cached != null) return cached;
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()});
} else { } else {
url = Uri.https('ch.tetr.io', 'api/streams/league_userrecent_${userID.toLowerCase().trim()}'); url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/league/recent');
} }
try { try {
final response = await client.get(url); final response = await client.get(url);
@ -774,7 +772,7 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
if (jsonDecode(response.body)['success']) { if (jsonDecode(response.body)['success']) {
TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID); TetraLeagueBetaStream stream = TetraLeagueBetaStream.fromJson(jsonDecode(response.body)['data']['entries'], userID);
_cache.store(stream, jsonDecode(response.body)['cache']['cached_until']); _cache.store(stream, jsonDecode(response.body)['cache']['cached_until']);
developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud");
return stream; return stream;
@ -941,6 +939,50 @@ class TetrioService extends DB {
} }
} }
Future<Summaries> fetchSummaries(String id) async {
Summaries? cached = _cache.get(id, Summaries);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "Summaries", "id": id});
} else {
url = Uri.https('ch.tetr.io', 'api/users/$id/summaries');
}
try{
final response = await client.get(url);
switch (response.statusCode) {
case 200:
if (jsonDecode(response.body)['success']) {
developer.log("fetchSummaries: $id summaries retrieved and cached", name: "services/tetrio_crud");
return Summaries.fromJson(jsonDecode(response.body)['data'], id);
} else {
developer.log("fetchSummaries: User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist();
}
case 403:
throw TetrioForbidden();
case 429:
throw TetrioTooManyRequests();
case 418:
throw TetrioOskwareBridgeProblem();
case 500:
case 502:
case 503:
case 504:
throw TetrioInternalProblem();
default:
developer.log("fetchRecords Failed to fetch records", name: "services/tetrio_crud", error: response.statusCode);
throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
}
} on http.ClientException catch (e, s) {
developer.log("$e, $s");
throw http.ClientException(e.message, e.uri);
}
}
/// Creates an entry in local DB for [tetrioPlayer]. Throws an exception if that player already here. /// Creates an entry in local DB for [tetrioPlayer]. Throws an exception if that player already here.
Future<void> createPlayer(TetrioPlayer tetrioPlayer) async { Future<void> createPlayer(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
@ -1131,7 +1173,7 @@ class TetrioService extends DB {
var json = jsonDecode(response.body); var json = jsonDecode(response.body);
if (json['success']) { if (json['success']) {
// parse and count stats // parse and count stats
TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true)); TetrioPlayer player = TetrioPlayer.fromJson(json['data'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['_id'], json['data']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true));
_cache.store(player, json['cache']['cached_until']); _cache.store(player, json['cache']['cached_until']);
developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud");
return player; return player;
@ -1175,14 +1217,14 @@ class TetrioService extends DB {
return data; return data;
} }
Future<void> fetchTracked() async { // Future<void> fetchTracked() async {
for (String userID in (await getAllPlayerToTrack())) { // for (String userID in (await getAllPlayerToTrack())) {
TetrioPlayer player = await fetchPlayer(userID); // TetrioPlayer player = await fetchPlayer(userID);
storeState(player); // storeState(player);
sleep(Durations.extralong4); // sleep(Durations.extralong4);
TetraLeagueAlphaStream matches = await fetchTLStream(userID); // TetraLeagueBetaStream matches = await fetchTLStream(userID);
saveTLMatchesFromStream(matches); // saveTLMatchesFromStream(matches);
sleep(Durations.extralong4); // sleep(Durations.extralong4);
} // }
} // }
} }

View File

@ -3,7 +3,8 @@ import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode);
final NumberFormat nonsecs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); final NumberFormat nonsecs = NumberFormat("00", LocaleSettings.currentLocale.languageCode);
final NumberFormat nonsecs3 = NumberFormat("000", LocaleSettings.currentLocale.languageCode);
final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode); final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode);
/// Returns string, that represents time difference between [dateTime] and now /// Returns string, that represents time difference between [dateTime] and now
@ -77,5 +78,11 @@ String readableTimeDifference(Duration a, Duration b){
} }
String countdown(Duration difference){ String countdown(Duration difference){
return "${difference.inDays}:${nonsecs.format(difference.inHours%24)}:${nonsecs.format(difference.inMinutes%60)}:${secs.format(difference.inSeconds%60)}"; return "${difference.inDays}d ${nonsecs.format(difference.inHours%24)}h ${nonsecs.format(difference.inMinutes%60)}m ${secs.format(difference.inSeconds%60)}s";
}
String playtime(Duration difference){
if (difference.inHours > 0) return "${intf.format(difference.inHours)}h ${nonsecs.format(difference.inMinutes%60)}m";
else if (difference.inMinutes > 0) return "${difference.inMinutes}m ${nonsecs.format(difference.inSeconds%60)}s";
else return "${secs.format(difference.inMilliseconds/1000)}s";
} }

View File

@ -155,7 +155,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
// Requesting Tetra League (alpha), records, news and top TR of player // Requesting Tetra League (alpha), records, news and top TR of player
late List<dynamic> requests; late List<dynamic> requests;
late TetraLeagueAlphaStream tlStream; late Summaries summaries;
late TetraLeagueBetaStream tlStream;
late UserRecords records; late UserRecords records;
late News news; late News news;
late SingleplayerStream recent; late SingleplayerStream recent;
@ -164,24 +165,26 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
late TetrioPlayerFromLeaderboard? topOne; late TetrioPlayerFromLeaderboard? topOne;
late TopTr? topTR; late TopTr? topTR;
requests = await Future.wait([ // all at once (7 requests to oskware lmao) requests = await Future.wait([ // all at once (7 requests to oskware lmao)
teto.fetchSummaries(_searchFor),
teto.fetchTLStream(_searchFor), teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor), //teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor), teto.fetchNews(_searchFor),
teto.fetchSingleplayerStream(_searchFor, "any_userrecent"), // teto.fetchSingleplayerStream(_searchFor, "any_userrecent"),
teto.fetchSingleplayerStream(_searchFor, "40l_userbest"), // teto.fetchSingleplayerStream(_searchFor, "40l_userbest"),
teto.fetchSingleplayerStream(_searchFor, "blitz_userbest"), // teto.fetchSingleplayerStream(_searchFor, "blitz_userbest"),
prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=><Map<String, double>>[]), // 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), //(me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null),
(me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR //(me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR
]); ]);
tlStream = requests[0] as TetraLeagueAlphaStream; summaries = requests[0] as Summaries;
records = requests[1] as UserRecords; tlStream = requests[1] as TetraLeagueBetaStream;
// records = requests[1] as UserRecords;
news = requests[2] as News; news = requests[2] as News;
recent = requests[3] as SingleplayerStream; // recent = requests[3] as SingleplayerStream;
sprint = requests[4] as SingleplayerStream; // sprint = requests[4] as SingleplayerStream;
blitz = requests[5] as SingleplayerStream; // blitz = requests[5] as SingleplayerStream;
topOne = requests[7] as TetrioPlayerFromLeaderboard?; // topOne = requests[7] as TetrioPlayerFromLeaderboard?;
topTR = requests[8] as TopTr?; // No TR - no Top TR // topTR = requests[8] as TopTr?; // No TR - no Top TR
meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId);
if (prefs.getBool("showPositions") == true){ if (prefs.getBool("showPositions") == true){
@ -193,37 +196,34 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!);
} }
} }
Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[6] as Cutoffs?)?.tr; //Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[6] as Cutoffs?)?.tr;
Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[6] as Cutoffs?)?.glicko; //Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[6] as Cutoffs?)?.glicko;
if (me.tlSeason1.gamesPlayed > 9) { // if (me.tlSeason1.gamesPlayed > 9) {
thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; // thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank];
thatRankGlickoCutoff = cutoffsGlicko?[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)]; // 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)]; // nextRankGlickoCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)];
} // }
if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; // if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0];
// Making list of Tetra League matches // Making list of Tetra League matches
List<TetraLeagueAlphaRecord> tlMatches = [];
bool isTracking = await teto.isPlayerTracking(me.userId); bool isTracking = await teto.isPlayerTracking(me.userId);
List<TetrioPlayer> states = []; List<TetrioPlayer> states = [];
TetraLeagueAlpha? compareWith; TetraLeagueAlpha? compareWith;
Set<TetraLeagueAlpha> uniqueTL = {}; Set<TetraLeagueAlpha> uniqueTL = {};
tlMatches = tlStream.records;
List<TetraLeagueAlphaRecord> storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches List<TetraLeagueAlphaRecord> storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches
if (isTracking){ // if tracked - save data to local DB if (isTracking){ // if tracked - save data to local DB
await teto.storeState(me); await teto.storeState(me);
await teto.saveTLMatchesFromStream(tlStream); //await teto.saveTLMatchesFromStream(tlStream);
} }
TetraLeagueAlphaStream? oldMatches;
// building list of TL matches // building list of TL matches
if(fetchTLmatches) { if(fetchTLmatches) {
try{ try{
List<TetraLeagueAlphaRecord> oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor); oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor);
storedRecords.addAll(oldMatches); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.records.length))));
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.length))));
}on TetrioHistoryNotExist{ }on TetrioHistoryNotExist{
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches))); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches)));
}on P1nkl0bst3rForbidden { }on P1nkl0bst3rForbidden {
@ -237,16 +237,16 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
} }
} }
if (storedRecords.isNotEmpty) _TLHistoryWasFetched = true; if (storedRecords.isNotEmpty) _TLHistoryWasFetched = true;
for (var match in storedRecords) {
// add stored match to list only if it missing from retrived ones // add stored match to list only if it missing from retrived ones
if (!tlMatches.contains(match)) tlMatches.add(match); if (oldMatches != null) tlStream.addFromAlphaStream(oldMatches);
}
tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list // tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list
if(a.timestamp.isBefore(b.timestamp)) return 1; // if(a.ts.isBefore(b.ts)) return 1;
if(a.timestamp.isAtSameMomentAs(b.timestamp)) return 0; // if(a.ts.isAtSameMomentAs(b.ts)) return 0;
if(a.timestamp.isAfter(b.timestamp)) return -1; // if(a.ts.isAfter(b.ts)) return -1;
return 0; // return 0;
}); // });
// Handling history // Handling history
if(fetchHistory){ if(fetchHistory){
@ -266,8 +266,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
states.addAll(await teto.getPlayer(me.userId)); states.addAll(await teto.getPlayer(me.userId));
for (var element in states) { // For graphs I need only unique entries for (var element in states) { // For graphs I need only unique entries
if (uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1); if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!);
if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1); if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!);
} }
// Also i need previous Tetra League State for comparison if avaliable // Also i need previous Tetra League State for comparison if avaliable
if (uniqueTL.length >= 2){ if (uniqueTL.length >= 2){
@ -305,8 +305,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
changePlayer(me.userId); changePlayer(me.userId);
}); });
} }
return [me, summaries, news, tlStream];
return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp];
} }
/// Triggers widgets rebuild /// Triggers widgets rebuild
@ -455,31 +455,31 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
width: MediaQuery.of(context).size.width-450, width: MediaQuery.of(context).size.width-450,
constraints: const BoxConstraints(maxWidth: 1024), constraints: const BoxConstraints(maxWidth: 1024),
child: TLThingy( child: TLThingy(
tl: snapshot.data![0].tlSeason1, tl: snapshot.data![1].league,
userID: snapshot.data![0].userId, userID: snapshot.data![0].userId,
states: snapshot.data![2], states: const [], //snapshot.data![2],
topTR: snapshot.data![7]?.tr, //topTR: snapshot.data![7]?.tr,
lastMatchPlayed: snapshot.data![11], //lastMatchPlayed: snapshot.data![11],
bot: snapshot.data![0].role == "bot", bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon", guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff, //thatRankCutoff: thatRankCutoff,
thatRankCutoffGlicko: thatRankGlickoCutoff, //thatRankCutoffGlicko: thatRankGlickoCutoff,
thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, //thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null,
nextRankCutoff: nextRankCutoff, //nextRankCutoff: nextRankCutoff,
nextRankCutoffGlicko: nextRankGlickoCutoff, //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, //nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null,
averages: rankAverages, //averages: rankAverages,
lbPositions: meAmongEveryone //lbPositions: meAmongEveryone
), ),
), ),
SizedBox( SizedBox(
width: 450, width: 450,
child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true,) child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true)
), ),
],), ],),
_History(chartsData: chartsData, 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![1].league.gamesPlayed > 0),
_TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, recent: snapshot.data![8], sprintStream: snapshot.data![9], blitzStream: snapshot.data![10]), _TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, recent: SingleplayerStream(userId: "userId", records: [], type: "recent"), sprintStream: SingleplayerStream(userId: "userId", records: [], type: "40l"), blitzStream: SingleplayerStream(userId: "userId", records: [], type: "blitz")),
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![2])
] : [ ] : [
TLThingy( TLThingy(
tl: snapshot.data![0].tlSeason1, tl: snapshot.data![0].tlSeason1,
@ -693,7 +693,7 @@ class _NavDrawerState extends State<NavDrawer> {
class _TLRecords extends StatelessWidget { class _TLRecords extends StatelessWidget {
final String userID; final String userID;
final Function changePlayer; final Function changePlayer;
final List<TetraLeagueAlphaRecord> data; final List<BetaRecord> data;
final bool wasActiveInTL; final bool wasActiveInTL;
final bool oldMathcesHere; final bool oldMathcesHere;
final bool separateScrollController; final bool separateScrollController;
@ -732,7 +732,7 @@ class _TLRecords extends StatelessWidget {
)); ));
} }
var accentColor = data[index].endContext.firstWhere((element) => element.userId == userID).success ? Colors.green : Colors.red; var accentColor = data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins > data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins ? Colors.green : Colors.red;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@ -741,19 +741,19 @@ class _TLRecords extends StatelessWidget {
) )
), ),
child: ListTile( child: ListTile(
leading: Text("${data[index].endContext.firstWhere((element) => element.userId == userID).points} : ${data[index].endContext.firstWhere((element) => element.userId != userID).points}", leading: Text("${data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins} : ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins}",
style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)),
title: Text("vs. ${data[index].endContext.firstWhere((element) => element.userId != userID).username}"), title: Text("vs. ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).username}"),
subtitle: Text(timestamp(data[index].timestamp), style: const TextStyle(color: Colors.grey)), subtitle: Text(timestamp(data[index].ts), style: const TextStyle(color: Colors.grey)),
trailing: TrailingStats( trailing: TrailingStats(
data[index].endContext.firstWhere((element) => element.userId == userID).secondary, data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.apm,
data[index].endContext.firstWhere((element) => element.userId == userID).tertiary, data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.pps,
data[index].endContext.firstWhere((element) => element.userId == userID).extra, data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.vs,
data[index].endContext.firstWhere((element) => element.userId != userID).secondary, data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.apm,
data[index].endContext.firstWhere((element) => element.userId != userID).tertiary, data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.pps,
data[index].endContext.firstWhere((element) => element.userId != userID).extra data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.vs,
), ),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), onTap: () => {if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.nanow)))} //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))),
), ),
); );
}); });
@ -1015,17 +1015,17 @@ class _TwoRecordsThingy extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
late MapEntry closestAverageBlitz; late MapEntry closestAverageBlitz;
late bool blitzBetterThanClosestAverage; late bool blitzBetterThanClosestAverage;
bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.endContext.score > blitzAverages[rank]! : null; bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null;
late MapEntry closestAverageSprint; late MapEntry closestAverageSprint;
late bool sprintBetterThanClosestAverage; late bool sprintBetterThanClosestAverage;
bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.endContext.finalTime < sprintAverages[rank]! : null; bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.stats.finalTime < sprintAverages[rank]! : null;
if (sprint != null) { if (sprint != null) {
closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.endContext.finalTime).abs() < (b -sprint!.endContext.finalTime).abs() ? a : b)); closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.stats.finalTime).abs() < (b -sprint!.stats.finalTime).abs() ? a : b));
sprintBetterThanClosestAverage = sprint!.endContext.finalTime < closestAverageSprint.value; sprintBetterThanClosestAverage = sprint!.stats.finalTime < closestAverageSprint.value;
} }
if (blitz != null){ if (blitz != null){
closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.endContext.score).abs() < (b -blitz!.endContext.score).abs() ? a : b)); closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.stats.score).abs() < (b -blitz!.stats.score).abs() ? a : b));
blitzBetterThanClosestAverage = blitz!.endContext.score > closestAverageBlitz.value; blitzBetterThanClosestAverage = blitz!.stats.score > closestAverageBlitz.value;
} }
return SingleChildScrollView(child: Padding( return SingleChildScrollView(child: Padding(
padding: const EdgeInsets.only(top: 20.0), padding: const EdgeInsets.only(top: 20.0),
@ -1047,19 +1047,19 @@ class _TwoRecordsThingy extends StatelessWidget {
children: [ children: [
Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
RichText(text: TextSpan( RichText(text: TextSpan(
text: sprint != null ? get40lTime(sprint!.endContext.finalTime.inMicroseconds) : "---", text: sprint != null ? get40lTime(sprint!.stats.finalTime.inMicroseconds) : "---",
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey),
//children: [TextSpan(text: get40lTime(record!.endContext.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] //children: [TextSpan(text: get40lTime(record!.stats.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
), ),
), ),
if (sprint != null) RichText(text: TextSpan( if (sprint != null) RichText(text: TextSpan(
text: "", text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [ children: [
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
)) ))
else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)), )),
if (sprint!.rank != null) TextSpan(text: "${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank!))), if (sprint!.rank != null) TextSpan(text: "${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank!))),
@ -1076,14 +1076,14 @@ class _TwoRecordsThingy extends StatelessWidget {
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
spacing: 20, spacing: 20,
children: [ children: [
StatCellNum(playerStat: sprint!.endContext.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: sprint!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: sprint!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: sprint!.stats.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
], ],
), ),
if (sprint != null) FinesseThingy(sprint?.endContext.finesse, sprint?.endContext.finessePercentage), if (sprint != null) FinesseThingy(sprint?.stats.finesse, sprint?.stats.finessePercentage),
if (sprint != null) LineclearsThingy(sprint!.endContext.clears, sprint!.endContext.lines, sprint!.endContext.holds, sprint!.endContext.tSpins), if (sprint != null) LineclearsThingy(sprint!.stats.clears, sprint!.stats.lines, sprint!.stats.holds, sprint!.stats.tSpins),
if (sprint != null) Text("${sprint!.endContext.inputs} KP • ${f2.format(sprint!.endContext.kps)} KPS"), if (sprint != null) Text("${sprint!.stats.inputs} KP • ${f2.format(sprint!.stats.kps)} KPS"),
if (sprint != null) Wrap( if (sprint != null) Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
@ -1101,10 +1101,10 @@ class _TwoRecordsThingy extends StatelessWidget {
for (int i = 1; i < sprintStream.records.length; i++) ListTile( for (int i = 1; i < sprintStream.records.length; i++) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))),
leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ),
title: Text(get40lTime(sprintStream.records[i].endContext.finalTime.inMicroseconds), title: Text(get40lTime(sprintStream.records[i].stats.finalTime.inMicroseconds),
style: const TextStyle(fontSize: 18)), style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(sprintStream.records[i].endContext) trailing: SpTrailingStats(sprintStream.records[i].stats, sprintStream.records[i].gamemode)
) )
], ],
), ),
@ -1128,7 +1128,7 @@ class _TwoRecordsThingy extends StatelessWidget {
text: "", text: "",
style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white),
children: [ children: [
TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.endContext.score) : "---"), TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.stats.score) : "---"),
//WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48)) //WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48))
] ]
), ),
@ -1139,10 +1139,10 @@ class _TwoRecordsThingy extends StatelessWidget {
text: "", text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [ children: [
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
)) ))
else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)), )),
TextSpan(text: timestamp(blitz!.timestamp)), TextSpan(text: timestamp(blitz!.timestamp)),
@ -1162,14 +1162,14 @@ class _TwoRecordsThingy extends StatelessWidget {
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20, spacing: 20,
children: [ children: [
StatCellNum(playerStat: blitz!.endContext.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: blitz!.stats.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: blitz!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true) StatCellNum(playerStat: blitz!.stats.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true)
], ],
), ),
if (blitz != null) FinesseThingy(blitz?.endContext.finesse, blitz?.endContext.finessePercentage), if (blitz != null) FinesseThingy(blitz?.stats.finesse, blitz?.stats.finessePercentage),
if (blitz != null) LineclearsThingy(blitz!.endContext.clears, blitz!.endContext.lines, blitz!.endContext.holds, blitz!.endContext.tSpins), if (blitz != null) LineclearsThingy(blitz!.stats.clears, blitz!.stats.lines, blitz!.stats.holds, blitz!.stats.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!.stats.piecesPlaced} P • ${blitz!.stats.inputs} KP • ${f2.format(blitz!.stats.kpp)} KPP • ${f2.format(blitz!.stats.kps)} KPS"),
if (blitz != null) Wrap( if (blitz != null) Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
@ -1187,10 +1187,10 @@ class _TwoRecordsThingy extends StatelessWidget {
for (int i = 1; i < blitzStream.records.length; i++) ListTile( for (int i = 1; i < blitzStream.records.length; i++) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))),
leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ),
title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].endContext.score)} points", title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].stats.score)} points",
style: const TextStyle(fontSize: 18)), style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(blitzStream.records[i].endContext) trailing: SpTrailingStats(blitzStream.records[i].stats, blitzStream.records[i].gamemode)
) )
], ],
), ),
@ -1277,7 +1277,9 @@ class _OtherThingy extends StatelessWidget {
Map<String, String> gametypes = { Map<String, String> gametypes = {
"40l": t.sprint, "40l": t.sprint,
"blitz": t.blitz, "blitz": t.blitz,
"5mblast": "5,000,000 Blast" "5mblast": "5,000,000 Blast",
"zenith": "Quick Play",
"zenithex": "Quick Play Expert",
}; };
// Individuly handle each entry type // Individuly handle each entry type
@ -1306,7 +1308,15 @@ class _OtherThingy extends StatelessWidget {
children: [ children: [
TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.personalbestMiddle), TextSpan(text: t.newsParts.personalbestMiddle),
TextSpan(text: news.data["gametype"] == "blitz" ? NumberFormat.decimalPattern().format(news.data["result"]) : get40lTime((news.data["result"]*1000).floor()), style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: switch (news.data["gametype"]){
"blitz" => NumberFormat.decimalPattern().format(news.data["result"]),
"40l" => get40lTime((news.data["result"]*1000).floor()),
"5mblast" => get40lTime((news.data["result"]*1000).floor()),
"zenith" => "${f2.format(news.data["result"])} m.",
_ => "unknown"
},
style: const TextStyle(fontWeight: FontWeight.bold)
),
] ]
) )
), ),

View File

@ -0,0 +1,714 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Badge;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/compare_view.dart';
import 'package:tetra_stats/widgets/stat_sell_num.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/main.dart';
import 'package:tetra_stats/widgets/tl_thingy.dart';
import 'package:tetra_stats/widgets/user_thingy.dart';
class MainView extends StatefulWidget {
final String? player;
/// The very first view, that user see when he launch this programm.
/// By default it loads my or defined in preferences user stats, but
/// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu.
const MainView({super.key, this.player});
@override
State<MainView> createState() => _MainState();
}
TetrioPlayer testPlayer = TetrioPlayer(
userId: "6098518e3d5155e6ec429cdc",
username: "dan63",
registrationTime: DateTime(2002, 2, 25, 9, 30, 01),
avatarRevision: 1704835194288,
bannerRevision: 1661462402700,
role: "sysop",
country: "BY",
state: DateTime(1970),
badges: [
Badge(badgeId: "kod_founder", label: "Убил оска", ts: DateTime(2023, 6, 27, 18, 51, 49)),
Badge(badgeId: "kod_by_founder", label: "Убит оском", ts: DateTime(2023, 6, 27, 18, 51, 51)),
Badge(badgeId: "5mblast_1", label: "5M Blast Winner"),
Badge(badgeId: "20tsd", label: "20 TSD"),
Badge(badgeId: "allclear", label: "10PC's"),
Badge(badgeId: "100player", label: "Won some shit"),
Badge(badgeId: "founder", label: "osk"),
Badge(badgeId: "early-supporter", label: "Sus"),
Badge(badgeId: "bugbounty", label: "Break some ribbons"),
Badge(badgeId: "infdev", label: "Closed player")
],
friendCount: 69,
gamesPlayed: 13747,
gamesWon: 6523,
gameTime: Duration(days: 79, minutes: 28, seconds: 23, microseconds: 637591),
xp: 1415239,
supporterTier: 2,
verified: true,
connections: null,
tlSeason1: TetraLeagueAlpha(timestamp: DateTime(1970), gamesPlayed: 28, gamesWon: 14, bestRank: "x", decaying: false, rating: 23500.6194, rank: "x", percentileRank: "x", percentile: 0.00, standing: 1, standingLocal: 1, nextAt: -1, prevAt: 500),
distinguishment: Distinguishment(type: "twc", detail: "2023"),
bio: "кровбер не в палку, без последнего тспина - 32 атаки. кровбер не в палку, без первого тсм и последнего тспина - 30 атаки. кровбер в палку с б2б - 38 атаки.(5 б2б)(не знаю от чего зависит) кровбер в палку с б2б - 36 атаки.(5 б2б)(не знаю от чего зависит)"
);
News testNews = News("6098518e3d5155e6ec429cdc", [
NewsEntry(type: "personalbest", data: {"gametype": "40l", "result": 23.232}, timestamp: DateTime(2002, 2, 25, 10, 30, 01)),
NewsEntry(type: "personalbest", data: {"gametype": "blitz", "result": 23.232}, timestamp: DateTime(2002, 2, 25, 10, 30, 02)),
NewsEntry(type: "personalbest", data: {"gametype": "5mblast", "result": 23.232}, timestamp: DateTime(2002, 2, 25, 10, 30, 03)),
]);
class _MainState extends State<MainView> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Scaffold(body: Row(
children: [
NavigationRail(
destinations: [
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('First'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('Second'),
),
NavigationRailDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: Text('Third'),
)
],
selectedIndex: 0
),
SizedBox(
width: 450.0,
child: Column(
children: [
NewUserThingy(player: testPlayer, showStateTimestamp: false, setState: setState),
Padding(
padding: const EdgeInsets.fromLTRB(4.0, 0.0, 4.0, 0.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: Icon(Icons.person_add), label: Text(t.track), style: ButtonStyle(shape: MaterialStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(12.0), right: Radius.zero)))))),
Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: Icon(Icons.balance), label: Text(t.compare), style: ButtonStyle(shape: MaterialStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.zero, right: Radius.circular(12.0)))))))
],
),
),
Card(
surfaceTintColor: theme.colorScheme.surface,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
child: Row(
children: [
Text("Badges", style: TextStyle(fontFamily: "Eurostile Round Extended")),
Spacer(),
Text(intf.format(testPlayer.badges.length))
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var badge in testPlayer.badges)
IconButton(
onPressed: () => showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(badge.label, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
content: SingleChildScrollView(
child: ListBody(
children: [
Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 25,
children: [
Image.asset("res/tetrio_badges/${badge.badgeId}.png"),
Text(badge.ts != null
? t.obtainDate(date: timestamp(badge.ts!))
: t.assignedManualy),
],
)
],
),
),
actions: <Widget>[
TextButton(
child: Text(t.popupActions.ok),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
),
tooltip: badge.label,
icon: Image.asset(
"res/tetrio_badges/${badge.badgeId}.png",
height: 32,
width: 32,
errorBuilder: (context, error, stackTrace) {
return Image.network(
kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png",
height: 32,
width: 32,
errorBuilder:(context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 32, width: 32);
}
);
},
)
)
],
),
)
],
),
),
if (testPlayer.distinguishment != null) DistinguishmentThingy(testPlayer.distinguishment!),
if (testPlayer.bio != null) Card(
surfaceTintColor: theme.colorScheme.surface,
child: Column(
children: [
Row(
children: [
Spacer(),
Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended")),
Spacer()
],
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: MarkdownBody(data: testPlayer.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)),
)
],
),
),
//if (testNews != null && testNews!.news.isNotEmpty)
Expanded(child: NewsThingy(testNews))
],
)
),
SizedBox(
width: 450.0,
child: Column(
children: [
],
),
)
],
));
}
}
class NewsThingy extends StatelessWidget{
final News news;
NewsThingy(this.news);
ListTile getNewsTile(NewsEntry news){
Map<String, String> gametypes = {
"40l": t.sprint,
"blitz": t.blitz,
"5mblast": "5,000,000 Blast"
};
// Individuly handle each entry type
switch (news.type) {
case "leaderboard":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white),
text: t.newsParts.leaderboardStart,
children: [
TextSpan(text: "${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.leaderboardMiddle),
TextSpan(text: "${gametypes[news.data["gametype"]]}", style: const TextStyle(fontWeight: FontWeight.bold)),
]
)
),
subtitle: Text(timestamp(news.timestamp)),
);
case "personalbest":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white),
text: t.newsParts.personalbest,
children: [
TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.personalbestMiddle),
TextSpan(text: news.data["gametype"] == "blitz" ? NumberFormat.decimalPattern().format(news.data["result"]) : get40lTime((news.data["result"]*1000).floor()), style: const TextStyle(fontWeight: FontWeight.bold)),
]
)
),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/icons/improvement-local.png",
height: 48,
width: 48,
errorBuilder: (context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 64, width: 64);
},
),
);
case "badge":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white),
text: t.newsParts.badgeStart,
children: [
TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.badgeEnd)
]
)
),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/tetrio_badges/${news.data["type"]}.png",
height: 48,
width: 48,
errorBuilder: (context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 64, width: 64);
},
),
);
case "rankup":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white),
text: t.newsParts.rankupStart,
children: [
TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.rankupEnd)
]
)
),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png",
height: 48,
width: 48,
errorBuilder: (context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 64, width: 64);
},
),
);
case "supporter":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white),
text: t.newsParts.supporterStart,
children: [
TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold))
]
)
),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/icons/supporter-tag.png",
height: 48,
width: 48,
errorBuilder: (context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 64, width: 64);
},
),
);
case "supporter_gift":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white),
text: t.newsParts.supporterGiftStart,
children: [
TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold))
]
)
),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/icons/supporter-tag.png",
height: 48,
width: 48,
errorBuilder: (context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 64, width: 64);
},
),
);
default: // if type is unknown
return ListTile(
title: Text(t.newsParts.unknownNews(type: news.type)),
subtitle: Text(timestamp(news.timestamp)),
);
}
}
@override
Widget build(BuildContext context) {
return Card(
surfaceTintColor: theme.colorScheme.surface,
child: SingleChildScrollView(
child: Column(
children: [
Row(
children: [
Spacer(),
Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended")),
Spacer()
]
),
for (NewsEntry entry in news.news) getNewsTile(entry)
],
),
),
);
}
}
class DistinguishmentThingy extends StatelessWidget{
final Distinguishment distinguishment;
DistinguishmentThingy(this.distinguishment, {super.key});
List<InlineSpan> getDistinguishmentTitle(String? text) {
// TWC champions don't have header in their distinguishments
if (distinguishment.type == "twc") return [const TextSpan(text: "TETR.IO World Champion", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.yellowAccent))];
// In case if it missing for some other reason, return this
if (text == null) return [const TextSpan(text: "Header is missing", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.redAccent))];
// Handling placeholders for logos
var exploded = text.split(" "); // wtf PHP reference?
List<InlineSpan> result = [];
for (String shit in exploded){
switch (shit) { // if %% thingy was found, insert svg of icon
case "%osk%":
result.add(WidgetSpan(child: Padding(
padding: const EdgeInsets.only(left: 8),
child: SvgPicture.asset("res/icons/osk.svg", height: 28),
)));
break;
case "%tetrio%":
result.add(WidgetSpan(child: Padding(
padding: const EdgeInsets.only(left: 8),
child: SvgPicture.asset("res/icons/tetrio-logo.svg", height: 28),
)));
break;
default: // if not, insert text span
result.add(TextSpan(text: " $shit", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white)));
}
}
return result;
}
/// Distinguishment title is barely predictable thing.
/// Receives [text], which is footer and returns sets of widgets for RichText widget
String getDistinguishmentSubtitle(String? text){
// TWC champions don't have footer in their distinguishments
if (distinguishment.type == "twc") return "${distinguishment.detail} TETR.IO World Championship";
// In case if it missing for some other reason, return this
if (text == null) return "Footer is missing";
// If everything ok, return as it is
return text;
}
Color getCardTint(String type, String detail){
switch(type){
case "staff":
switch(detail){
case "founder": return Color(0xAAFD82D4);
case "kagarin": return Color(0xAAFF0060);
case "team": return Color(0xAAFACC2E);
case "team-minor": return Color(0xAAF5BD45);
case "administrator": return Color(0xAAFF4E8A);
case "globalmod": return Color(0xAAE878FF);
case "communitymod": return Color(0xAA4E68FB);
case "alumni": return Color(0xAA6057DB);
default: return theme.colorScheme.surface;
}
case "champion":
switch (detail){
case "blitz":
case "40l": return Color(0xAACCF5F6);
case "league": return Color(0xAAFFDB31);
}
case "twc": return Color(0xAAFFDB31);
default: return theme.colorScheme.surface;
}
return theme.colorScheme.surface;
}
@override
Widget build(BuildContext context) {
return Card(
surfaceTintColor: getCardTint(distinguishment.type, distinguishment.detail??"null"),
child: Column(
children: [
Row(
children: [
Spacer(),
Text(t.distinguishment, style: TextStyle(fontFamily: "Eurostile Round Extended")),
Spacer()
],
),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: getDistinguishmentTitle(distinguishment.header),
),
),
Text(getDistinguishmentSubtitle(distinguishment.footer), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center),
],
),
);
}
}
class NewUserThingy extends StatelessWidget {
final TetrioPlayer player;
final bool showStateTimestamp;
final Function setState;
const NewUserThingy({super.key, required this.player, required this.showStateTimestamp, required this.setState});
Color roleColor(String role){
switch (role){
case "sysop":
return Color.fromARGB(255, 23, 165, 133);
case "admin":
return Color.fromARGB(255, 255, 78, 138);
case "mod":
return Color.fromARGB(255, 204, 128, 242);
case "halfmod":
return Color.fromARGB(255, 95, 118, 254);
case "bot":
return Color.fromARGB(255, 60, 93, 55);
case "banned":
return Color.fromARGB(255, 248, 28, 28);
default:
return Colors.white10;
}
}
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
return LayoutBuilder(builder: (context, constraints) {
bool bigScreen = constraints.maxWidth > 768;
double pfpHeight = 128;
int xpTableID = 0;
while (player.xp > xpTableScuffed.values.toList()[xpTableID]) {
xpTableID++;
}
return Card(
clipBehavior: Clip.antiAlias,
surfaceTintColor: theme.colorScheme.surface,
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
children: [
Container(
constraints: BoxConstraints(maxWidth: 960),
height: player.bannerRevision != null ? 218.0 : 138.0,
child: Stack(
//clipBehavior: Clip.none,
children: [
if (player.bannerRevision != null) Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}",
fit: BoxFit.cover,
height: 120,
//width: 450,
errorBuilder: (context, error, stackTrace) {
return Container();
},
),
Positioned(
top: player.bannerRevision != null ? 90.0 : 10.0,
left: 16.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: player.role == "banned"
? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,)
: player.avatarRevision != null
? Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}",
// TODO: osk banner can cause memory leak
fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) {
return Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight);
})
: Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight),
)
),
Positioned(
top: player.bannerRevision != null ? 120.0 : 40.0,
left: 160.0,
child: Text(player.username,
//softWrap: true,
overflow: TextOverflow.fade,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28,
)
),
),
Positioned(
top: player.bannerRevision != null ? 160.0 : 80.0,
left: 160.0,
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Chip(label: Text(player.role.toUpperCase(), style: TextStyle(shadows: textShadow),), padding: EdgeInsets.all(0.0), color: MaterialStatePropertyAll(roleColor(player.role))),
),
RichText(
text: TextSpan(
style: TextStyle(fontFamily: "Eurostile Round"),
children:
[
if (player.friendCount > 0) WidgetSpan(child: Icon(Icons.person), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic),
if (player.friendCount > 0) TextSpan(text: "${intf.format(player.friendCount)} "),
if (player.supporterTier > 0) WidgetSpan(child: Icon(player.supporterTier > 1 ? Icons.star : Icons.star_border, color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic),
if (player.supporterTier > 0) TextSpan(text: player.supporterTier.toString(), style: TextStyle(color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)),
]
)
)
],
),
),
Positioned(
top: player.bannerRevision != null ? 193.0 : 113.0,
left: 160.0,
child: RichText(
text: TextSpan(
style: TextStyle(fontFamily: "Eurostile Round"),
children: [
if (player.country != null) TextSpan(text: "${t.countries[player.country]}"),
TextSpan(text: "${player.registrationTime == null ? t.wasFromBeginning : '${timestamp(player.registrationTime!)}'}", style: TextStyle(color: Colors.grey))
]
)
)
),
Positioned(
top: player.bannerRevision != null ? 126.0 : 46.0,
right: 16.0,
child: RichText(
textAlign: TextAlign.end,
text: TextSpan(
style: TextStyle(fontFamily: "Eurostile Round"),
children: [
TextSpan(text: "Level ${intf.format(player.level.floor())}", recognizer: TapGestureRecognizer()..onTap = (){
showDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: Text("Level ${intf.format(player.level.floor())}"),
content: SingleChildScrollView(
child: ListBody(children: [
Text(
"${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(player.xp)} XP",
style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold)
),
Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 8),
child: SfLinearGauge(
minimum: 0,
maximum: 1,
interval: 1,
ranges: [
LinearGaugeRange(startValue: 0, endValue: player.level - player.level.floor(), color: Colors.cyanAccent),
LinearGaugeRange(startValue: 0, endValue: (player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross)
],
showTicks: true,
showLabels: false
),
),
Text("${t.statCellNum.xpProgress}: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"),
Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - player.xp)} ${t.statCellNum.xpLeft})")
]
),
),
actions: <Widget>[
TextButton(
child: Text("OK"),
onPressed: () {Navigator.of(context).pop();}
)
]
)
);
}),
TextSpan(text:"\n"),
TextSpan(text: player.gameTime.isNegative ? "-h --m" : playtime(player.gameTime), style: TextStyle(color: player.gameTime.isNegative ? Colors.grey : Colors.white), recognizer: TapGestureRecognizer()..onTap = (){
showDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: Text(t.exactGametime),
content: SingleChildScrollView(
child: ListBody(children: [
Text(
//"${intf.format(testPlayer.gameTime.inDays)} d\n${nonsecs.format(testPlayer.gameTime.inHours%24)} h\n${nonsecs.format(testPlayer.gameTime.inMinutes%60)} m\n${nonsecs.format(testPlayer.gameTime.inSeconds%60)} s\n${nonsecs3.format(testPlayer.gameTime.inMilliseconds%1000)} ms\n${nonsecs.format(testPlayer.gameTime.inMicroseconds%1000)} μs",
"${intf.format(testPlayer.gameTime.inDays)}d ${nonsecs.format(testPlayer.gameTime.inHours%24)}h ${nonsecs.format(testPlayer.gameTime.inMinutes%60)}m ${nonsecs.format(testPlayer.gameTime.inSeconds%60)}s ${nonsecs3.format(testPlayer.gameTime.inMilliseconds%1000)}ms ${nonsecs.format(testPlayer.gameTime.inMicroseconds%1000)}μs",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24)
),
]
),
),
actions: <Widget>[
TextButton(
child: Text("OK"),
onPressed: () {Navigator.of(context).pop();}
)
]
)
);
}),
TextSpan(text:"\n"),
TextSpan(text: "${player.gamesWon > -1 ? intf.format(player.gamesWon) : "---"}", style: TextStyle(color: player.gamesWon > -1 ? Colors.white : Colors.grey)),
TextSpan(text: "/${player.gamesPlayed > -1 ? intf.format(player.gamesPlayed) : "---"}", style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
]
)
)
)
],
),
),
// Row(
// mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: Icon(Icons.person_add), label: Text(t.track), style: ButtonStyle(shape: MaterialStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(8), right: Radius.zero))))),
// ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: Icon(Icons.balance), label: Text(t.compare), style: ButtonStyle(shape: MaterialStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.zero, right: Radius.circular(8))))))
// ]
// )
],
),
),
);
});
}
}

View File

@ -17,7 +17,7 @@ class SingleplayerRecordView extends StatelessWidget {
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar( appBar: AppBar(
title: Text("${ title: Text("${
switch (record.endContext.gameType){ switch (record.gamemode){
"40l" => t.sprint, "40l" => t.sprint,
"blitz" => t.blitz, "blitz" => t.blitz,
String() => "5000000 Blast", String() => "5000000 Blast",

View File

@ -58,6 +58,6 @@ class StateState extends State<StateView> {
headerSliverBuilder: (context, value) { headerSliverBuilder: (context, value) {
return [SliverToBoxAdapter(child: UserThingy(player: widget.state, showStateTimestamp: true, setState: _justUpdate))]; return [SliverToBoxAdapter(child: UserThingy(player: widget.state, showStateTimestamp: true, setState: _justUpdate))];
}, },
body: TLThingy(tl: widget.state.tlSeason1, userID: widget.state.userId, states: const [],)))); body: TLThingy(tl: widget.state.tlSeason1!, userID: widget.state.userId, states: const [], hidePreSeasonThingy: true,))));
} }
} }

View File

@ -61,7 +61,7 @@ class StatesState extends State<StatesView> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
return ListTile( return ListTile(
title: Text(timestamp(widget.states[index].state)), title: Text(timestamp(widget.states[index].state)),
subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: NumberFormat.compact().format(widget.states[index].tlSeason1.rd))), subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: NumberFormat.compact().format(widget.states[index].tlSeason1?.rd??0))),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.delete_forever), icon: const Icon(Icons.delete_forever),
onPressed: () { onPressed: () {

View File

@ -25,7 +25,7 @@ class RecentSingleplayerGames extends StatelessWidget{
for(RecordSingle record in recent.records) ListTile( for(RecordSingle record in recent.records) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: record))), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: record))),
leading: Text( leading: Text(
switch (record.endContext.gameType){ switch (record.gamemode){
"40l" => "40L", "40l" => "40L",
"blitz" => "BLZ", "blitz" => "BLZ",
"5mblast" => "5MB", "5mblast" => "5MB",
@ -34,15 +34,15 @@ class RecentSingleplayerGames extends StatelessWidget{
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9)
), ),
title: Text( title: Text(
switch (record.endContext.gameType){ switch (record.gamemode){
"40l" => get40lTime(record.endContext.finalTime.inMicroseconds), "40l" => get40lTime(record.stats.finalTime.inMicroseconds),
"blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(record.endContext.score)), "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(record.stats.score)),
"5mblast" => get40lTime(record.endContext.finalTime.inMicroseconds), "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds),
String() => "huh", String() => "huh",
}, },
style: const TextStyle(fontSize: 18)), style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(record.endContext) trailing: SpTrailingStats(record.stats, record.gamemode)
) )
], ],
); );

View File

@ -36,16 +36,16 @@ class SingleplayerRecord extends StatelessWidget {
if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)));
late MapEntry closestAverageBlitz; late MapEntry closestAverageBlitz;
late bool blitzBetterThanClosestAverage; late bool blitzBetterThanClosestAverage;
bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.score > blitzAverages[rank]! : null; bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.score > blitzAverages[rank]! : null;
late MapEntry closestAverageSprint; late MapEntry closestAverageSprint;
late bool sprintBetterThanClosestAverage; late bool sprintBetterThanClosestAverage;
bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.finalTime < sprintAverages[rank]! : null; bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.finalTime < sprintAverages[rank]! : null;
if (record!.endContext.gameType == "40l") { if (record!.gamemode == "40l") {
closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.endContext.finalTime).abs() < (b -record!.endContext.finalTime).abs() ? a : b)); closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.stats.finalTime).abs() < (b -record!.stats.finalTime).abs() ? a : b));
sprintBetterThanClosestAverage = record!.endContext.finalTime < closestAverageSprint.value; sprintBetterThanClosestAverage = record!.stats.finalTime < closestAverageSprint.value;
}else if (record!.endContext.gameType == "blitz"){ }else if (record!.gamemode == "blitz"){
closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-record!.endContext.score).abs() < (b -record!.endContext.score).abs() ? a : b)); closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-record!.stats.score).abs() < (b -record!.stats.score).abs() ? a : b));
blitzBetterThanClosestAverage = record!.endContext.score > closestAverageBlitz.value; blitzBetterThanClosestAverage = record!.stats.score > closestAverageBlitz.value;
} }
return LayoutBuilder( return LayoutBuilder(
@ -61,20 +61,20 @@ class SingleplayerRecord extends StatelessWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (record!.endContext.gameType == "40l") Padding(padding: const EdgeInsets.only(right: 8.0), if (record!.gamemode == "40l") Padding(padding: const EdgeInsets.only(right: 8.0),
child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96)
), ),
if (record!.endContext.gameType == "blitz") Padding(padding: const EdgeInsets.only(right: 8.0), if (record!.gamemode == "blitz") Padding(padding: const EdgeInsets.only(right: 8.0),
child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96)
), ),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (record!.endContext.gameType == "40l" && !hideTitle) Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), if (record!.gamemode == "40l" && !hideTitle) Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
if (record!.endContext.gameType == "blitz" && !hideTitle) Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), if (record!.gamemode == "blitz" && !hideTitle) Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
RichText(text: TextSpan( RichText(text: TextSpan(
text: record!.endContext.gameType == "40l" ? get40lTime(record!.endContext.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record!.endContext.score), text: record!.gamemode == "40l" ? get40lTime(record!.stats.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record!.stats.score),
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white),
), ),
), ),
@ -82,16 +82,16 @@ class SingleplayerRecord extends StatelessWidget {
text: "", text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [ children: [
if (record!.endContext.gameType == "40l" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( if (record!.gamemode == "40l" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
)) ))
else if (record!.endContext.gameType == "40l" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( else if (record!.gamemode == "40l" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)) ))
else if (record!.endContext.gameType == "blitz" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( else if (record!.gamemode == "blitz" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
)) ))
else if (record!.endContext.gameType == "blitz" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( else if (record!.gamemode == "blitz" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)), )),
if (record!.rank != null) TextSpan(text: "${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank!))), if (record!.rank != null) TextSpan(text: "${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank!))),
@ -103,29 +103,29 @@ class SingleplayerRecord extends StatelessWidget {
],), ],),
], ],
), ),
if (record!.endContext.gameType == "40l") Wrap( if (record!.gamemode == "40l") Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
spacing: 20, spacing: 20,
children: [ children: [
StatCellNum(playerStat: record!.endContext.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.stats.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
], ],
), ),
if (record!.endContext.gameType == "blitz") Wrap( if (record!.gamemode == "blitz") Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20, spacing: 20,
children: [ children: [
StatCellNum(playerStat: record!.endContext.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.stats.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) StatCellNum(playerStat: record!.stats.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true)
], ],
), ),
FinesseThingy(record?.endContext.finesse, record?.endContext.finessePercentage), FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage),
LineclearsThingy(record!.endContext.clears, record!.endContext.lines, record!.endContext.holds, record!.endContext.tSpins), LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins),
if (record!.endContext.gameType == "40l") Text("${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kps)} KPS"), if (record!.gamemode == "40l") Text("${record!.stats.inputs} KP • ${f2.format(record!.stats.kps)} KPS"),
if (record!.endContext.gameType == "blitz") Text("${record!.endContext.piecesPlaced} P • ${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kpp)} KPP • ${f2.format(record!.endContext.kps)} KPS"), if (record!.gamemode == "blitz") Text("${record!.stats.piecesPlaced} P • ${record!.stats.inputs} KP • ${f2.format(record!.stats.kpp)} KPP • ${f2.format(record!.stats.kps)} KPS"),
if (record != null) Wrap( if (record != null) Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
@ -141,15 +141,15 @@ class SingleplayerRecord extends StatelessWidget {
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9)
), ),
title: Text( title: Text(
switch (stream!.records[i].endContext.gameType){ switch (stream!.records[i].gamemode){
"40l" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds), "40l" => get40lTime(stream!.records[i].stats.finalTime.inMicroseconds),
"blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(stream!.records[i].endContext.score)), "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(stream!.records[i].stats.score)),
"5mblast" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds), "5mblast" => get40lTime(stream!.records[i].stats.finalTime.inMicroseconds),
String() => "huh", String() => "huh",
}, },
style: const TextStyle(fontSize: 18)), style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(stream!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), subtitle: Text(timestamp(stream!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(stream!.records[i].endContext) trailing: SpTrailingStats(stream!.records[i].stats, stream!.records[i].gamemode)
) )
] ]
), ),

View File

@ -3,9 +3,10 @@ import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
class SpTrailingStats extends StatelessWidget{ class SpTrailingStats extends StatelessWidget{
final EndContextSingle endContext; final ResultsStats endContext;
final String gamemode;
const SpTrailingStats(this.endContext, {super.key}); const SpTrailingStats(this.endContext, this.gamemode, {super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -16,7 +17,7 @@ class SpTrailingStats extends StatelessWidget{
children: [ children: [
Text("${endContext.piecesPlaced} P, ${f2.format(endContext.pps)} PPS", style: style, textAlign: TextAlign.right), Text("${endContext.piecesPlaced} P, ${f2.format(endContext.pps)} PPS", style: style, textAlign: TextAlign.right),
Text("${intf.format(endContext.finessePercentage*100)}% F, ${endContext.finesse?.faults} FF", style: style, textAlign: TextAlign.right), Text("${intf.format(endContext.finessePercentage*100)}% F, ${endContext.finesse?.faults} FF", style: style, textAlign: TextAlign.right),
Text(switch(endContext.gameType){ Text(switch(gamemode){
"40l" => "${f2.format(endContext.kps)} KPS, ${f2.format(endContext.kpp)} KPP", "40l" => "${f2.format(endContext.kps)} KPS, ${f2.format(endContext.kpp)} KPP",
"blitz" => "${intf.format(endContext.spp)} SPP, lvl ${endContext.level}", "blitz" => "${intf.format(endContext.spp)} SPP, lvl ${endContext.level}",
"5mblast" => "${intf.format(endContext.spp)} SPP, ${endContext.lines} L", "5mblast" => "${intf.format(endContext.spp)} SPP, ${endContext.lines} L",

View File

@ -27,9 +27,9 @@ class TLRatingThingy extends StatelessWidget{
List<String> formatedGlicko = f4.format(tlData.glicko).split(decimalSeparator); List<String> formatedGlicko = f4.format(tlData.glicko).split(decimalSeparator);
List<String> formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); List<String> formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator);
DateTime now = DateTime.now(); DateTime now = DateTime.now();
bool beforeS1end = now.isBefore(seasonEnd); //bool beforeS1end = now.isBefore(seasonEnd);
int daysLeft = seasonEnd.difference(now).inDays; //int daysLeft = seasonEnd.difference(now).inDays;
int safeRD = min(100, (100 + ((tlData.rd! >= 100 && tlData.decaying) ? 7 : max(0, 7 - (lastMatchPlayed != null ? now.difference(lastMatchPlayed!).inDays : 7))) - daysLeft).toInt()); //int safeRD = min(100, (100 + ((tlData.rd! >= 100 && tlData.decaying) ? 7 : max(0, 7 - (lastMatchPlayed != null ? now.difference(lastMatchPlayed!).inDays : 7))) - daysLeft).toInt());
return Wrap( return Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
alignment: WrapAlignment.spaceAround, alignment: WrapAlignment.spaceAround,
@ -91,7 +91,7 @@ class TLRatingThingy extends StatelessWidget{
TextSpan(text: "${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.rating)} TR • RD: " : "Glicko: ${f2.format(tlData.glicko!)}±"}"), TextSpan(text: "${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.rating)} TR • RD: " : "Glicko: ${f2.format(tlData.glicko!)}±"}"),
TextSpan(text: f2.format(tlData.rd!), style: tlData.decaying ? TextStyle(color: tlData.rd! > 98 ? Colors.red : Colors.yellow) : null), TextSpan(text: f2.format(tlData.rd!), style: tlData.decaying ? TextStyle(color: tlData.rd! > 98 ? Colors.red : Colors.yellow) : null),
if (tlData.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: tlData.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), if (tlData.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: tlData.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic),
if (beforeS1end) tlData.rd! <= safeRD ? TextSpan(text: " (Safe)", style: TextStyle(color: Colors.greenAccent)) : TextSpan(text: " (> ${safeRD} RD !!!)", style: TextStyle(color: Colors.redAccent)) //if (beforeS1end) tlData.rd! <= safeRD ? TextSpan(text: " (Safe)", style: TextStyle(color: Colors.greenAccent)) : TextSpan(text: " (> ${safeRD} RD !!!)", style: TextStyle(color: Colors.redAccent))
], ],
), ),
), ),

View File

@ -2,9 +2,11 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path/path.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart';
import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/colors_functions.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart';
@ -24,6 +26,7 @@ class TLThingy extends StatefulWidget {
final List<TetrioPlayer> states; final List<TetrioPlayer> states;
final bool showTitle; final bool showTitle;
final bool bot; final bool bot;
final bool hidePreSeasonThingy;
final bool guest; final bool guest;
final double? topTR; final double? topTR;
final PlayerLeaderboardPosition? lbPositions; final PlayerLeaderboardPosition? lbPositions;
@ -35,7 +38,7 @@ class TLThingy extends StatefulWidget {
final double? nextRankCutoffGlicko; final double? nextRankCutoffGlicko;
final double? nextRankTarget; final double? nextRankTarget;
final DateTime? lastMatchPlayed; final DateTime? lastMatchPlayed;
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, this.lastMatchPlayed}); const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.hidePreSeasonThingy=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget, this.lastMatchPlayed});
@override @override
State<TLThingy> createState() => _TLThingyState(); State<TLThingy> createState() => _TLThingyState();
@ -48,8 +51,9 @@ class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
late RangeValues _currentRangeValues; late RangeValues _currentRangeValues;
late List<TetrioPlayer> sortedStates; late List<TetrioPlayer> sortedStates;
late Timer _countdownTimer; late Timer _countdownTimer;
Duration seasonLeft = seasonEnd.difference(DateTime.now()); //Duration seasonLeft = seasonEnd.difference(DateTime.now());
Duration postSeasonLeft = seasonStart.difference(DateTime.now());
@override @override
void initState() { void initState() {
_currentRangeValues = const RangeValues(0, 1); _currentRangeValues = const RangeValues(0, 1);
@ -61,7 +65,8 @@ class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
Durations.extralong4, Durations.extralong4,
(Timer timer) { (Timer timer) {
setState(() { setState(() {
seasonLeft = seasonEnd.difference(DateTime.now()); //seasonLeft = seasonEnd.difference(DateTime.now());
postSeasonLeft = seasonStart.difference(DateTime.now());
}); });
}, },
); );
@ -80,6 +85,47 @@ class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
String decimalSeparator = f2.symbols.DECIMAL_SEP; String decimalSeparator = f2.symbols.DECIMAL_SEP;
List<String> estTRformated = currentTl.estTr != null ? f2.format(currentTl.estTr!.esttr).split(decimalSeparator) : []; List<String> estTRformated = currentTl.estTr != null ? f2.format(currentTl.estTr!.esttr).split(decimalSeparator) : [];
List<String> estTRaccFormated = currentTl.esttracc != null ? intFDiff.format(currentTl.esttracc!).split(".") : []; List<String> estTRaccFormated = currentTl.esttracc != null ? intFDiff.format(currentTl.esttracc!).split(".") : [];
if (DateTime.now().isBefore(seasonStart) && !widget.hidePreSeasonThingy) {
return Center(child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(t.postSeason.toUpperCase(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center),
Text(t.seasonStarts, textAlign: TextAlign.center),
const Spacer(),
Text(countdown(postSeasonLeft), textAlign: TextAlign.center, style: const TextStyle(fontSize: 36.0),),
if (prefs.getBool("hideDanMessadge") != true) const Spacer(),
if (prefs.getBool("hideDanMessadge") != true) Card(
child: Container(
constraints: const BoxConstraints(maxWidth: 450.0),
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
children: [
Text(
t.myMessadgeHeader,
textAlign: TextAlign.center,
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.bold)
),
const Spacer(),
IconButton(onPressed: (){setState(() {
prefs.setBool("hideDanMessadge", true);
});}, icon: const Icon(Icons.close))
],
),
Text(t.myMessadgeBody, textAlign: TextAlign.center),
],
),
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(t.preSeasonMessage(n: postSeasonLeft.inDays >= 14 ? "1" : "2"), textAlign: TextAlign.center),
),
],
));
}
if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,)); if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,));
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
bool bigScreen = constraints.maxWidth >= 768; bool bigScreen = constraints.maxWidth >= 768;
@ -90,8 +136,8 @@ class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
return Column( return Column(
children: [ children: [
if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft))) //if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft)))
else Text(t.seasonEnded), //else Text(t.seasonEnded),
if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)), if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)),
textAlign: TextAlign.center,), textAlign: TextAlign.center,),
if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(),
@ -105,7 +151,7 @@ class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
if (values.start.round() == 0){ if (values.start.round() == 0){
currentTl = widget.tl; currentTl = widget.tl;
}else{ }else{
currentTl = sortedStates[values.start.round()-1].tlSeason1; currentTl = sortedStates[values.start.round()-1].tlSeason1!;
} }
if (values.end.round() == 0){ if (values.end.round() == 0){
oldTl = widget.tl; oldTl = widget.tl;

View File

@ -87,6 +87,12 @@
"verdictBetter": "better", "verdictBetter": "better",
"verdictWorse": "worse", "verdictWorse": "worse",
"smooth": "Smooth", "smooth": "Smooth",
"postSeason": "Off-season",
"seasonStarts": "Season starts in:",
"myMessadgeHeader": "A messadge from dan63",
"myMessadgeBody": "TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.",
"preSeasonMessage": "Right now you can play unranked FT3 matches against absolutely random player.\nSeason ${n} rules applied",
"nanow": "Not avaliable for now...",
"seasonEnds": "Season ends in ${countdown}", "seasonEnds": "Season ends in ${countdown}",
"seasonEnded": "Season has ended", "seasonEnded": "Season has ended",
"gamesUntilRanked": "${left} games until being ranked", "gamesUntilRanked": "${left} games until being ranked",

View File

@ -87,6 +87,12 @@
"verdictBetter": "Лучше", "verdictBetter": "Лучше",
"verdictWorse": "Хуже", "verdictWorse": "Хуже",
"smooth": "Гладкий", "smooth": "Гладкий",
"postSeason": "Внесезонье",
"seasonStarts": "Сезон начнётся через:",
"myMessadgeHeader": "Сообщение от dan63",
"myMessadgeBody": "TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.",
"preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона",
"nanow": "Пока недоступно...",
"seasonEnds": "Сезон закончится через ${countdown}", "seasonEnds": "Сезон закончится через ${countdown}",
"seasonEnded": "Сезон закончился", "seasonEnded": "Сезон закончился",
"gamesUntilRanked": "${left} матчей до получения рейтинга", "gamesUntilRanked": "${left} матчей до получения рейтинга",