From b62369314801716ce86dbe24e55a7e5864481f74 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 27 Jul 2024 22:10:45 +0300 Subject: [PATCH] swamp water --- lib/data_objects/tetrio.dart | 390 ++++++++++--- lib/gen/strings.g.dart | 28 +- lib/main.dart | 44 +- lib/services/tetrio_crud.dart | 84 ++- lib/utils/relative_timestamps.dart | 11 +- lib/views/main_view.dart | 218 ++++---- lib/views/main_view_tiles.dart | 714 ++++++++++++++++++++++++ lib/views/singleplayer_record_view.dart | 2 +- lib/views/state_view.dart | 2 +- lib/views/states_view.dart | 2 +- lib/widgets/recent_sp_games.dart | 12 +- lib/widgets/singleplayer_record.dart | 68 +-- lib/widgets/sp_trailing_stats.dart | 7 +- lib/widgets/tl_rating_thingy.dart | 8 +- lib/widgets/tl_thingy.dart | 60 +- res/i18n/strings.i18n.json | 6 + res/i18n/strings_ru.i18n.json | 6 + 17 files changed, 1365 insertions(+), 297 deletions(-) create mode 100644 lib/views/main_view_tiles.dart diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 45ab41d..7a8b3f1 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -60,7 +60,8 @@ const Map rankTargets = { "d+": 606, "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 { tr, glicko, @@ -261,9 +262,7 @@ class TetrioPlayer { bool? badstanding; String? botmaster; Connections? connections; - late TetraLeagueAlpha tlSeason1; - List sprint = []; - List blitz = []; + TetraLeagueAlpha? tlSeason1; TetrioZen? zen; Distinguishment? distinguishment; DateTime? cachedUntil; @@ -290,8 +289,6 @@ class TetrioPlayer { this.botmaster, required this.connections, required this.tlSeason1, - required this.sprint, - required this.blitz, this.zen, this.distinguishment, this.cachedUntil @@ -318,7 +315,7 @@ class TetrioPlayer { country = json['country']; supporterTier = json['supporter_tier'] ?? 0; verified = json['verified'] ?? false; - tlSeason1 = TetraLeagueAlpha.fromJson(json['league'], stateTime); + tlSeason1 = json['league'] != null ? TetraLeagueAlpha.fromJson(json['league'], stateTime) : null; avatarRevision = json['avatar_revision']; bannerRevision = json['banner_revision']; bio = json['bio']; @@ -344,7 +341,7 @@ class TetrioPlayer { if (country != null) data['country'] = country; if (supporterTier > 0) data['supporter_tier'] = supporterTier; if (verified) data['verified'] = verified; - data['league'] = tlSeason1.toJson(); + data['league'] = tlSeason1?.toJson(); if (distinguishment != null) data['distinguishment'] = distinguishment?.toJson(); if (avatarRevision != null) data['avatar_revision'] = avatarRevision; if (bannerRevision != null) data['banner_revision'] = bannerRevision; @@ -380,12 +377,12 @@ class TetrioPlayer { } bool checkForRetrivedHistory(covariant TetrioPlayer other) { - return tlSeason1.lessStrictCheck(other.tlSeason1); + return tlSeason1!.lessStrictCheck(other.tlSeason1!); } TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard() => TetrioPlayerFromLeaderboard( - userId, username, role, xp, country, supporterTier > 0, 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); + 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); @override String toString() { @@ -395,59 +392,59 @@ class TetrioPlayer { num? getStatByEnum(Stats stat){ switch (stat) { case Stats.tr: - return tlSeason1.rating; + return tlSeason1?.rating; case Stats.glicko: - return tlSeason1.glicko; + return tlSeason1?.glicko; case Stats.rd: - return tlSeason1.rd; + return tlSeason1?.rd; case Stats.gp: - return tlSeason1.gamesPlayed; + return tlSeason1?.gamesPlayed; case Stats.gw: - return tlSeason1.gamesWon; + return tlSeason1?.gamesWon; case Stats.wr: - return tlSeason1.winrate; + return tlSeason1?.winrate; case Stats.apm: - return tlSeason1.apm; + return tlSeason1?.apm; case Stats.pps: - return tlSeason1.pps; + return tlSeason1?.pps; case Stats.vs: - return tlSeason1.vs; + return tlSeason1?.vs; case Stats.app: - return tlSeason1.nerdStats?.app; + return tlSeason1?.nerdStats?.app; case Stats.dss: - return tlSeason1.nerdStats?.dss; + return tlSeason1?.nerdStats?.dss; case Stats.dsp: - return tlSeason1.nerdStats?.dsp; + return tlSeason1?.nerdStats?.dsp; case Stats.appdsp: - return tlSeason1.nerdStats?.appdsp; + return tlSeason1?.nerdStats?.appdsp; case Stats.vsapm: - return tlSeason1.nerdStats?.vsapm; + return tlSeason1?.nerdStats?.vsapm; case Stats.cheese: - return tlSeason1.nerdStats?.cheese; + return tlSeason1?.nerdStats?.cheese; case Stats.gbe: - return tlSeason1.nerdStats?.gbe; + return tlSeason1?.nerdStats?.gbe; case Stats.nyaapp: - return tlSeason1.nerdStats?.nyaapp; + return tlSeason1?.nerdStats?.nyaapp; case Stats.area: - return tlSeason1.nerdStats?.area; + return tlSeason1?.nerdStats?.area; case Stats.eTR: - return tlSeason1.estTr?.esttr; + return tlSeason1?.estTr?.esttr; case Stats.acceTR: - return tlSeason1.esttracc; + return tlSeason1?.esttracc; case Stats.acceTRabs: - return tlSeason1.esttracc?.abs(); + return tlSeason1?.esttracc?.abs(); case Stats.opener: - return tlSeason1.playstyle?.opener; + return tlSeason1?.playstyle?.opener; case Stats.plonk: - return tlSeason1.playstyle?.plonk; + return tlSeason1?.playstyle?.plonk; case Stats.infDS: - return tlSeason1.playstyle?.infds; + return tlSeason1?.playstyle?.infds; case Stats.stride: - return tlSeason1.playstyle?.stride; + return tlSeason1?.playstyle?.stride; 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: - 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); } +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 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 { late String badgeId; late String label; @@ -652,8 +667,7 @@ class Finesse { } } -class EndContextSingle { - late String gameType; +class ResultsStats { late int topBtB; late int topCombo; late int holds; @@ -662,7 +676,7 @@ class EndContextSingle { late int piecesPlaced; late int lines; late int score; - late double seed; + late int seed; late Duration finalTime; late int tSpins; late Clears clears; @@ -674,8 +688,8 @@ class EndContextSingle { double get kps => inputs / (finalTime.inMicroseconds / 1000000); double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0; - EndContextSingle( - {required this.gameType, + ResultsStats( + { required this.topBtB, required this.topCombo, required this.holds, @@ -690,12 +704,12 @@ class EndContextSingle { required this.clears, required this.finesse}); - EndContextSingle.fromJson(Map json) { - seed = json['seed'].toDouble(); + ResultsStats.fromJson(Map json) { + seed = json['seed']; lines = json['lines']; inputs = json['inputs']; holds = json['holds'] ?? 0; - finalTime = doubleMillisecondsToDuration(json['finalTime'].toDouble()); + finalTime = doubleMillisecondsToDuration(json['finaltime'].toDouble()); score = json['score']; level = json['level']; topCombo = json['topcombo']; @@ -704,7 +718,6 @@ class EndContextSingle { piecesPlaced = json['piecesplaced']; clears = Clears.fromJson(json['clears']); finesse = json.containsKey("finesse") ? Finesse.fromJson(json['finesse']) : null; - gameType = json['gametype']; } Map toJson() { @@ -722,7 +735,6 @@ class EndContextSingle { data['clears'] = clears.toJson(); if (finesse != null) data['finesse'] = finesse!.toJson(); data['finalTime'] = finalTime; - data['gametype'] = gameType; return data; } } @@ -873,6 +885,119 @@ class TetraLeagueAlphaStream{ } } +class TetraLeagueBetaStream{ + late String id; + List records = []; + + TetraLeagueBetaStream({required this.id, required this.records}); + + TetraLeagueBetaStream.fromJson(List json, String userID) { + id = userID; + for (var entry in json) records.add(BetaRecord.fromJson(entry)); + } + + addFromAlphaStream(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 json){ + id = json['_id']; + replayID = json['replayid']; + gamemode = json['gamemode']; + ts = DateTime.parse(json['ts']); + enemyUsername = json['otherusers'][0]['username']; + enemyID = json['otherusers'][0]['id']; + results = BetaLeagueResults.fromJson(json['results']); + } +} + +class BetaLeagueResults{ + List leaderboard = []; + List> rounds = []; + + BetaLeagueResults({required this.leaderboard, required this.rounds}); + + BetaLeagueResults.fromJson(Map json){ + for (var lbEntry in json['leaderboard']) leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry)); + for (var roundEntry in json['rounds']){ + List round = []; + for (var r in roundEntry) round.add(BetaLeagueRound.fromJson(r)); + rounds.add(round); + } + } +} + +class BetaLeagueLeaderboardEntry{ + late String id; + late String username; + late int naturalorder; + late int wins; + late BetaLeagueStats stats; + + BetaLeagueLeaderboardEntry({required this.id, required this.username, required this.naturalorder, required this.wins, required this.stats}); + + BetaLeagueLeaderboardEntry.fromJson(Map json){ + id = json['id']; + username = json['username']; + naturalorder = json['naturalorder']; + wins = json['wins']; + stats = BetaLeagueStats.fromJson(json['stats']); + } +} + +class BetaLeagueStats{ + late double apm; + late double pps; + late double vs; + late int garbageSent; + late int garbageReceived; + late int kills; + late double altitude; + late int rank; + int? targetingFactor; + int? targetingRace; + late NerdStats nerdStats; + late EstTr estTr; + late Playstyle playstyle; + + BetaLeagueStats({required this.apm, required this.pps, required this.vs, required this.garbageSent, required this.garbageReceived, required this.kills, required this.altitude, required this.rank, required this.nerdStats, required this.estTr, required this.playstyle}); + + BetaLeagueStats.fromJson(Map 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 json){ + id = json['id']; + username = json['username']; + active = json['active']; + naturalorder = json['naturalorder']; + alive = json['alive']; + lifetime = Duration(milliseconds: json['lifetime']); + stats = BetaLeagueStats.fromJson(json['stats']); + } +} + class TetraLeagueAlphaRecord{ late String replayId; late String ownId; @@ -1132,26 +1369,28 @@ class RecordSingle { late String userId; late String replayId; late String ownId; + late String gamemode; late DateTime timestamp; - late EndContextSingle endContext; + late ResultsStats stats; 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 json, int? ran) { //developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio"); ownId = json['_id']; - endContext = EndContextSingle.fromJson(json['endcontext']); + gamemode = json['gamemode']; + stats = ResultsStats.fromJson(json['results']['stats']); replayId = json['replayid']; timestamp = DateTime.parse(json['ts']); - userId = json['user']['_id']; + userId = json['user']['id']; rank = ran; } Map toJson() { final Map data = {}; data['_id'] = ownId; - data['endcontext'] = endContext.toJson(); + data['results']['stats'] = stats.toJson(); data['ismulti'] = false; data['replayid'] = replayId; data['ts'] = timestamp; @@ -1470,8 +1709,8 @@ class TetrioPlayersLeaderboard { avgPPS += entry.pps; avgVS += entry.vs; avgTR += entry.rating; - avgGlicko += entry.glicko; - avgRD += entry.rd; + if (entry.glicko != null) avgGlicko += entry.glicko!; + if (entry.rd != null) avgRD += entry.rd!; avgAPP += entry.nerdStats.app; avgVSAPM += entry.nerdStats.vsapm; avgDSS += entry.nerdStats.dss; @@ -1494,13 +1733,13 @@ class TetrioPlayersLeaderboard { lowestTRid = entry.userId; lowestTRnick = entry.username; } - if (entry.glicko < lowestGlicko){ - lowestGlicko = entry.glicko; + if (entry.glicko != null && entry.glicko! < lowestGlicko){ + lowestGlicko = entry.glicko!; lowestGlickoID = entry.userId; lowestGlickoNick = entry.username; } - if (entry.rd < lowestRD){ - lowestRD = entry.rd; + if (entry.rd != null && entry.rd! < lowestRD){ + lowestRD = entry.rd!; lowestRdID = entry.userId; lowestRdNick = entry.username; } @@ -1614,13 +1853,13 @@ class TetrioPlayersLeaderboard { highestTRid = entry.userId; highestTRnick = entry.username; } - if (entry.glicko > highestGlicko){ - highestGlicko = entry.glicko; + if (entry.glicko != null && entry.glicko! > highestGlicko){ + highestGlicko = entry.glicko!; highestGlickoID = entry.userId; highestGlickoNick = entry.username; } - if (entry.rd > highestRD){ - highestRD = entry.rd; + if (entry.rd != null && entry.rd! > highestRD){ + highestRD = entry.rd!; highestRdID = entry.userId; highestRdNick = entry.username; } @@ -1929,7 +2168,7 @@ class TetrioPlayersLeaderboard { } PlayerLeaderboardPosition? getLeaderboardPosition(TetrioPlayer user) { - if (user.tlSeason1.gamesPlayed == 0) return null; + if (user.tlSeason1?.gamesPlayed == 0) return null; bool fakePositions = false; late List copyOfLeaderboard; if (leaderboard.indexWhere((element) => element.userId == user.userId) == -1){ @@ -2029,16 +2268,15 @@ class TetrioPlayerFromLeaderboard { late String role; late double xp; String? country; - late bool supporter; late bool verified; late DateTime timestamp; late int gamesPlayed; late int gamesWon; late double rating; - late double glicko; - late double rd; + late double? glicko; + late double? rd; late String rank; - late String bestRank; + late String? bestRank; late double apm; late double pps; late double vs; @@ -2053,7 +2291,6 @@ class TetrioPlayerFromLeaderboard { this.role, this.xp, this.country, - this.supporter, this.verified, this.timestamp, this.gamesPlayed, @@ -2081,14 +2318,13 @@ class TetrioPlayerFromLeaderboard { role = json['role']; xp = json['xp'].toDouble(); country = json['country']; - supporter = json['supporter']; verified = json['verified']; timestamp = ts; - gamesPlayed = json['league']['gamesplayed']; - gamesWon = json['league']['gameswon']; - rating = json['league']['rating'].toDouble(); - glicko = json['league']['glicko'].toDouble(); - rd = json['league']['rd'].toDouble(); + gamesPlayed = json['league']['gamesplayed'] as int; + gamesWon = json['league']['gameswon'] as int; + rating = json['league']['rating'] != null ? json['league']['rating'].toDouble() : 0; + glicko = json['league']['glicko'] != null ? json['league']['glicko'].toDouble() : null; + rd = json['league']['rd'] != null ? json['league']['rd'].toDouble() : null; rank = json['league']['rank']; bestRank = json['league']['bestrank']; apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00; @@ -2105,9 +2341,9 @@ class TetrioPlayerFromLeaderboard { case Stats.tr: return rating; case Stats.glicko: - return glicko; + return glicko??-1; case Stats.rd: - return rd; + return rd??-1; case Stats.gp: return gamesPlayed; case Stats.gw: diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index e3b9fb2..4c181cc 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 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 // ignore_for_file: type=lint @@ -222,6 +222,12 @@ class Translations implements BaseTranslations { String get verdictBetter => 'better'; String get verdictWorse => 'worse'; 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 get seasonEnded => 'Season has ended'; 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 verdictWorse => 'Хуже'; @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 get seasonEnded => 'Сезон закончился'; @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; @@ -1608,6 +1620,12 @@ extension on Translations { case 'verdictBetter': return 'better'; case 'verdictWorse': return 'worse'; 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 'seasonEnded': return 'Season has ended'; case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; @@ -2221,6 +2239,12 @@ extension on _StringsRu { case 'verdictBetter': return 'Лучше'; case 'verdictWorse': 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 'seasonEnded': return 'Сезон закончился'; case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; diff --git a/lib/main.dart b/lib/main.dart index 162f783..fdc1407 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,39 +25,15 @@ import 'package:go_router/go_router.dart'; late final PackageInfo packageInfo; late SharedPreferences prefs; late TetrioService teto; -ThemeData theme = ThemeData(fontFamily: 'Eurostile Round', colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white), scaffoldBackgroundColor: Colors.black); - -// Future computeIsolate(Future Function() function) async { -// final receivePort = ReceivePort(); -// var rootToken = RootIsolateToken.instance!; -// await Isolate.spawn<_IsolateData>( -// _isolateEntry, -// _IsolateData( -// 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, -// }); -// } +ThemeData theme = ThemeData( + fontFamily: 'Eurostile Round', + colorScheme: const ColorScheme.dark( + primary: Colors.cyanAccent, + surface: Color.fromARGB(255, 10, 10, 10), + secondary: Colors.white + ), + scaffoldBackgroundColor: Colors.black +); final router = GoRouter( initialLocation: "/", @@ -189,4 +165,4 @@ class MyAppState extends State { theme: theme ); } -} +} \ No newline at end of file diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index f821c73..04f5a31 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -557,8 +557,6 @@ class TetrioService extends DB { nextAt: -1, prevAt: -1 ), - sprint: [], - blitz: [] ); history.add(state); } @@ -601,7 +599,7 @@ class TetrioService extends DB { } /// Docs later - Future> fetchAndSaveOldTLmatches(String userID) async { + Future fetchAndSaveOldTLmatches(String userID) async { Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID}); @@ -616,7 +614,7 @@ class TetrioService extends DB { case 200: TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID); saveTLMatchesFromStream(stream); - return stream.records; + return stream; case 404: developer.log("fetchAndSaveOldTLmatches: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); throw TetrioHistoryNotExist(); @@ -650,7 +648,7 @@ class TetrioService extends DB { if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); } else { - url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); + url = Uri.https('ch.tetr.io', 'api/users/by/league'); } try{ final response = await client.get(url); @@ -660,7 +658,7 @@ class TetrioService extends DB { _lbPositions.clear(); var rawJson = jsonDecode(response.body); 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"); //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; _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). /// Throws an exception if fails to retrieve. - Future fetchTLStream(String userID) async { - TetraLeagueAlphaStream? cached = _cache.get(userID, TetraLeagueAlphaStream); + Future fetchTLStream(String userID) async { + TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueAlphaStream); if (cached != null) return cached; Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()}); } 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 { final response = await client.get(url); @@ -774,7 +772,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: 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']); developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); return stream; @@ -941,6 +939,50 @@ class TetrioService extends DB { } } + Future 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. Future createPlayer(TetrioPlayer tetrioPlayer) async { await ensureDbIsOpen(); @@ -1131,7 +1173,7 @@ class TetrioService extends DB { var json = jsonDecode(response.body); if (json['success']) { // 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']); developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); return player; @@ -1175,14 +1217,14 @@ class TetrioService extends DB { return data; } - Future fetchTracked() async { - for (String userID in (await getAllPlayerToTrack())) { - TetrioPlayer player = await fetchPlayer(userID); - storeState(player); - sleep(Durations.extralong4); - TetraLeagueAlphaStream matches = await fetchTLStream(userID); - saveTLMatchesFromStream(matches); - sleep(Durations.extralong4); - } - } + // Future fetchTracked() async { + // for (String userID in (await getAllPlayerToTrack())) { + // TetrioPlayer player = await fetchPlayer(userID); + // storeState(player); + // sleep(Durations.extralong4); + // TetraLeagueBetaStream matches = await fetchTLStream(userID); + // saveTLMatchesFromStream(matches); + // sleep(Durations.extralong4); + // } + // } } diff --git a/lib/utils/relative_timestamps.dart b/lib/utils/relative_timestamps.dart index 833c33e..f73a267 100644 --- a/lib/utils/relative_timestamps.dart +++ b/lib/utils/relative_timestamps.dart @@ -3,7 +3,8 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; 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); /// Returns string, that represents time difference between [dateTime] and now @@ -77,5 +78,11 @@ String readableTimeDifference(Duration a, Duration b){ } 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"; } \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index d5a0d95..8375696 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -155,7 +155,8 @@ class _MainState extends State with TickerProviderStateMixin { // Requesting Tetra League (alpha), records, news and top TR of player late List requests; - late TetraLeagueAlphaStream tlStream; + late Summaries summaries; + late TetraLeagueBetaStream tlStream; late UserRecords records; late News news; late SingleplayerStream recent; @@ -164,24 +165,26 @@ class _MainState extends State with TickerProviderStateMixin { late TetrioPlayerFromLeaderboard? topOne; late TopTr? topTR; requests = await Future.wait([ // all at once (7 requests to oskware lmao) + teto.fetchSummaries(_searchFor), teto.fetchTLStream(_searchFor), - teto.fetchRecords(_searchFor), + //teto.fetchRecords(_searchFor), teto.fetchNews(_searchFor), - teto.fetchSingleplayerStream(_searchFor, "any_userrecent"), - teto.fetchSingleplayerStream(_searchFor, "40l_userbest"), - teto.fetchSingleplayerStream(_searchFor, "blitz_userbest"), - prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>>[]), - (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), - (me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR + // teto.fetchSingleplayerStream(_searchFor, "any_userrecent"), + // teto.fetchSingleplayerStream(_searchFor, "40l_userbest"), + // teto.fetchSingleplayerStream(_searchFor, "blitz_userbest"), + // prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>>[]), + //(me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), + //(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; - records = requests[1] as UserRecords; + summaries = requests[0] as Summaries; + tlStream = requests[1] as TetraLeagueBetaStream; + // records = requests[1] as UserRecords; news = requests[2] as News; - recent = requests[3] as SingleplayerStream; - sprint = requests[4] as SingleplayerStream; - blitz = requests[5] as SingleplayerStream; - topOne = requests[7] as TetrioPlayerFromLeaderboard?; - topTR = requests[8] as TopTr?; // No TR - no Top TR + // recent = requests[3] as SingleplayerStream; + // sprint = requests[4] as SingleplayerStream; + // blitz = requests[5] as SingleplayerStream; + // topOne = requests[7] as TetrioPlayerFromLeaderboard?; + // topTR = requests[8] as TopTr?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); if (prefs.getBool("showPositions") == true){ @@ -193,37 +196,34 @@ class _MainState extends State with TickerProviderStateMixin { if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } } - Map? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[6] as Cutoffs?)?.tr; - Map? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[6] as Cutoffs?)?.glicko; + //Map? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[6] as Cutoffs?)?.tr; + //Map? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[6] as Cutoffs?)?.glicko; - if (me.tlSeason1.gamesPlayed > 9) { - thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; - thatRankGlickoCutoff = cutoffsGlicko?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; - nextRankCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.rating??25000 : cutoffs?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; - nextRankGlickoCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; - } + // if (me.tlSeason1.gamesPlayed > 9) { + // thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; + // thatRankGlickoCutoff = cutoffsGlicko?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; + // nextRankCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.rating??25000 : cutoffs?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; + // nextRankGlickoCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; + // } - if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; + // if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; // Making list of Tetra League matches - List tlMatches = []; bool isTracking = await teto.isPlayerTracking(me.userId); List states = []; TetraLeagueAlpha? compareWith; Set uniqueTL = {}; - tlMatches = tlStream.records; List storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches if (isTracking){ // if tracked - save data to local DB await teto.storeState(me); - await teto.saveTLMatchesFromStream(tlStream); + //await teto.saveTLMatchesFromStream(tlStream); } - + TetraLeagueAlphaStream? oldMatches; // building list of TL matches if(fetchTLmatches) { try{ - List oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor); - storedRecords.addAll(oldMatches); - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.length)))); + oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.records.length)))); }on TetrioHistoryNotExist{ if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches))); }on P1nkl0bst3rForbidden { @@ -237,16 +237,16 @@ class _MainState extends State with TickerProviderStateMixin { } } if (storedRecords.isNotEmpty) _TLHistoryWasFetched = true; - for (var match in storedRecords) { - // add stored match to list only if it missing from retrived ones - if (!tlMatches.contains(match)) tlMatches.add(match); - } - 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.timestamp.isAtSameMomentAs(b.timestamp)) return 0; - if(a.timestamp.isAfter(b.timestamp)) return -1; - return 0; - }); + + // add stored match to list only if it missing from retrived ones + if (oldMatches != null) tlStream.addFromAlphaStream(oldMatches); + + // tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list + // if(a.ts.isBefore(b.ts)) return 1; + // if(a.ts.isAtSameMomentAs(b.ts)) return 0; + // if(a.ts.isAfter(b.ts)) return -1; + // return 0; + // }); // Handling history if(fetchHistory){ @@ -266,8 +266,8 @@ class _MainState extends State with TickerProviderStateMixin { states.addAll(await teto.getPlayer(me.userId)); for (var element in states) { // For graphs I need only unique entries - if (uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1); - if (uniqueTL.isEmpty) 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!); } // Also i need previous Tetra League State for comparison if avaliable if (uniqueTL.length >= 2){ @@ -305,8 +305,8 @@ class _MainState extends State with TickerProviderStateMixin { changePlayer(me.userId); }); } - - return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; + return [me, summaries, news, tlStream]; + //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; } /// Triggers widgets rebuild @@ -455,31 +455,31 @@ class _MainState extends State with TickerProviderStateMixin { width: MediaQuery.of(context).size.width-450, constraints: const BoxConstraints(maxWidth: 1024), child: TLThingy( - tl: snapshot.data![0].tlSeason1, + tl: snapshot.data![1].league, userID: snapshot.data![0].userId, - states: snapshot.data![2], - topTR: snapshot.data![7]?.tr, - lastMatchPlayed: snapshot.data![11], + states: const [], //snapshot.data![2], + //topTR: snapshot.data![7]?.tr, + //lastMatchPlayed: snapshot.data![11], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon", - thatRankCutoff: thatRankCutoff, - thatRankCutoffGlicko: thatRankGlickoCutoff, - thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, - nextRankCutoff: nextRankCutoff, - nextRankCutoffGlicko: nextRankGlickoCutoff, - nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null, - averages: rankAverages, - lbPositions: meAmongEveryone + //thatRankCutoff: thatRankCutoff, + //thatRankCutoffGlicko: thatRankGlickoCutoff, + //thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, + //nextRankCutoff: nextRankCutoff, + //nextRankCutoffGlicko: nextRankGlickoCutoff, + //nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null, + //averages: rankAverages, + //lbPositions: meAmongEveryone ), ), SizedBox( 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), - _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]), - _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) + _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![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![2]) ] : [ TLThingy( tl: snapshot.data![0].tlSeason1, @@ -693,7 +693,7 @@ class _NavDrawerState extends State { class _TLRecords extends StatelessWidget { final String userID; final Function changePlayer; - final List data; + final List data; final bool wasActiveInTL; final bool oldMathcesHere; 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( decoration: BoxDecoration( gradient: LinearGradient( @@ -741,19 +741,19 @@ class _TLRecords extends StatelessWidget { ) ), 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)), - title: Text("vs. ${data[index].endContext.firstWhere((element) => element.userId != userID).username}"), - subtitle: Text(timestamp(data[index].timestamp), style: const TextStyle(color: Colors.grey)), + title: Text("vs. ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).username}"), + subtitle: Text(timestamp(data[index].ts), style: const TextStyle(color: Colors.grey)), trailing: TrailingStats( - data[index].endContext.firstWhere((element) => element.userId == userID).secondary, - data[index].endContext.firstWhere((element) => element.userId == userID).tertiary, - data[index].endContext.firstWhere((element) => element.userId == userID).extra, - data[index].endContext.firstWhere((element) => element.userId != userID).secondary, - data[index].endContext.firstWhere((element) => element.userId != userID).tertiary, - data[index].endContext.firstWhere((element) => element.userId != userID).extra + data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.apm, + data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.pps, + data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.vs, + data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.apm, + data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.pps, + 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) { late MapEntry closestAverageBlitz; 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 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) { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.endContext.finalTime).abs() < (b -sprint!.endContext.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = sprint!.endContext.finalTime < closestAverageSprint.value; + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.stats.finalTime).abs() < (b -sprint!.stats.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = sprint!.stats.finalTime < closestAverageSprint.value; } 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)); - blitzBetterThanClosestAverage = blitz!.endContext.score > closestAverageBlitz.value; + 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!.stats.score > closestAverageBlitz.value; } return SingleChildScrollView(child: Padding( padding: const EdgeInsets.only(top: 20.0), @@ -1047,19 +1047,19 @@ class _TwoRecordsThingy extends StatelessWidget { children: [ Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), 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), - //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( text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), 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 )) - 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 )), 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, spacing: 20, children: [ - StatCellNum(playerStat: sprint!.endContext.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!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: sprint!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, 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!.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) LineclearsThingy(sprint!.endContext.clears, sprint!.endContext.lines, sprint!.endContext.holds, sprint!.endContext.tSpins), - if (sprint != null) Text("${sprint!.endContext.inputs} KP • ${f2.format(sprint!.endContext.kps)} KPS"), + if (sprint != null) FinesseThingy(sprint?.stats.finesse, sprint?.stats.finessePercentage), + if (sprint != null) LineclearsThingy(sprint!.stats.clears, sprint!.stats.lines, sprint!.stats.holds, sprint!.stats.tSpins), + if (sprint != null) Text("${sprint!.stats.inputs} KP • ${f2.format(sprint!.stats.kps)} KPS"), if (sprint != null) Wrap( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.start, @@ -1101,10 +1101,10 @@ class _TwoRecordsThingy extends StatelessWidget { for (int i = 1; i < sprintStream.records.length; i++) ListTile( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), - title: Text(get40lTime(sprintStream.records[i].endContext.finalTime.inMicroseconds), + title: Text(get40lTime(sprintStream.records[i].stats.finalTime.inMicroseconds), style: const TextStyle(fontSize: 18)), 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: "", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), 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)) ] ), @@ -1139,10 +1139,10 @@ class _TwoRecordsThingy extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), 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 )) - 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 )), TextSpan(text: timestamp(blitz!.timestamp)), @@ -1162,14 +1162,14 @@ class _TwoRecordsThingy extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.start, spacing: 20, children: [ - StatCellNum(playerStat: blitz!.endContext.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!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true) + StatCellNum(playerStat: blitz!.stats.level, playerStatLabel: t.statCellNum.level, 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!.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) LineclearsThingy(blitz!.endContext.clears, blitz!.endContext.lines, blitz!.endContext.holds, blitz!.endContext.tSpins), - if (blitz != null) Text("${blitz!.endContext.piecesPlaced} P • ${blitz!.endContext.inputs} KP • ${f2.format(blitz!.endContext.kpp)} KPP • ${f2.format(blitz!.endContext.kps)} KPS"), + if (blitz != null) FinesseThingy(blitz?.stats.finesse, blitz?.stats.finessePercentage), + if (blitz != null) LineclearsThingy(blitz!.stats.clears, blitz!.stats.lines, blitz!.stats.holds, blitz!.stats.tSpins), + 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( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.start, @@ -1187,10 +1187,10 @@ class _TwoRecordsThingy extends StatelessWidget { for (int i = 1; i < blitzStream.records.length; i++) ListTile( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), - title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].endContext.score)} points", + title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].stats.score)} points", style: const TextStyle(fontSize: 18)), 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 gametypes = { "40l": t.sprint, "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 @@ -1306,7 +1308,15 @@ class _OtherThingy extends StatelessWidget { 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)), + 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) + ), ] ) ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart new file mode 100644 index 0000000..76ced7a --- /dev/null +++ b/lib/views/main_view_tiles.dart @@ -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 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 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( + 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: [ + 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 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 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 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: [ + 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: [ + 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)))))) + // ] + // ) + ], + ), + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/views/singleplayer_record_view.dart b/lib/views/singleplayer_record_view.dart index eb5b2fd..2126c2f 100644 --- a/lib/views/singleplayer_record_view.dart +++ b/lib/views/singleplayer_record_view.dart @@ -17,7 +17,7 @@ class SingleplayerRecordView extends StatelessWidget { backgroundColor: Colors.black, appBar: AppBar( title: Text("${ - switch (record.endContext.gameType){ + switch (record.gamemode){ "40l" => t.sprint, "blitz" => t.blitz, String() => "5000000 Blast", diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart index f37384b..5e367d2 100644 --- a/lib/views/state_view.dart +++ b/lib/views/state_view.dart @@ -58,6 +58,6 @@ class StateState extends State { headerSliverBuilder: (context, value) { 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,)))); } } diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart index 4263129..a7cab43 100644 --- a/lib/views/states_view.dart +++ b/lib/views/states_view.dart @@ -61,7 +61,7 @@ class StatesState extends State { itemBuilder: (context, index) { return ListTile( 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( icon: const Icon(Icons.delete_forever), onPressed: () { diff --git a/lib/widgets/recent_sp_games.dart b/lib/widgets/recent_sp_games.dart index 9cb033f..76e389c 100644 --- a/lib/widgets/recent_sp_games.dart +++ b/lib/widgets/recent_sp_games.dart @@ -25,7 +25,7 @@ class RecentSingleplayerGames extends StatelessWidget{ for(RecordSingle record in recent.records) ListTile( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: record))), leading: Text( - switch (record.endContext.gameType){ + switch (record.gamemode){ "40l" => "40L", "blitz" => "BLZ", "5mblast" => "5MB", @@ -34,15 +34,15 @@ class RecentSingleplayerGames extends StatelessWidget{ style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), title: Text( - switch (record.endContext.gameType){ - "40l" => get40lTime(record.endContext.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(record.endContext.score)), - "5mblast" => get40lTime(record.endContext.finalTime.inMicroseconds), + switch (record.gamemode){ + "40l" => get40lTime(record.stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(record.stats.score)), + "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), String() => "huh", }, style: const TextStyle(fontSize: 18)), subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(record.endContext) + trailing: SpTrailingStats(record.stats, record.gamemode) ) ], ); diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index 5d19c94..3e5e3e3 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -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))); late MapEntry closestAverageBlitz; 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 bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.finalTime < sprintAverages[rank]! : null; - if (record!.endContext.gameType == "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)); - sprintBetterThanClosestAverage = record!.endContext.finalTime < closestAverageSprint.value; - }else if (record!.endContext.gameType == "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)); - blitzBetterThanClosestAverage = record!.endContext.score > closestAverageBlitz.value; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.finalTime < sprintAverages[rank]! : null; + if (record!.gamemode == "40l") { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.stats.finalTime).abs() < (b -record!.stats.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = record!.stats.finalTime < closestAverageSprint.value; + }else if (record!.gamemode == "blitz"){ + 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!.stats.score > closestAverageBlitz.value; } return LayoutBuilder( @@ -61,20 +61,20 @@ class SingleplayerRecord extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, 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) ), - 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) ), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - if (record!.endContext.gameType == "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 == "40l" && !hideTitle) Text(t.sprint, 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( - 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), ), ), @@ -82,16 +82,16 @@ class SingleplayerRecord extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), 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 )) - 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 )) - 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 )) - 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 )), 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, spacing: 20, children: [ - StatCellNum(playerStat: record!.endContext.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!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, 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!.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, crossAxisAlignment: WrapCrossAlignment.start, spacing: 20, children: [ - StatCellNum(playerStat: record!.endContext.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!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) + StatCellNum(playerStat: record!.stats.level, playerStatLabel: t.statCellNum.level, 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!.stats.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) ], ), - FinesseThingy(record?.endContext.finesse, record?.endContext.finessePercentage), - LineclearsThingy(record!.endContext.clears, record!.endContext.lines, record!.endContext.holds, record!.endContext.tSpins), - if (record!.endContext.gameType == "40l") Text("${record!.endContext.inputs} KP • ${f2.format(record!.endContext.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"), + FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), + LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), + if (record!.gamemode == "40l") Text("${record!.stats.inputs} KP • ${f2.format(record!.stats.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( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.start, @@ -141,15 +141,15 @@ class SingleplayerRecord extends StatelessWidget { style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), title: Text( - switch (stream!.records[i].endContext.gameType){ - "40l" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(stream!.records[i].endContext.score)), - "5mblast" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds), + switch (stream!.records[i].gamemode){ + "40l" => get40lTime(stream!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(stream!.records[i].stats.score)), + "5mblast" => get40lTime(stream!.records[i].stats.finalTime.inMicroseconds), String() => "huh", }, style: const TextStyle(fontSize: 18)), 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) ) ] ), diff --git a/lib/widgets/sp_trailing_stats.dart b/lib/widgets/sp_trailing_stats.dart index 4a5ac72..9013211 100644 --- a/lib/widgets/sp_trailing_stats.dart +++ b/lib/widgets/sp_trailing_stats.dart @@ -3,9 +3,10 @@ import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; 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 Widget build(BuildContext context) { @@ -16,7 +17,7 @@ class SpTrailingStats extends StatelessWidget{ children: [ 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(switch(endContext.gameType){ + Text(switch(gamemode){ "40l" => "${f2.format(endContext.kps)} KPS, ${f2.format(endContext.kpp)} KPP", "blitz" => "${intf.format(endContext.spp)} SPP, lvl ${endContext.level}", "5mblast" => "${intf.format(endContext.spp)} SPP, ${endContext.lines} L", diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index 0ebaecb..e023ade 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -27,9 +27,9 @@ class TLRatingThingy extends StatelessWidget{ List formatedGlicko = f4.format(tlData.glicko).split(decimalSeparator); List formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); DateTime now = DateTime.now(); - bool beforeS1end = now.isBefore(seasonEnd); - 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()); + //bool beforeS1end = now.isBefore(seasonEnd); + //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()); return Wrap( direction: Axis.horizontal, 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: 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 (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)) ], ), ), diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index ba9ee0c..d554ca5 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:path/path.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; @@ -24,6 +26,7 @@ class TLThingy extends StatefulWidget { final List states; final bool showTitle; final bool bot; + final bool hidePreSeasonThingy; final bool guest; final double? topTR; final PlayerLeaderboardPosition? lbPositions; @@ -35,7 +38,7 @@ class TLThingy extends StatefulWidget { final double? nextRankCutoffGlicko; final double? nextRankTarget; 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 State createState() => _TLThingyState(); @@ -48,8 +51,9 @@ class _TLThingyState extends State with TickerProviderStateMixin { late RangeValues _currentRangeValues; late List sortedStates; late Timer _countdownTimer; - Duration seasonLeft = seasonEnd.difference(DateTime.now()); - + //Duration seasonLeft = seasonEnd.difference(DateTime.now()); + Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + @override void initState() { _currentRangeValues = const RangeValues(0, 1); @@ -61,7 +65,8 @@ class _TLThingyState extends State with TickerProviderStateMixin { Durations.extralong4, (Timer timer) { setState(() { - seasonLeft = seasonEnd.difference(DateTime.now()); + //seasonLeft = seasonEnd.difference(DateTime.now()); + postSeasonLeft = seasonStart.difference(DateTime.now()); }); }, ); @@ -80,6 +85,47 @@ class _TLThingyState extends State with TickerProviderStateMixin { String decimalSeparator = f2.symbols.DECIMAL_SEP; List estTRformated = currentTl.estTr != null ? f2.format(currentTl.estTr!.esttr).split(decimalSeparator) : []; List 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,)); return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth >= 768; @@ -90,8 +136,8 @@ class _TLThingyState extends State with TickerProviderStateMixin { return Column( children: [ if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft))) - else Text(t.seasonEnded), + //if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft))) + //else Text(t.seasonEnded), if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)), textAlign: TextAlign.center,), if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), @@ -105,7 +151,7 @@ class _TLThingyState extends State with TickerProviderStateMixin { if (values.start.round() == 0){ currentTl = widget.tl; }else{ - currentTl = sortedStates[values.start.round()-1].tlSeason1; + currentTl = sortedStates[values.start.round()-1].tlSeason1!; } if (values.end.round() == 0){ oldTl = widget.tl; diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 0cc9abb..a2200b2 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -87,6 +87,12 @@ "verdictBetter": "better", "verdictWorse": "worse", "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}", "seasonEnded": "Season has ended", "gamesUntilRanked": "${left} games until being ranked", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index e23b1cb..61e2bbb 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -87,6 +87,12 @@ "verdictBetter": "Лучше", "verdictWorse": "Хуже", "smooth": "Гладкий", + "postSeason": "Внесезонье", + "seasonStarts": "Сезон начнётся через:", + "myMessadgeHeader": "Сообщение от dan63", + "myMessadgeBody": "TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.", + "preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона", + "nanow": "Пока недоступно...", "seasonEnds": "Сезон закончится через ${countdown}", "seasonEnded": "Сезон закончился", "gamesUntilRanked": "${left} матчей до получения рейтинга",