From b62369314801716ce86dbe24e55a7e5864481f74 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 27 Jul 2024 22:10:45 +0300 Subject: [PATCH 01/33] 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} матчей до получения рейтинга", From c1eaa3223319aa4f89959938d95a04b686d0bb80 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 27 Jul 2024 22:41:42 +0300 Subject: [PATCH 02/33] I'm stupid --- lib/views/main_view.dart | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 8375696..205616c 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -482,27 +482,28 @@ class _MainState extends State with TickerProviderStateMixin { _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, + tl: snapshot.data![1].league, userID: snapshot.data![0].userId, - states: snapshot.data![2], - topTR: snapshot.data![7]?.tr, + 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 ), - _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), - SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![9]), - SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![10]), - _RecentSingleplayersThingy(snapshot.data![8]), - _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6]) + _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![1].league.gamesPlayed > 0), + SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")), + SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "Blitz")), + _RecentSingleplayersThingy(SingleplayerStream(userId: "userId", records: [], type: "recent")), + _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![2]) ], ), ), From 6adecbe64d302808d8876708bf05ba48d366aba8 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 28 Jul 2024 20:12:47 +0300 Subject: [PATCH 03/33] `TlMatchResultView` now available --- lib/data_objects/tetrio.dart | 26 +- lib/views/main_view.dart | 2 +- lib/views/main_view_tiles.dart | 10 +- lib/views/mathes_view.dart | 6 - lib/views/tl_match_view.dart | 631 +++++++++++++-------------------- 5 files changed, 261 insertions(+), 414 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 7a8b3f1..64da900 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -921,10 +921,7 @@ class TetraLeagueBetaStream{ 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, + rank: -1 ) ), BetaLeagueLeaderboardEntry( @@ -940,10 +937,7 @@ class TetraLeagueBetaStream{ 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, + rank: -1 ) ) ], @@ -964,10 +958,7 @@ class TetraLeagueBetaStream{ garbageReceived: -1, kills: 0, altitude: 0.0, - rank: -1, - nerdStats: entry.endContext[0].nerdStatsTracking[i], - playstyle: entry.endContext[0].playstyleTracking[i], - estTr: entry.endContext[0].estTrTracking[i], + rank: -1 ) ),BetaLeagueRound( id: entry.endContext[1].userId, @@ -984,10 +975,7 @@ class TetraLeagueBetaStream{ garbageReceived: -1, kills: 0, altitude: 0.0, - rank: -1, - nerdStats: entry.endContext[1].nerdStatsTracking[i], - playstyle: entry.endContext[1].playstyleTracking[i], - estTr: entry.endContext[1].estTrTracking[i], + rank: -1 ) )] ] @@ -1084,7 +1072,11 @@ class BetaLeagueStats{ 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({required this.apm, required this.pps, required this.vs, required this.garbageSent, required this.garbageReceived, required this.kills, required this.altitude, required this.rank}){ + 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); + } BetaLeagueStats.fromJson(Map json){ apm = json['apm'].toDouble(); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 205616c..fc00955 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -754,7 +754,7 @@ class _TLRecords extends StatelessWidget { data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.pps, data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.vs, ), - 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))), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), ), ); }); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 76ced7a..4ea415b 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -214,7 +214,15 @@ class _MainState extends State with TickerProviderStateMixin { width: 450.0, child: Column( children: [ - + Card( + child: Row( + children: [ + Spacer(), + Text("test card"), + Spacer() + ], + ), + ) ], ), ) diff --git a/lib/views/mathes_view.dart b/lib/views/mathes_view.dart index 8f03a82..270ff8d 100644 --- a/lib/views/mathes_view.dart +++ b/lib/views/mathes_view.dart @@ -73,12 +73,6 @@ class MatchesState extends State { })); }, ), - onTap: (){Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TlMatchResultView(record: value, initPlayerId: widget.userID), - ), - );}, )] : [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))], ); diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 7d001ae..975d5b8 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -2,13 +2,11 @@ import 'dart:io'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; -import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy; +import 'package:tetra_stats/views/compare_view.dart' show CompareThingy; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/vs_graphs.dart'; -import 'package:tetra_stats/main.dart' show teto; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -30,7 +28,7 @@ Duration framesToTime(int frames){ } class TlMatchResultView extends StatefulWidget { - final TetraLeagueAlphaRecord record; + final BetaRecord record; final String initPlayerId; const TlMatchResultView({super.key, required this.record, required this.initPlayerId}); @@ -40,15 +38,65 @@ class TlMatchResultView extends StatefulWidget { class TlMatchResultState extends State { late Future replayData; + late Duration time; + late String readableTime; + late String reason; + Duration totalTime = Duration(); + List roundLengths = []; + List timeWeightedStats = []; + late bool initPlayerWon; @override void initState(){ rounds = [DropdownMenuItem(value: -1, child: Text(t.match))]; - rounds.addAll([for (int i = 0; i < widget.record.endContext.first.secondaryTracking.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]); - replayData = teto.analyzeReplay(widget.record.replayId, widget.record.replayAvalable); + rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]); + if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch))); + greenSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id == widget.initPlayerId); + redSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id != widget.initPlayerId); + List APMmultipliedByWeights = [0, 0]; + List PPSmultipliedByWeights = [0, 0]; + List VSmultipliedByWeights = [0, 0]; + for (var round in widget.record.results.rounds){ + var longerLifetime = round[0].lifetime.compareTo(round[1].lifetime) == 1 ? round[0].lifetime : round[1].lifetime; + roundLengths.add(longerLifetime); + totalTime += longerLifetime; + + BetaLeagueRound greenSide = round.firstWhere((element) => element.id == widget.initPlayerId); + BetaLeagueRound redSide = round.firstWhere((element) => element.id != widget.initPlayerId); + + APMmultipliedByWeights[0] += greenSide.stats.apm*longerLifetime.inMilliseconds; + APMmultipliedByWeights[1] += redSide.stats.apm*longerLifetime.inMilliseconds; + PPSmultipliedByWeights[0] += greenSide.stats.pps*longerLifetime.inMilliseconds; + PPSmultipliedByWeights[1] += redSide.stats.pps*longerLifetime.inMilliseconds; + VSmultipliedByWeights[0] += greenSide.stats.vs*longerLifetime.inMilliseconds; + VSmultipliedByWeights[1] += redSide.stats.vs*longerLifetime.inMilliseconds; + } + timeWeightedStats = [ + BetaLeagueStats( + apm: APMmultipliedByWeights[0]/totalTime.inMilliseconds, + pps: PPSmultipliedByWeights[0]/totalTime.inMilliseconds, + vs: VSmultipliedByWeights[0]/totalTime.inMilliseconds, + garbageSent: widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent, + garbageReceived: widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived, + kills: widget.record.results.leaderboard[greenSidePlayer].stats.kills, + altitude: widget.record.results.leaderboard[greenSidePlayer].stats.altitude, + rank: widget.record.results.leaderboard[greenSidePlayer].stats.rank + ), + BetaLeagueStats( + apm: APMmultipliedByWeights[1]/totalTime.inMilliseconds, + pps: PPSmultipliedByWeights[1]/totalTime.inMilliseconds, + vs: VSmultipliedByWeights[1]/totalTime.inMilliseconds, + garbageSent: widget.record.results.leaderboard[redSidePlayer].stats.garbageSent, + garbageReceived: widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived, + kills: widget.record.results.leaderboard[redSidePlayer].stats.kills, + altitude: widget.record.results.leaderboard[redSidePlayer].stats.altitude, + rank: widget.record.results.leaderboard[redSidePlayer].stats.rank + ), + ]; + initPlayerWon = widget.record.results.leaderboard[greenSidePlayer].wins > widget.record.results.leaderboard[redSidePlayer].wins; if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${timestamp(widget.record.timestamp)}"); + windowManager.setTitle("Tetra Stats: ${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}"); } super.initState(); } @@ -62,45 +110,16 @@ class TlMatchResultState extends State { Widget buildComparison(double width, bool showMobileSelector){ bool bigScreen = width >= 768; + if (roundSelector.isNegative){ + time = totalTime; + readableTime = "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}"; + }else{ + time = roundLengths[roundSelector]; + readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${widget.record.results.rounds[roundSelector].firstWhere((element) => element.alive)}"; + } return SizedBox( width: width, - child: FutureBuilder(future: replayData, builder: (context, snapshot){ - late Duration time; - late String readableTime; - late String reason; - timeWeightedStatsAvaliable = true; - if (snapshot.connectionState != ConnectionState.done) return const LinearProgressIndicator(); - if (!snapshot.hasError){ - if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch))); - greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId); - redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId); - if (roundSelector.isNegative){ - time = framesToTime(snapshot.data!.totalLength); - readableTime = "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}"; - }else{ - time = framesToTime(snapshot.data!.roundLengths[roundSelector]); - readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}"; - } - }else{ - switch (snapshot.error.runtimeType){ - case ReplayNotAvalable: - reason = t.matchIsTooOld; - break; - case SzyNotFound: - reason = t.matchIsTooOld; - break; - case SzyForbidden: - reason = t.errors.replayRejected; - break; - case SzyTooManyRequests: - reason = t.errors.tooManyRequests; - break; - default: - reason = snapshot.error.toString(); - break; - } - } - return NestedScrollView( + child: NestedScrollView( headerSliverBuilder: (context, value) { return [ SliverToBoxAdapter( @@ -117,15 +136,15 @@ class TlMatchResultState extends State { colors: const [Colors.green, Colors.transparent], begin: Alignment.bottomCenter, end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], + stops: [0.0, initPlayerWon ? 0.4 : 0.0], )), child: Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column(children: [ - Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: bigScreen ? const TextStyle( + Text(widget.record.results.leaderboard[greenSidePlayer].username, style: bigScreen ? const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 28) : const TextStyle()), - Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle( + Text(widget.record.results.leaderboard[greenSidePlayer].wins.toString(), style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 42)) ]), @@ -143,15 +162,15 @@ class TlMatchResultState extends State { colors: const [Colors.red, Colors.transparent], begin: Alignment.bottomCenter, end: Alignment.topCenter, - stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], + stops: [0.0, !initPlayerWon ? 0.4 : 0.0], )), child: Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column(children: [ - Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: bigScreen ? const TextStyle( + Text(widget.record.results.leaderboard[redSidePlayer].username, style: bigScreen ? const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 28) : const TextStyle()), - Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( + Text(widget.record.results.leaderboard[redSidePlayer].wins.toString(), style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 42)) ]), @@ -179,10 +198,10 @@ class TlMatchResultState extends State { ), ), ), - if (widget.record.ownId == widget.record.replayId && showMobileSelector) SliverToBoxAdapter( + if (widget.record.id == widget.record.replayID && showMobileSelector) SliverToBoxAdapter( child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), ), - if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(snapshot.hasError ? reason : readableTime, textAlign: TextAlign.center))), + if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(readableTime, textAlign: TextAlign.center))), const SliverToBoxAdapter( child: Divider(), ) @@ -194,106 +213,37 @@ class TlMatchResultState extends State { children: [ CompareThingy( label: "APM", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : - roundSelector == -1 ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], + greenSide: roundSelector == -2 ? timeWeightedStats[0].apm : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, + redSide: roundSelector == -2 ? timeWeightedStats[1].apm : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( label: "PPS", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps: - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], + greenSide: roundSelector == -2 ? timeWeightedStats[0].pps : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, + redSide: roundSelector == -2 ? timeWeightedStats[1].pps : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( label: "VS", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], + greenSide: roundSelector == -2 ? timeWeightedStats[0].vs : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, + redSide: roundSelector == -2 ? timeWeightedStats[1].vs : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs, fractionDigits: 2, higherIsBetter: true, ), - if (snapshot.hasData) Column(children: [ - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, - label: "Inputs", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, - label: "Pieces Placed", higherIsBetter: true), - CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kpp : - roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kpp : - roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp, - label: "KPP", higherIsBetter: false, fractionDigits: 2,), - CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kps : - roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kps : - roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps, - label: "KPS", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared, - label: "Lines Cleared", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score, - label: "Score", higherIsBetter: true), - CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].spp : - roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].spp : - roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp, - label: "SPP", higherIsBetter: true, fractionDigits: 2,), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100, - label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike, - label: "Best Spike", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo, - label: "Best Combo", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB, - label: "Best BtB", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent, - label: "Sent", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived, - label: "Received", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack, - label: "Attack", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared, - label: "Cleared", higherIsBetter: true), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears, - label: "PC", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins, - label: "T-spins", higherIsBetter: true), - CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads, - redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads, - label: "Quads", higherIsBetter: true), - ],), - ], - ), - const Divider(), + if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageSent, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageSent, + label: "Sent", higherIsBetter: true), + if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageReceived, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageReceived, + label: "Received", higherIsBetter: true), const Divider(), Column( children: [ Padding( @@ -305,180 +255,179 @@ class TlMatchResultState extends State { ), CompareThingy( label: "APP", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.app : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.app : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.app : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.app, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.app : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.app, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "VS/APM", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.vsapm : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.vsapm : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.vsapm : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.vsapm, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.vsapm : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.vsapm, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "DS/S", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dss : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dss : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dss : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dss, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dss : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dss, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "DS/P", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dsp : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dsp, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dsp : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dsp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "APP + DS/P", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.appdsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.appdsp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.appdsp : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.appdsp, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.appdsp : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.appdsp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.cheese : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.cheese : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.cheese : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.cheese, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.cheese : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.cheese, fractionDigits: 2, higherIsBetter: false, ), CompareThingy( label: "Gb Eff.", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.gbe : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.gbe : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.gbe : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.gbe, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.gbe : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.gbe, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "wAPP", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.nyaapp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.nyaapp : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.nyaapp : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.nyaapp, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.nyaapp : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.nyaapp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Area", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.area : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.area : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area, + greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.area : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.area, + redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.area : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.area, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( label: t.statCellNum.estOfTRShort, - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].estTr.esttr : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].estTr.esttr : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr, + greenSide: roundSelector == -2 ? timeWeightedStats[0].estTr.esttr : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr, + redSide: roundSelector == -2 ? timeWeightedStats[1].estTr.esttr : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.estTr.esttr, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( label: "Opener", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.opener : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.opener : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener, + greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.opener : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.opener, + redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.opener : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.opener, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Plonk", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.plonk : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.plonk : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk, + greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.plonk : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk, + redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.plonk : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Stride", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.stride : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.stride : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride, + greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.stride : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.stride, + redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.stride : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.stride, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Inf. DS", - greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.infds : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds, - redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.infds : - roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds, + greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.infds : + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.infds, + redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.infds : + roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.infds, fractionDigits: 3, higherIsBetter: true, ), VsGraphs( - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector], - (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector] + roundSelector == -2 ? timeWeightedStats[0].apm : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, + roundSelector == -2 ? timeWeightedStats[0].pps : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, + roundSelector == -2 ? timeWeightedStats[0].vs : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, + roundSelector == -2 ? timeWeightedStats[0].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats, + roundSelector == -2 ? timeWeightedStats[0].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle, + roundSelector == -2 ? timeWeightedStats[1].apm : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, + roundSelector == -2 ? timeWeightedStats[1].pps : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps, + roundSelector == -2 ? timeWeightedStats[1].vs : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs, + roundSelector == -2 ? timeWeightedStats[1].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats, + roundSelector == -2 ? timeWeightedStats[1].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle, ) ], ), - if (widget.record.ownId != widget.record.replayId) const Divider(), - if (widget.record.ownId != widget.record.replayId) Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("Handling", - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, - label: "DAS", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, - label: "ARR", fractionDigits: 1, postfix: "F", - higherIsBetter: false), - CompareThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, - label: "SDF", prefix: "x", - higherIsBetter: true), - CompareBoolThingy( - greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, - redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, - label: "Safe HD", - trueIsBetter: true) - ], - ) + // if (widget.record.ownId != widget.record.replayId) const Divider(), + // if (widget.record.ownId != widget.record.replayId) Column( + // children: [ + // Padding( + // padding: const EdgeInsets.only(bottom: 16), + // child: Text("Handling", + // style: TextStyle( + // fontFamily: "Eurostile Round Extended", + // fontSize: bigScreen ? 42 : 28)), + // ), + // CompareThingy( + // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, + // redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, + // label: "DAS", fractionDigits: 1, postfix: "F", + // higherIsBetter: false), + // CompareThingy( + // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, + // redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, + // label: "ARR", fractionDigits: 1, postfix: "F", + // higherIsBetter: false), + // CompareThingy( + // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, + // redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, + // label: "SDF", prefix: "x", + // higherIsBetter: true), + // CompareBoolThingy( + // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, + // redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, + // label: "Safe HD", + // trueIsBetter: true) + // ], + // ) ], ) - ); - }), + ])), ); } @@ -494,76 +443,30 @@ class TlMatchResultState extends State { Wrap( alignment: WrapAlignment.spaceBetween, children: [ - FutureBuilder(future: replayData, builder: (context, snapshot) { - switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const CircularProgressIndicator(); - case ConnectionState.done: - if (!snapshot.hasError){ - var time = framesToTime(snapshot.data!.totalLength); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.matchLength), - RichText( - text: TextSpan( - text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white), - children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ) - ],); - }else{ - String reason; - switch (snapshot.error.runtimeType){ - case ReplayNotAvalable: - reason = t.matchIsTooOld; - break; - case SzyNotFound: - reason = t.matchIsTooOld; - break; - case SzyForbidden: - reason = t.errors.replayRejected; - break; - case SzyTooManyRequests: - reason = t.errors.tooManyRequests; - break; - default: - reason = snapshot.error.toString(); - break; - } - timeWeightedStatsAvaliable = false; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.record.ownId != widget.record.replayId) Text("${t.replayIssue}: $reason"), - if (widget.record.ownId == widget.record.replayId) Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), - if (widget.record.ownId != widget.record.replayId) RichText( - text: const TextSpan( - text: "-:--", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.grey), - children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ) - ],); - } - - } - },), - if (widget.record.ownId != widget.record.replayId) Column( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.matchLength), + RichText( + text: TextSpan( + text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}", + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white), + children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(totalTime.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ) + ],), + if (widget.record.id != widget.record.replayID) Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(t.numberOfRounds), RichText( text: TextSpan( - text: widget.record.endContext.first.secondaryTracking.isNotEmpty ? widget.record.endContext.first.secondaryTracking.length.toString() : "---", - style: TextStyle( + text: widget.record.results.rounds.length.toString(), + style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, - color: widget.record.endContext.first.secondaryTracking.isEmpty ? Colors.grey : Colors.white + color: Colors.white ), ), ) @@ -582,99 +485,49 @@ class TlMatchResultState extends State { roundSelector = -2; setState(() {}); } : null, child: Text(t.timeWeightedmatchStats)) , - //TextButton( child: const Text('Button 3'), onPressed: () {}), ], ) ]), - // Column( - // children: [ - // ListTile( - // leading: Text("Round time"), - // title: Text("Winner", textAlign: TextAlign.center,), - // trailing: Text("Round stats"), - // ) - // ], - // ) ], ) ) ]; }, - body: ListView.builder(itemCount: widget.record.endContext.first.secondaryTracking.length, + body: ListView.builder(itemCount: widget.record.results.rounds.length, itemBuilder: (BuildContext context, int index) { - return FutureBuilder(future: replayData, builder: (context, snapshot) { - switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const LinearProgressIndicator(); - case ConnectionState.done: - if (!snapshot.hasError){ - var time = framesToTime(snapshot.data!.roundLengths[index]); - var accentColor = snapshot.data!.roundWinners[index][0] == widget.initPlayerId ? Colors.green : Colors.red; - var bgColor = roundSelector == index ? Colors.grey.shade900 : Colors.transparent; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - stops: const [0, 0.05], - colors: [accentColor, bgColor] - ) - ), - child: ListTile( - leading:RichText( - text: TextSpan( - text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.white), - children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ), - title: Text(snapshot.data!.roundWinners[index][1], textAlign: TextAlign.center), - trailing: TrailingStats( - widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[index], - widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[index] - ), - onTap:(){ - roundSelector = index; - setState(() {}); - }, - ), - ); - }else{ - return Container( - decoration: BoxDecoration( - color: roundSelector == index ? Colors.grey.shade900 : Colors.transparent - ), - child: ListTile( - leading: RichText( - text: const TextSpan( - text: "-:--", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.grey), - children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ), - title: const Text("---", style: TextStyle(color: Colors.grey), textAlign: TextAlign.center), - trailing: TrailingStats( - widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[index], - widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[index], - widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[index] - ), - onTap:(){ - roundSelector = index; - setState(() {}); - }, - ), - ); - } - } - } - ); + var accentColor = widget.record.results.rounds[index][0].id == widget.initPlayerId ? Colors.green : Colors.red; + var bgColor = roundSelector == index ? Colors.grey.shade900 : Colors.transparent; + var time = roundLengths[index]; + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + stops: const [0, 0.05], + colors: [accentColor, bgColor] + ) + ), + child: ListTile( + leading:RichText( + text: TextSpan( + text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.white), + children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ), + title: Text(widget.record.results.rounds[index][0].username, textAlign: TextAlign.center), + trailing: TrailingStats( + widget.record.results.rounds[index].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, + widget.record.results.rounds[index].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, + widget.record.results.rounds[index].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, + widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, + widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.pps, + widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.vs + ), + onTap:(){ + roundSelector = index; + setState(() {}); + }, + ), + ); }) ), ), @@ -707,10 +560,10 @@ class TlMatchResultState extends State { final t = Translations.of(context); return Scaffold( appBar: AppBar( - title: Text("${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${timestamp(widget.record.timestamp)}"), + title: Text("${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}"), actions: [ PopupMenuButton( - enabled: widget.record.replayAvalable, + enabled: widget.record.gamemode == "league", itemBuilder: (BuildContext context) => [ PopupMenuItem( value: 1, @@ -724,10 +577,10 @@ class TlMatchResultState extends State { onSelected: (value) async { switch (value) { case 1: - await launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${widget.record.replayId}")); + await launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${widget.record.replayID}")); break; case 2: - await launchInBrowser(Uri.parse("https://tetr.io/#r:${widget.record.replayId}")); + await launchInBrowser(Uri.parse("https://tetr.io/#r:${widget.record.replayID}")); break; default: } From 90ad788c6c21eb3c59093df8f4ef3af323538a4d Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 28 Jul 2024 20:38:14 +0300 Subject: [PATCH 04/33] You can see old TL matches again --- lib/data_objects/tetrio.dart | 4 ++-- lib/views/main_view.dart | 8 ++++---- lib/views/tl_match_view.dart | 29 +++++++++++++++++++---------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 64da900..78022fe 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -896,8 +896,8 @@ class TetraLeagueBetaStream{ for (var entry in json) records.add(BetaRecord.fromJson(entry)); } - addFromAlphaStream(TetraLeagueAlphaStream oldStream){ - for (var entry in oldStream.records) { + addFromAlphaStream(List r){ + for (var entry in r) { records.add( BetaRecord( id: entry.ownId, diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index fc00955..861b37e 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -236,10 +236,10 @@ class _MainState extends State with TickerProviderStateMixin { _TLHistoryWasFetched = true; } } - if (storedRecords.isNotEmpty) _TLHistoryWasFetched = true; - - // add stored match to list only if it missing from retrived ones - if (oldMatches != null) tlStream.addFromAlphaStream(oldMatches); + if (storedRecords.isNotEmpty) { + _TLHistoryWasFetched = true; + tlStream.addFromAlphaStream(storedRecords); + } // tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list // if(a.ts.isBefore(b.ts)) return 1; diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 975d5b8..fdd2f76 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -112,10 +112,11 @@ class TlMatchResultState extends State { bool bigScreen = width >= 768; if (roundSelector.isNegative){ time = totalTime; - readableTime = "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}"; + readableTime = !time.isNegative ? "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "${t.matchLength}: ---"; }else{ time = roundLengths[roundSelector]; - readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${widget.record.results.rounds[roundSelector].firstWhere((element) => element.alive)}"; + int alive = widget.record.results.rounds[roundSelector].indexWhere((element) => element.alive); + readableTime = "${t.roundLength}: ${!time.isNegative ? "${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "---"}\n${t.winner}: ${alive == -1 ? "idk" : widget.record.results.rounds[roundSelector][alive].username}"; } return SizedBox( width: width, @@ -448,12 +449,16 @@ class TlMatchResultState extends State { children: [ Text(t.matchLength), RichText( - text: TextSpan( - text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white), - children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(totalTime.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ) + text: !totalTime.isNegative ? TextSpan( + text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}", + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white), + children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(totalTime.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ) : const TextSpan( + text: "-:--", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.grey), + children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), + ) ],), if (widget.record.id != widget.record.replayID) Column( crossAxisAlignment: CrossAxisAlignment.end, @@ -507,11 +512,15 @@ class TlMatchResultState extends State { ), child: ListTile( leading:RichText( - text: TextSpan( + text: !time.isNegative ? TextSpan( text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.white), children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), + ) : const TextSpan( + text: "-:--", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.grey), + children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] + ), ), title: Text(widget.record.results.rounds[index][0].username, textAlign: TextAlign.center), trailing: TrailingStats( From c1561fba803f1a6b0b10ba3f2f4e21aff4d877db Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 29 Jul 2024 23:58:17 +0300 Subject: [PATCH 05/33] Zenith added I probably should push Nerd Stats into new widget, but i planning to redo UI a little bit, so idk --- lib/data_objects/tetrio.dart | 99 +++++++-- lib/services/tetrio_crud.dart | 4 +- lib/utils/relative_timestamps.dart | 5 + lib/views/main_view.dart | 239 ++++++++++++++++++++++ lib/views/main_view_tiles.dart | 314 +++++++++++++++++------------ lib/widgets/stat_sell_num.dart | 2 +- 6 files changed, 519 insertions(+), 144 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 78022fe..51d2d1b 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -457,8 +457,10 @@ class TetrioPlayer { class Summaries{ late String id; - late RecordSingle sprint; - late RecordSingle blitz; + RecordSingle? sprint; + RecordSingle? blitz; + RecordSingle? zenith; + RecordSingle? zenithEx; late TetraLeagueAlpha league; late TetrioZen zen; @@ -466,8 +468,10 @@ class Summaries{ 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']); + if (json['40l']['record'] != null) sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank'], json['40l']['rank_local']); + if (json['blitz']['record'] != null) blitz = RecordSingle.fromJson(json['blitz']['record'], json['blitz']['rank'], json['40l']['rank_local']); + if (json['zenith']['record'] != null) zenith = RecordSingle.fromJson(json['zenith']['record'], json['zenith']['rank'], json['zenith']['rank_local']); + if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); league = TetraLeagueAlpha.fromJson(json['league'], DateTime.now()); zen = TetrioZen.fromJson(json['zen']); } @@ -676,11 +680,13 @@ class ResultsStats { late int piecesPlaced; late int lines; late int score; - late int seed; + int? seed; late Duration finalTime; late int tSpins; late Clears clears; - late Finesse? finesse; + late int kills; + Finesse? finesse; + ZenithResults? zenith; double get pps => piecesPlaced / (finalTime.inMicroseconds / 1000000); double get kpp => inputs / piecesPlaced; @@ -717,7 +723,9 @@ class ResultsStats { tSpins = json['tspins']; piecesPlaced = json['piecesplaced']; clears = Clears.fromJson(json['clears']); - finesse = json.containsKey("finesse") ? Finesse.fromJson(json['finesse']) : null; + kills = json['kills']; + if (json.containsKey("finesse")) finesse = Finesse.fromJson(json['finesse']); + if (json.containsKey("zenith")) zenith = ZenithResults.fromJson(json['zenith']); } Map toJson() { @@ -739,6 +747,39 @@ class ResultsStats { } } +class ZenithResults{ + late double altitude; + late double rank; + late double peakrank; + late double avgrankpts; + late int floor; + late double targetingfactor; + late double targetinggrace; + late double totalbonus; + late int revives; + late int revivesTotal; + late bool speedrun; + late bool speedrunSeen; + late List splits; + + ZenithResults.fromJson(Map json){ + altitude = json['altitude'].toDouble(); + rank = json['rank'].toDouble(); + peakrank = json['peakrank'].toDouble(); + avgrankpts = json['avgrankpts'].toDouble(); + floor = json['floor']; + targetingfactor = json['targetingfactor'].toDouble(); + targetinggrace = json['targetinggrace'].toDouble(); + totalbonus = json['totalbonus'].toDouble(); + revives = json['revives']; + revivesTotal = json['revivesTotal']; + speedrun = json['speedrun']; + speedrunSeen = json['speedrun_seen']; + splits = []; + for (int ms in json['splits']) splits.add(Duration(milliseconds: ms)); + } +} + class Handling { late num arr; late num das; @@ -997,7 +1038,7 @@ class SingleplayerStream{ userId = userID; type = tp; records = []; - for (var value in json) {records.add(RecordSingle.fromJson(value, null));} + for (var value in json) {records.add(RecordSingle.fromJson(value, -1, -1));} } } @@ -1079,9 +1120,9 @@ class BetaLeagueStats{ } BetaLeagueStats.fromJson(Map json){ - apm = json['apm'].toDouble(); - pps = json['pps'].toDouble(); - vs = json['vsscore'].toDouble(); + apm = json['apm'] != null ? json['apm'].toDouble() : 0.00; + pps = json['apm'] != null ? json['pps'].toDouble() : 0.00; + vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00; garbageSent = json['garbagesent']; garbageReceived = json['garbagereceived']; kills = json['kills']; @@ -1364,11 +1405,13 @@ class RecordSingle { late String gamemode; late DateTime timestamp; late ResultsStats stats; - int? rank; + late int rank; + late int countryRank; + late AggregateStats aggregateStats; - RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, this.rank}); + RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); - RecordSingle.fromJson(Map json, int? ran) { + RecordSingle.fromJson(Map json, int ran, int cran) { //developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio"); ownId = json['_id']; gamemode = json['gamemode']; @@ -1377,6 +1420,8 @@ class RecordSingle { timestamp = DateTime.parse(json['ts']); userId = json['user']['id']; rank = ran; + countryRank = cran; + aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']); } Map toJson() { @@ -1391,12 +1436,38 @@ class RecordSingle { } } +class AggregateStats{ + late double apm; + late double pps; + late double vs; + late NerdStats nerdStats; + late EstTr estTr; + late Playstyle playstyle; + + AggregateStats(this.apm, this.pps, this.vs){ + 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); + } + + AggregateStats.fromJson(Map json){ + apm = json['apm'] != null ? json['apm'].toDouble() : 0.00; + pps = json['apm'] != null ? json['pps'].toDouble() : 0.00; + vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00; + 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 TetrioZen { late int level; late int score; TetrioZen({required this.level, required this.score}); + double get scoreRequirement => (10000 + 10000 * ((log(level + 1) / log(2)) - 1)); + TetrioZen.fromJson(Map json) { level = json['level']; score = json['score']; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 04f5a31..382201b 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -904,10 +904,10 @@ class TetrioService extends DB { if (jsonDecode(response.body)['success']) { Map jsonRecords = jsonDecode(response.body); var sprint = jsonRecords['data']['records']['40l']['record'] != null - ? RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank']) + ? RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'], jsonRecords['data']['records']['40l']['rank_local']) : null; var blitz = jsonRecords['data']['records']['blitz']['record'] != null - ? RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank']) + ? RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'], jsonRecords['data']['records']['blitz']['rank_local']) : null; var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); UserRecords result = UserRecords(userID, sprint, blitz, zen); diff --git a/lib/utils/relative_timestamps.dart b/lib/utils/relative_timestamps.dart index f73a267..7425441 100644 --- a/lib/utils/relative_timestamps.dart +++ b/lib/utils/relative_timestamps.dart @@ -3,6 +3,7 @@ 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 fixedSecs = NumberFormat("00.000", 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); @@ -70,6 +71,10 @@ String get40lTime(int microseconds){ return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); } +String getMoreNormalTime(Duration time){ + return "${nonsecs.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}"; +} + /// Readable [a] - [b], without sign String readableTimeDifference(Duration a, Duration b){ Duration result = a - b; diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 861b37e..8f3fb1b 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -11,6 +11,7 @@ import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; @@ -23,6 +24,8 @@ import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/singleplayer_record_view.dart'; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; import 'package:tetra_stats/widgets/finesse_thingy.dart'; +import 'package:tetra_stats/widgets/gauget_num.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/recent_sp_games.dart'; @@ -430,12 +433,14 @@ class _MainState extends State with TickerProviderStateMixin { tabs: bigScreen ? [ Tab(text: t.tetraLeague,), Tab(text: t.history), + Tab(text: "Quick Play"), Tab(text: "${t.sprint} & ${t.blitz}"), Tab(text: t.other), ] : [ Tab(text: t.tetraLeague), Tab(text: t.tlRecords), Tab(text: t.history), + Tab(text: "Quick Play"), Tab(text: t.sprint), Tab(text: t.blitz), Tab(text: t.recentRuns), @@ -478,6 +483,7 @@ class _MainState extends State with TickerProviderStateMixin { ), ],), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0), + _ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx), _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]) ] : [ @@ -500,6 +506,7 @@ class _MainState extends State with TickerProviderStateMixin { ), _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![1].league.gamesPlayed > 0), + _ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx), SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")), SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "Blitz")), _RecentSingleplayersThingy(SingleplayerStream(userId: "userId", records: [], type: "recent")), @@ -1218,7 +1225,230 @@ class _RecentSingleplayersThingy extends StatelessWidget { child: RecentSingleplayerGames(recent: recent, hideTitle: true) ); } +} +class _ZenithThingy extends StatefulWidget{ + final RecordSingle? record; + final RecordSingle? recordEX; + + _ZenithThingy({this.record, this.recordEX}); + + @override + State<_ZenithThingy> createState() => _ZenithThingyState(); +} + +class _ZenithThingyState extends State<_ZenithThingy> { + late RecordSingle? record; + bool ex = false; + + @override + void initState(){ + super.initState(); + record = ex ? widget.recordEX : widget.record; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints){ + bool bigScreen = constraints.maxWidth > 768; + if (record == null) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Text("Quick Play${ex ? " Expert" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: "--- m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.grey), + ), + ), + TextButton(onPressed: (){ + if (ex){ + ex = false; + }else{ + ex = true; + } + setState(() { + record = ex ? widget.recordEX : widget.record; + }); + }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), + ], + ), + ); + } + return SingleChildScrollView( + child: Padding(padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Text("Quick Play${ex ? " Expert" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: "${f2.format(record!.stats.zenith!.altitude)} m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (record!.rank != -1) TextSpan(text: "№${record!.rank}"), + if (record!.rank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"), + if (record!.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(widget.record!.timestamp)), + ] + ), + ), + TextButton(onPressed: (){ + if (ex){ + ex = false; + }else{ + ex = true; + } + setState(() { + record = ex ? widget.recordEX : widget.record; + }); + }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), + Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 20, + children: [ + StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), + StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), + StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true) + ], + ), + FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), + LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + Table( + children: [ + TableRow( + children: [ + Text("Floor"), + Text("Split"), + Text("Total"), + ] + ), + for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( + children: [ + Text((i+1).toString()), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---"), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---"), + ] + ) + ], + ), + ], + ), + ), + ), + Column( + children: [ + Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Padding( + padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 35, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ + GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), + GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), + GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), + GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), + GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.appDescription), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}") + ]), + GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ + GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), + GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), + GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.vsapmDescription), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}") + ]) + ]), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, + alertWidgets: [Text(t.statCellNum.dssDescription), + Text("${t.formula}: (VS / 100) - (APM / 60)"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),], + okText: t.popupActions.ok, + higherIsBetter: true,), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, + alertWidgets: [Text(t.statCellNum.dspDescription), + Text("${t.formula}: DS/S / PPS"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, + alertWidgets: [Text(t.statCellNum.appdspDescription), + Text("${t.formula}: APP + DS/P"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, + alertWidgets: [Text(t.statCellNum.cheeseDescription), + Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),], + okText: t.popupActions.ok, + higherIsBetter: false), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, + alertWidgets: [Text(t.statCellNum.gbeDescription), + Text("${t.formula}: APP * DS/P * 2"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, + alertWidgets: [Text(t.statCellNum.nyaappDescription), + Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, + alertWidgets: [Text(t.statCellNum.areaDescription), + Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),], + okText: t.popupActions.ok, + higherIsBetter: true) + ]), + ) + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), + ) + ], + ) + ), + ); + }); + } } class _OtherThingy extends StatelessWidget { @@ -1314,6 +1544,7 @@ class _OtherThingy extends StatelessWidget { "40l" => get40lTime((news.data["result"]*1000).floor()), "5mblast" => get40lTime((news.data["result"]*1000).floor()), "zenith" => "${f2.format(news.data["result"])} m.", + "zenithex" => "${f2.format(news.data["result"])} m.", _ => "unknown" }, style: const TextStyle(fontWeight: FontWeight.bold) @@ -1463,6 +1694,14 @@ class _OtherThingy extends StatelessWidget { Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Text("${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), Text("${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: const TextStyle(fontSize: 18)), + Container( + constraints: BoxConstraints(maxWidth: 300.0), + child: Row(children: [ + Text("Score requirement to level up:"), + Spacer(), + Text(intf.format(zen!.scoreRequirement)) + ],), + ) ], ), ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 4ea415b..65e90d9 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -68,8 +68,21 @@ News testNews = News("6098518e3d5155e6ec429cdc", [ 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)), ]); +late ScrollController controller; class _MainState extends State with TickerProviderStateMixin { + @override + void initState() { + controller = ScrollController(); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold(body: Row( @@ -94,138 +107,185 @@ class _MainState extends State with TickerProviderStateMixin { ], 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( + Expanded( + child: Scrollbar( + controller: controller, + thumbVisibility: true, + child: SingleChildScrollView( + controller: controller, + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 450.0, + child: Column( children: [ - Spacer(), - Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended")), - Spacer() + 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: [ + Card( + child: Row( + children: [ + Spacer(), + Text("test card"), + Spacer() + ], + ), + ) ], ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: MarkdownBody(data: testPlayer.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), - ) - ], - ), + ), + SizedBox( + width: 450.0, + child: Column( + children: [ + Card( + child: Row( + children: [ + Spacer(), + Text("test card"), + Spacer() + ], + ), + ) + ], + ), + ), + SizedBox( + width: 450.0, + child: Column( + children: [ + Card( + child: Row( + children: [ + Spacer(), + Text("test card"), + Spacer() + ], + ), + ) + ], + ), + ), + ], ), - //if (testNews != null && testNews!.news.isNotEmpty) - Expanded(child: NewsThingy(testNews)) - ], - ) - ), - SizedBox( - width: 450.0, - child: Column( - children: [ - Card( - child: Row( - children: [ - Spacer(), - Text("test card"), - Spacer() - ], - ), - ) - ], + ), ), - ) + ), ], )); } diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 6e415ed..44ced86 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -11,7 +11,7 @@ class StatCellNum extends StatelessWidget { required this.playerStat, required this.playerStatLabel, required this.isScreenBig, - this.smallDecimal = true, + this.smallDecimal = false, this.alertWidgets, this.fractionDigits, this.oldPlayerStat, From 31659d646dd7439c0f58fa263f597d0abbdb6101 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 1 Aug 2024 00:50:15 +0300 Subject: [PATCH 06/33] zenith recent runs + some fixes --- .github/workflows/main.yml | 84 +++---- lib/data_objects/tetrio.dart | 25 +- lib/gen/strings.g.dart | 56 ++++- lib/services/tetrio_crud.dart | 26 +- lib/views/main_view.dart | 351 ++++++++------------------- lib/views/zenith_record_view.dart | 46 ++++ lib/widgets/singleplayer_record.dart | 14 +- lib/widgets/stat_sell_num.dart | 2 +- lib/widgets/zenith_thingy.dart | 259 ++++++++++++++++++++ res/i18n/strings.i18n.json | 13 +- res/i18n/strings_ru.i18n.json | 13 +- 11 files changed, 568 insertions(+), 321 deletions(-) create mode 100644 lib/views/zenith_record_view.dart create mode 100644 lib/widgets/zenith_thingy.dart diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f22269d..b468862 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,51 +40,26 @@ jobs: tag: Auto-${{ github.run_number }} body: Builded with GitHub Action workflow token: ${{ secrets.TOKEN }} - # build-and-release-linux: - # name: Build Linux App - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 - # - uses: subosito/flutter-action@v1 - # - uses: ashutoshvarma/setup-ninja@master - # with: - # channel: 'stable' - # flutter-version: '3.16.5' - # - name: Install project dependencies - # run: flutter pub get - # - name: Build artifacts - # run: flutter build linux --release - # - name: Archive Release - # uses: thedoctor0/zip-release@master - # with: - # type: 'zip' - # filename: TetraStats-${{github.ref_name}}-windows.zip - # directory: build/linux/x64/runner/Release/bundle - # - name: Push to Releases - # uses: ncipollo/release-action@v1 - # with: - # prerelease: true - # allowUpdates: true - # replacesArtifacts: false - # discussionCategory: autobuilded-releases - # artifacts: "build/linux/x64/runner/Release/bundle/TetraStats-${{github.ref_name}}-linux.zip" - # tag: Auto-${{ github.run_number }} - # body: Builded with GitHub Action workflow - # token: ${{ secrets.TOKEN }} - build-and-release-android: - name: Build Android App + build-and-release-linux: + name: Build Linux App runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' + - uses: actions/checkout@v2 - uses: subosito/flutter-action@v1 + - uses: ashutoshvarma/setup-ninja@master with: + channel: 'stable' flutter-version: '3.16.5' - - run: flutter pub get - # - run: flutter test // lmao. Tests? Who needs it? - - run: flutter build apk --split-per-abi + - name: Install project dependencies + run: flutter pub get + - name: Build artifacts + run: flutter build linux --release + - name: Archive Release + uses: thedoctor0/zip-release@master + with: + type: 'zip' + filename: TetraStats-${{github.ref_name}}-linux.zip + directory: build/linux/x64/runner/Release/bundle - name: Push to Releases uses: ncipollo/release-action@v1 with: @@ -92,7 +67,32 @@ jobs: allowUpdates: true replacesArtifacts: false discussionCategory: autobuilded-releases - artifacts: "build/app/outputs/flutter-apk/*" + artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip" tag: Auto-${{ github.run_number }} body: Builded with GitHub Action workflow - token: ${{ secrets.TOKEN }} \ No newline at end of file + token: ${{ secrets.TOKEN }} + # build-and-release-android: + # name: Build Android App + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v1 + # - uses: actions/setup-java@v1 + # with: + # java-version: '12.x' + # - uses: subosito/flutter-action@v1 + # with: + # flutter-version: '3.16.5' + # - run: flutter pub get + # # - run: flutter test // lmao. Tests? Who needs it? + # - run: flutter build apk --split-per-abi + # - name: Push to Releases + # uses: ncipollo/release-action@v1 + # with: + # prerelease: true + # allowUpdates: true + # replacesArtifacts: false + # discussionCategory: autobuilded-releases + # artifacts: "build/app/outputs/flutter-apk/*" + # tag: Auto-${{ github.run_number }} + # body: Builded with GitHub Action workflow + # token: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 51d2d1b..f2101c6 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -693,6 +693,7 @@ class ResultsStats { double get spp => score / piecesPlaced; double get kps => inputs / (finalTime.inMicroseconds / 1000000); double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0; + double get cps => zenith != null ? zenith!.avgrankpts / (finalTime.inMilliseconds / 1000 * 60) : 0; ResultsStats( { @@ -1399,7 +1400,7 @@ class TetraLeagueAlpha { } class RecordSingle { - late String userId; + late String? userId; late String replayId; late String ownId; late String gamemode; @@ -1408,6 +1409,7 @@ class RecordSingle { late int rank; late int countryRank; late AggregateStats aggregateStats; + late RecordExtras extras; RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); @@ -1418,10 +1420,17 @@ class RecordSingle { stats = ResultsStats.fromJson(json['results']['stats']); replayId = json['replayid']; timestamp = DateTime.parse(json['ts']); - userId = json['user']['id']; + if (json['user'] != null) userId = json['user']['id']; rank = ran; countryRank = cran; aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']); + var ex = json['extras'] as Map; + switch (ex.keys.firstOrNull){ + case "zenith": + extras = ZenithExtras.fromJson(json['extras']['zenith']); + default: + break; + } } Map toJson() { @@ -1460,6 +1469,18 @@ class AggregateStats{ } } +class RecordExtras{ + +} + +class ZenithExtras extends RecordExtras{ + List mods = []; + + ZenithExtras.fromJson(Map json){ + for (var mod in json["mods"]) mods.add(mod); + } +} + class TetrioZen { late int level; late int score; diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 4c181cc..6b2d7c8 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: 1198 (599 per locale) +/// Strings: 1216 (608 per locale) /// -/// Built on 2024-07-27 at 18:54 UTC +/// Built on 2024-07-31 at 20:51 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -226,7 +226,7 @@ class Translations implements BaseTranslations { 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 preSeasonMessage({required Object n}) => 'Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\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'; @@ -242,6 +242,17 @@ class Translations implements BaseTranslations { String get neverPlayedTL => 'That user never played Tetra League'; String get botTL => 'Bots are not allowed to play Tetra League'; String get anonTL => 'Guests are not allowed to play Tetra League'; + String get quickPlay => 'Quick Play'; + String get expert => 'Expert'; + String get withMods => 'With mods'; + String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'with ${n} mods', + one: 'with ${n} mod', + two: 'with ${n} mods', + few: 'with ${n} mods', + many: 'with ${n} mods', + other: 'with ${n} mods', + ); String get exportDB => 'Export local database'; String get exportDBDescription => 'It contains states and Tetra League records of the tracked players and list of tracked players.'; String get desktopExportAlertTitle => 'Desktop export'; @@ -929,7 +940,7 @@ class _StringsRu implements Translations { @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 preSeasonMessage({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона'; @override String get nanow => 'Пока недоступно...'; @override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}'; @override String get seasonEnded => 'Сезон закончился'; @@ -945,6 +956,17 @@ class _StringsRu implements Translations { @override String get neverPlayedTL => 'Этот игрок никогда не играл в Тетра Лигу'; @override String get botTL => 'Ботам нельзя играть в Тетра Лигу'; @override String get anonTL => 'Гостям нельзя играть в Тетра Лигу'; + @override String get quickPlay => 'Быстрая Игра'; + @override String get expert => 'Эксперт'; + @override String get withMods => 'С модами'; + @override String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: 'с ${n} модами', + one: 'с ${n} модом', + two: 'с ${n} модами', + few: 'с ${n} модами', + many: 'с ${n} модами', + other: 'с ${n} модами', + ); @override String get exportDB => 'Экспортировать локальную базу данных'; @override String get exportDBDescription => 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; @override String get desktopExportAlertTitle => 'Экспорт на десктопе'; @@ -1624,7 +1646,7 @@ extension on Translations { 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 'preSeasonMessage': return ({required Object n}) => 'Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\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'; @@ -1640,6 +1662,17 @@ extension on Translations { case 'neverPlayedTL': return 'That user never played Tetra League'; case 'botTL': return 'Bots are not allowed to play Tetra League'; case 'anonTL': return 'Guests are not allowed to play Tetra League'; + case 'quickPlay': return 'Quick Play'; + case 'expert': return 'Expert'; + case 'withMods': return 'With mods'; + case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'with ${n} mods', + one: 'with ${n} mod', + two: 'with ${n} mods', + few: 'with ${n} mods', + many: 'with ${n} mods', + other: 'with ${n} mods', + ); case 'exportDB': return 'Export local database'; case 'exportDBDescription': return 'It contains states and Tetra League records of the tracked players and list of tracked players.'; case 'desktopExportAlertTitle': return 'Desktop export'; @@ -2243,7 +2276,7 @@ extension on _StringsRu { 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 'preSeasonMessage': return ({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона'; case 'nanow': return 'Пока недоступно...'; case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}'; case 'seasonEnded': return 'Сезон закончился'; @@ -2259,6 +2292,17 @@ extension on _StringsRu { case 'neverPlayedTL': return 'Этот игрок никогда не играл в Тетра Лигу'; case 'botTL': return 'Ботам нельзя играть в Тетра Лигу'; case 'anonTL': return 'Гостям нельзя играть в Тетра Лигу'; + case 'quickPlay': return 'Быстрая Игра'; + case 'expert': return 'Эксперт'; + case 'withMods': return 'С модами'; + case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: 'с ${n} модами', + one: 'с ${n} модом', + two: 'с ${n} модами', + few: 'с ${n} модами', + many: 'с ${n} модами', + other: 'с ${n} модами', + ); case 'exportDB': return 'Экспортировать локальную базу данных'; case 'exportDBDescription': return 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; case 'desktopExportAlertTitle': return 'Экспорт на десктопе'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 382201b..80318f0 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -90,8 +90,6 @@ class CacheController { return object.runtimeType.toString(); case TetrioPlayerFromLeaderboard: // i may be a little stupid return "${object.runtimeType}topone"; - case TetraLeagueAlphaStream: - return object.runtimeType.toString()+object.userId; case SingleplayerStream: return object.type+object.userId; default: @@ -99,8 +97,8 @@ class CacheController { } } - void store(dynamic object, int? cachedUntil) async { - String key = _getObjectId(object) + cachedUntil!.toString(); + void store(dynamic object, int cachedUntil) async { + String key = _getObjectId(object) + cachedUntil.toString(); _cache[key] = object; } @@ -113,6 +111,8 @@ class CacheController { objectEntry = id.length <= 16 ? _cache.entries.firstWhere((element) => element.key.startsWith(_nicknames[id]??"huh?")) : _cache.entries.firstWhere((element) => element.key.startsWith(id)); if (id.length <= 16) id = _nicknames[id]??"huh?"; break; + case SingleplayerStream: + objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(id)); default: objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(datatype.toString()+id)); id = datatype.toString()+id; @@ -309,15 +309,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 fetchSingleplayerStream(String userID, String stream) async { - SingleplayerStream? cached = _cache.get(userID, SingleplayerStream); + Future fetchStream(String userID, String stream) async { + SingleplayerStream? cached = _cache.get(stream+userID, SingleplayerStream); if (cached != null) return cached; Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream}); } else { - url = Uri.https('ch.tetr.io', 'api/streams/${stream}_${userID.toLowerCase().trim()}'); + url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/$stream'); } try { final response = await client.get(url); @@ -325,7 +325,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: if (jsonDecode(response.body)['success']) { - SingleplayerStream records = SingleplayerStream.fromJson(jsonDecode(response.body)['data']['records'], userID, stream); + SingleplayerStream records = SingleplayerStream.fromJson(jsonDecode(response.body)['data']['entries'], userID, stream); _cache.store(records, jsonDecode(response.body)['cache']['cached_until']); developer.log("fetchSingleplayerStream: $stream $userID stream retrieved and cached", name: "services/tetrio_crud"); return records; @@ -709,7 +709,7 @@ class TetrioService extends DB { /// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve. Future fetchNews(String userID) async{ - News? cached = _cache.get(userID, News); + News? cached = _cache.get("user_$userID", News); if (cached != null) return cached; Uri url; @@ -757,7 +757,7 @@ 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 { - TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueAlphaStream); + TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream); if (cached != null) return cached; Uri url; @@ -957,7 +957,9 @@ class TetrioService extends DB { 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); + Summaries summaries = Summaries.fromJson(jsonDecode(response.body)['data'], id); + _cache.store(summaries, jsonDecode(response.body)['cache']['cached_until']); + return summaries; } else { developer.log("fetchSummaries: User dosen't exist", name: "services/tetrio_crud", error: response.body); throw TetrioPlayerNotExist(); @@ -1183,6 +1185,8 @@ class TetrioService extends DB { } case 403: throw TetrioForbidden(); + case 404: + throw TetrioPlayerNotExist(); case 429: throw TetrioTooManyRequests(); case 418: diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 8f3fb1b..d195de7 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -23,6 +23,7 @@ import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/singleplayer_record_view.dart'; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; +import 'package:tetra_stats/views/zenith_record_view.dart'; import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/graphs.dart'; @@ -36,6 +37,7 @@ import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; +import 'package:tetra_stats/widgets/zenith_thingy.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; @@ -82,6 +84,7 @@ class _MainState extends State with TickerProviderStateMixin { bool _TLHistoryWasFetched = false; late TabController _tabController; late TabController _wideScreenTabController; + bool zenithEX = false; String get title => "Tetra Stats: $_titleNickname"; @@ -89,8 +92,8 @@ class _MainState extends State with TickerProviderStateMixin { void initState() { initDB(); _scrollController = ScrollController(); - _tabController = TabController(length: 7, vsync: this); - _wideScreenTabController = TabController(length: 4, vsync: this); + _tabController = TabController(length: 9, vsync: this); + _wideScreenTabController = TabController(length: 5, vsync: this); _zoomPanBehavior = ZoomPanBehavior( enablePinching: true, enableSelectionZooming: true, @@ -160,29 +163,33 @@ class _MainState extends State with TickerProviderStateMixin { late List requests; late Summaries summaries; late TetraLeagueBetaStream tlStream; - late UserRecords records; late News news; - late SingleplayerStream recent; - late SingleplayerStream sprint; - late SingleplayerStream blitz; - late TetrioPlayerFromLeaderboard? topOne; - late TopTr? topTR; - requests = await Future.wait([ // all at once (7 requests to oskware lmao) + // late SingleplayerStream recentSprint; + // late SingleplayerStream recentBlitz; + // late SingleplayerStream sprint; + // late SingleplayerStream blitz; + late SingleplayerStream recentZenith; + late SingleplayerStream recentZenithEX; + // late TetrioPlayerFromLeaderboard? topOne; + // late TopTr? topTR; + requests = await Future.wait([ // all at once (8 requests to oskware in total) teto.fetchSummaries(_searchFor), teto.fetchTLStream(_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.fetchStream(_searchFor, "zenith/recent"), + teto.fetchStream(_searchFor, "zenithex/recent"), + //teto.fetchStream(_searchFor, "40l/top"), + //teto.fetchStream(_searchFor, "blitz/top"), ]); + //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 summaries = requests[0] as Summaries; tlStream = requests[1] as TetraLeagueBetaStream; // records = requests[1] as UserRecords; news = requests[2] as News; + recentZenith = requests[3] as SingleplayerStream; + recentZenithEX = requests[4] as SingleplayerStream; // recent = requests[3] as SingleplayerStream; // sprint = requests[4] as SingleplayerStream; // blitz = requests[5] as SingleplayerStream; @@ -194,7 +201,7 @@ class _MainState extends State with TickerProviderStateMixin { // Get tetra League leaderboard everyone = teto.getCachedLeaderboard(); everyone ??= await teto.fetchTLLeaderboard(); - if (meAmongEveryone == null){ + if (meAmongEveryone == null && everyone!.leaderboard.isNotEmpty){ meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } @@ -308,7 +315,7 @@ class _MainState extends State with TickerProviderStateMixin { changePlayer(me.userId); }); } - return [me, summaries, news, tlStream]; + return [me, summaries, news, tlStream, recentZenith, recentZenithEX]; //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; } @@ -317,6 +324,10 @@ class _MainState extends State with TickerProviderStateMixin { setState(() {}); } + void toggleZenith(){ + setState(() {zenithEX = !zenithEX;}); + } + @override Widget build(BuildContext context) { final t = Translations.of(context); @@ -433,14 +444,15 @@ class _MainState extends State with TickerProviderStateMixin { tabs: bigScreen ? [ Tab(text: t.tetraLeague,), Tab(text: t.history), - Tab(text: "Quick Play"), + Tab(text: t.quickPlay), Tab(text: "${t.sprint} & ${t.blitz}"), Tab(text: t.other), ] : [ Tab(text: t.tetraLeague), Tab(text: t.tlRecords), Tab(text: t.history), - Tab(text: "Quick Play"), + Tab(text: t.quickPlay), + Tab(text: "${t.quickPlay} ${t.recent}"), Tab(text: t.sprint), Tab(text: t.blitz), Tab(text: t.recentRuns), @@ -483,7 +495,20 @@ class _MainState extends State with TickerProviderStateMixin { ), ],), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0), - _ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: MediaQuery.of(context).size.width-450, + constraints: const BoxConstraints(maxWidth: 1024), + child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX) + ), + SizedBox( + width: 450.0, + child: _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true), + ) + ], + ), _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]) ] : [ @@ -506,7 +531,8 @@ class _MainState extends State with TickerProviderStateMixin { ), _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![1].league.gamesPlayed > 0), - _ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx), + ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX), + _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true), SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")), SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "Blitz")), _RecentSingleplayersThingy(SingleplayerStream(userId: "userId", records: [], type: "recent")), @@ -768,6 +794,63 @@ class _TLRecords extends StatelessWidget { } } +class _ZenithRecords extends StatelessWidget { + final String userID; + final SingleplayerStream data; + final bool separateScrollController; + + /// Widget, that displays Quick Play records. + /// Accepts list of TL records ([data]) and [userID] of player from the view + const _ZenithRecords({required this.userID, required this.data, this.separateScrollController = false}); + + @override + Widget build(BuildContext context) { + if (data.records.isEmpty) { + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + ], + )); + } + bool bigScreen = MediaQuery.of(context).size.width >= 768; + int length = data.records.length; + return ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + controller: separateScrollController ? ScrollController() : null, + itemCount: length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == length) { + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + ], + )); + } + const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100, fontSize: 13); + return Container( + child: ListTile( + leading: Text("QP", + style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), + title: Text("${f2.format(data.records[index].stats.zenith!.altitude)} m${(data.records[index].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (data.records[index].extras as ZenithExtras).mods.length)})" : ""}"), + subtitle: Text(timestamp(data.records[index].timestamp), style: const TextStyle(color: Colors.grey)), + trailing: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("${f2.format(data.records[index].aggregateStats.apm)} APM, ${f2.format(data.records[index].aggregateStats.pps)} PPS", style: style, textAlign: TextAlign.right), + Text("${f2.format(data.records[index].stats.cps)} CSP (${f2.format(data.records[index].stats.zenith!.peakrank)} peak)", style: style, textAlign: TextAlign.right), + Text("${data.records[index].stats.kills} KO's, ${getMoreNormalTime(data.records[index].stats.finalTime)}", style: style, textAlign: TextAlign.right) + ], + ), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => ZenithRecordView(record: data.records[index]))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), + ), + ); + }); + } +} + class _History extends StatelessWidget{ final List>> chartsData; final String userID; @@ -1227,230 +1310,6 @@ class _RecentSingleplayersThingy extends StatelessWidget { } } -class _ZenithThingy extends StatefulWidget{ - final RecordSingle? record; - final RecordSingle? recordEX; - - _ZenithThingy({this.record, this.recordEX}); - - @override - State<_ZenithThingy> createState() => _ZenithThingyState(); -} - -class _ZenithThingyState extends State<_ZenithThingy> { - late RecordSingle? record; - bool ex = false; - - @override - void initState(){ - super.initState(); - record = ex ? widget.recordEX : widget.record; - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints){ - bool bigScreen = constraints.maxWidth > 768; - if (record == null) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Text("Quick Play${ex ? " Expert" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: "--- m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.grey), - ), - ), - TextButton(onPressed: (){ - if (ex){ - ex = false; - }else{ - ex = true; - } - setState(() { - record = ex ? widget.recordEX : widget.record; - }); - }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), - ], - ), - ); - } - return SingleChildScrollView( - child: Padding(padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Text("Quick Play${ex ? " Expert" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: "${f2.format(record!.stats.zenith!.altitude)} m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (record!.rank != -1) TextSpan(text: "№${record!.rank}"), - if (record!.rank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"), - if (record!.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(widget.record!.timestamp)), - ] - ), - ), - TextButton(onPressed: (){ - if (ex){ - ex = false; - }else{ - ex = true; - } - setState(() { - record = ex ? widget.recordEX : widget.record; - }); - }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), - Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true) - ], - ), - FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), - LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), - Table( - children: [ - TableRow( - children: [ - Text("Floor"), - Text("Split"), - Text("Total"), - ] - ), - for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( - children: [ - Text((i+1).toString()), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---"), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---"), - ] - ) - ], - ), - ], - ), - ), - ), - Column( - children: [ - Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 35, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}") - ]), - GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}") - ]) - ]), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true,), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: false), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true) - ]), - ) - ], - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), - ) - ], - ) - ), - ); - }); - } -} - class _OtherThingy extends StatelessWidget { final TetrioZen? zen; final String? bio; diff --git a/lib/views/zenith_record_view.dart b/lib/views/zenith_record_view.dart new file mode 100644 index 0000000..3b61276 --- /dev/null +++ b/lib/views/zenith_record_view.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:tetra_stats/widgets/zenith_thingy.dart'; + +class ZenithRecordView extends StatelessWidget { + final RecordSingle record; + + const ZenithRecordView({super.key, required this.record}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + //bool bigScreen = MediaQuery.of(context).size.width >= 368; + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: Text("${ + switch (record.gamemode){ + "zenith" => "Quick Play", + "zenithex" => "Quick Play Expert", + String() => "5000000 Blast", + } + } ${timestamp(record.timestamp)}"), + ), + body: SafeArea( + child: SingleChildScrollView( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + ZenithThingy(record: record, switchable: false), + // TODO: Insert replay link here + ] + ) + ], + ) + ) + ), + ); + } + +} \ No newline at end of file diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index 3e5e3e3..7bb057b 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/open_in_browser.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; @@ -22,15 +23,6 @@ class SingleplayerRecord extends StatelessWidget { /// Widget that displays data from [record] const SingleplayerRecord({super.key, required this.record, this.stream, this.rank, this.hideTitle = false}); - Color getColorOfRank(int rank){ - if (rank == 1) return Colors.yellowAccent; - if (rank == 2) return Colors.blueGrey; - if (rank == 3) return Colors.brown[400]!; - if (rank <= 9) return Colors.blueAccent; - if (rank <= 99) return Colors.greenAccent; - return Colors.grey; - } - @override Widget build(BuildContext context) { if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); @@ -94,8 +86,8 @@ class SingleplayerRecord extends StatelessWidget { 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!))), - if (record!.rank != null) const TextSpan(text: " • "), + if (record!.rank != -1) TextSpan(text: "№${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank))), + if (record!.rank != -1) const TextSpan(text: " • "), TextSpan(text: timestamp(record!.timestamp)), ] ), diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 44ced86..6a9586e 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -52,7 +52,7 @@ class StatCellNum extends StatelessWidget { RichText( text: TextSpan(text: splited[0], children: [ - if ((fractionDigits??0) > 0) TextSpan(text: f.symbols.DECIMAL_SEP+splited[1], style: smallDecimal ? const TextStyle(fontFamily: "Eurostile Round", fontSize: 16) : null) + if ((fractionDigits??0) > 0 && splited.elementAtOrNull(1) != null) TextSpan(text: f.symbols.DECIMAL_SEP+splited[1], style: smallDecimal ? const TextStyle(fontFamily: "Eurostile Round", fontSize: 16) : null) ], style: TextStyle( fontFamily: "Eurostile Round Extended", diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart new file mode 100644 index 0000000..14dc7fb --- /dev/null +++ b/lib/widgets/zenith_thingy.dart @@ -0,0 +1,259 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/widgets/finesse_thingy.dart'; +import 'package:tetra_stats/widgets/gauget_num.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; +import 'package:tetra_stats/widgets/lineclears_thingy.dart'; +import 'package:tetra_stats/widgets/stat_sell_num.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class ZenithThingy extends StatefulWidget{ + final RecordSingle? record; + final bool switchable; + final bool initEXvalue; + final RecordSingle? recordEX; + final Function? parentZenithToggle; + + ZenithThingy({this.record, this.recordEX, this.switchable = true, this.parentZenithToggle, this.initEXvalue = false}); + + @override + State createState() => _ZenithThingyState(); +} + +class _ZenithThingyState extends State { + late RecordSingle? record; + bool ex = false; + + @override + void initState(){ + ex = widget.initEXvalue; + + super.initState(); + if (widget.switchable){ + record = (ex ? widget.recordEX : widget.record); + }else{ + record = widget.record; + ex = widget.record!.gamemode == "zenithex"; + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints){ + bool bigScreen = constraints.maxWidth > 768; + if (record == null) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: "--- m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.grey), + ), + ), + TextButton(onPressed: (){ + if (ex){ + ex = false; + }else{ + ex = true; + } + setState(() { + if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); + record = ex ? widget.recordEX : widget.record; + }); + }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), + ], + ), + ); + } + return SingleChildScrollView( + child: Padding(padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: "${f2.format(record!.stats.zenith!.altitude)} m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + if ((record!.extras as ZenithExtras).mods.isNotEmpty) RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.white), + children: [ + TextSpan(text: "${t.withMods}: "), + for (String mod in (record!.extras as ZenithExtras).mods) TextSpan(text: "${mod.toUpperCase()} "), + ] + ), + ), + RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (record!.rank != -1) TextSpan(text: "№${record!.rank}"), + if (record!.rank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"), + if (record!.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(widget.record!.timestamp)), + ] + ), + ), + if (widget.switchable) TextButton(onPressed: (){ + if (ex){ + ex = false; + }else{ + ex = true; + } + setState(() { + if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); + record = ex ? widget.recordEX : widget.record; + }); + }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), + Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 20, + children: [ + StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), + StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), + StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true), + StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "CPS\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) + ], + ), + FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), + LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + Table( + children: [ + TableRow( + children: [ + Text("Floor"), + Text("Split"), + Text("Total"), + ] + ), + for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( + children: [ + Text((i+1).toString()), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---"), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---"), + ] + ) + ], + ), + ], + ), + ), + ), + Column( + children: [ + Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Padding( + padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 35, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ + GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), + GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), + GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), + GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), + GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.appDescription), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}") + ]), + GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ + GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), + GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), + GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.vsapmDescription), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}") + ]) + ]), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, + alertWidgets: [Text(t.statCellNum.dssDescription), + Text("${t.formula}: (VS / 100) - (APM / 60)"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),], + okText: t.popupActions.ok, + higherIsBetter: true,), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, + alertWidgets: [Text(t.statCellNum.dspDescription), + Text("${t.formula}: DS/S / PPS"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, + alertWidgets: [Text(t.statCellNum.appdspDescription), + Text("${t.formula}: APP + DS/P"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, + alertWidgets: [Text(t.statCellNum.cheeseDescription), + Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),], + okText: t.popupActions.ok, + higherIsBetter: false), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, + alertWidgets: [Text(t.statCellNum.gbeDescription), + Text("${t.formula}: APP * DS/P * 2"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, + alertWidgets: [Text(t.statCellNum.nyaappDescription), + Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, + alertWidgets: [Text(t.statCellNum.areaDescription), + Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),], + okText: t.popupActions.ok, + higherIsBetter: true) + ]), + ) + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), + ) + ], + ) + ), + ); + }); + } +} \ No newline at end of file diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index a2200b2..89a9dcd 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -91,7 +91,7 @@ "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", + "preSeasonMessage": "Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied", "nanow": "Not avaliable for now...", "seasonEnds": "Season ends in ${countdown}", "seasonEnded": "Season has ended", @@ -107,6 +107,17 @@ "neverPlayedTL": "That user never played Tetra League", "botTL": "Bots are not allowed to play Tetra League", "anonTL": "Guests are not allowed to play Tetra League", + "quickPlay": "Quick Play", + "expert": "Expert", + "withMods": "With mods", + "withModsPlural":{ + "zero": "with $n mods", + "one": "with $n mod", + "two": "with $n mods", + "few": "with $n mods", + "many": "with $n mods", + "other": "with $n mods" + }, "exportDB": "Export local database", "exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.", "desktopExportAlertTitle": "Desktop export", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 61e2bbb..f8dbbc9 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -91,7 +91,7 @@ "seasonStarts": "Сезон начнётся через:", "myMessadgeHeader": "Сообщение от dan63", "myMessadgeBody": "TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.", - "preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона", + "preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона", "nanow": "Пока недоступно...", "seasonEnds": "Сезон закончится через ${countdown}", "seasonEnded": "Сезон закончился", @@ -107,6 +107,17 @@ "neverPlayedTL": "Этот игрок никогда не играл в Тетра Лигу", "botTL": "Ботам нельзя играть в Тетра Лигу", "anonTL": "Гостям нельзя играть в Тетра Лигу", + "quickPlay": "Быстрая Игра", + "expert": "Эксперт", + "withMods": "С модами", + "withModsPlural":{ + "zero": "с $n модами", + "one": "с $n модом", + "two": "с $n модами", + "few": "с $n модами", + "many": "с $n модами", + "other": "с $n модами" + }, "exportDB": "Экспортировать локальную базу данных", "exportDBDescription": "Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.", "desktopExportAlertTitle": "Экспорт на десктопе", From b93898cc340c06f0384ff6c4766d878515b8e80e Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 2 Aug 2024 02:20:36 +0300 Subject: [PATCH 07/33] 1 small fix + redesign thoughts --- lib/main.dart | 3 +- lib/views/main_view_tiles.dart | 411 ++++++++++++++++++--------------- lib/views/tl_match_view.dart | 2 +- 3 files changed, 225 insertions(+), 191 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index fdc1407..8306737 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,8 +30,9 @@ ThemeData theme = ThemeData( colorScheme: const ColorScheme.dark( primary: Colors.cyanAccent, surface: Color.fromARGB(255, 10, 10, 10), - secondary: Colors.white + secondary: Colors.white, ), + cardTheme: CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), scaffoldBackgroundColor: Colors.black ); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 65e90d9..f6e0dd9 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,8 +1,6 @@ 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'; @@ -30,6 +28,17 @@ class MainView extends StatefulWidget { State createState() => _MainState(); } +enum Cards {overview, tetraLeague, quickPlay, quickPlayExpert, sprint, blitz, other} +Map cardsTitles = { + Cards.overview: "Overview", + Cards.tetraLeague: t.tetraLeague, + Cards.quickPlay: t.quickPlay, + Cards.quickPlayExpert: "${t.quickPlay} ${t.expert}", + Cards.sprint: t.sprint, + Cards.blitz: t.blitz, + Cards.other: t.other +}; + TetrioPlayer testPlayer = TetrioPlayer( userId: "6098518e3d5155e6ec429cdc", username: "dan63", @@ -85,209 +94,234 @@ 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 - ), - Expanded( - child: Scrollbar( - controller: controller, - thumbVisibility: true, - child: SingleChildScrollView( - controller: controller, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - 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))))))) - ], + return Scaffold( + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Row( + children: [ + NavigationRail( + leading: FloatingActionButton( + elevation: 0, + onPressed: () { + // Add your onPressed code here! + }, + child: const Icon(Icons.search), + ), + trailing: IconButton( + onPressed: () { + // Add your onPressed code here! + }, + icon: const Icon(Icons.more_horiz_rounded), + ), + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.home), + selectedIcon: Icon(Icons.home), + label: Text('First'), + ), + NavigationRailDestination( + icon: Icon(Icons.leaderboard), + selectedIcon: Icon(Icons.leaderboard), + label: Text('Second'), + ), + NavigationRailDestination( + icon: Icon(Icons.compress), + selectedIcon: Icon(Icons.compress), + label: Text('Third'), + ), + NavigationRailDestination( + icon: Icon(Icons.calculate), + selectedIcon: Icon(Icons.calculate), + label: Text('Calc'), + ), + NavigationRailDestination( + icon: Icon(Icons.settings), + selectedIcon: Icon(Icons.settings), + label: Text('Third'), + ) + ], + selectedIndex: 0 + ), + Row( + children: [ + 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( + 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)) + ], + ), ), - ), - 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( + 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: [ - 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), - ], - ) + 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", + 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.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); - } - ); - }, - ) + 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( + ) + ], + ), + ) + ], + ), + ), + if (testPlayer.distinguishment != null) DistinguishmentThingy(testPlayer.distinguishment!), + if (testPlayer.bio != null) Card( + child: Column( + children: [ + Row( 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: [ - Card( - child: Row( - children: [ - Spacer(), - Text("test card"), + 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)), + ) + ], + ), ), - ), - SizedBox( - width: 450.0, - child: Column( - children: [ - Card( - child: Row( - children: [ - Spacer(), - Text("test card"), - Spacer() - ], - ), - ) - ], + //if (testNews != null && testNews!.news.isNotEmpty) + Expanded(child: NewsThingy(testNews)) + ], + ) + ), + SizedBox( + width: constraints.maxWidth - 450 - 80, + child: Column( + //crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Spacer() + ], + ), ), - ), - SizedBox( - width: 450.0, - child: Column( - children: [ - Card( - child: Row( - children: [ - Spacer(), - Text("test card"), - Spacer() - ], - ), - ) + Card(), + SegmentedButton( + segments: const >[ + ButtonSegment( + value: Cards.overview, + label: Text('Overview'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + label: Text('Tetra League'), + icon: Icon(Icons.calendar_view_week)), + ButtonSegment( + value: Cards.quickPlay, + label: Text('Quick Play'), + icon: Icon(Icons.calendar_view_month)), + // ButtonSegment( + // value: Cards.quickPlayExpert, + // label: Text('QP Expert'), + // icon: Icon(Icons.calendar_today)), + ButtonSegment( + value: Cards.sprint, + label: Text('40 Lines'), + icon: Icon(Icons.calendar_today)), + ButtonSegment( + value: Cards.blitz, + label: Text('Blitz'), + icon: Icon(Icons.calendar_today)), + // ButtonSegment( + // value: Cards.other, + // label: Text('Other'), + // icon: Icon(Icons.calendar_today)), ], - ), - ), - ], + selected: {Cards.tetraLeague}, + onSelectionChanged: (Set newSelection) { + setState(() { + // By default there is only a single segment that can be + // selected at one time, so its value is always the first + // item in the selected set. + //calendarView = newSelection.first; + });}) + ], + ), ), + ], ), - ), - ), - ], - )); + ], + ); + }, + )); } } @@ -599,7 +633,6 @@ class NewUserThingy extends StatelessWidget { return Card( clipBehavior: Clip.antiAlias, - surfaceTintColor: theme.colorScheme.surface, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Column( diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index fdd2f76..1e8e8f3 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -338,7 +338,7 @@ class TlMatchResultState extends State { CompareThingy( label: t.statCellNum.estOfTRShort, greenSide: roundSelector == -2 ? timeWeightedStats[0].estTr.esttr : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr, + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr, redSide: roundSelector == -2 ? timeWeightedStats[1].estTr.esttr : roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.estTr.esttr, fractionDigits: 2, From a3d9aa5e70cd457972c3ef676d9499792f7b5290 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 3 Aug 2024 20:52:20 +0300 Subject: [PATCH 08/33] zenith view fix + `dart fix` + redesign progress --- lib/data_objects/tetrio.dart | 28 +- .../tetrio_multiplayer_replay.dart | 40 +- lib/main.dart | 8 +- lib/services/tetrio_crud.dart | 1 - lib/utils/relative_timestamps.dart | 7 +- lib/views/main_view.dart | 22 +- lib/views/main_view_tiles.dart | 175 ++++++--- lib/views/mathes_view.dart | 1 - lib/views/tl_match_view.dart | 36 +- lib/views/zenith_record_view.dart | 22 +- lib/widgets/graphs.dart | 4 +- lib/widgets/stat_sell_num.dart | 2 +- lib/widgets/tl_rating_thingy.dart | 3 +- lib/widgets/tl_thingy.dart | 1 - lib/widgets/zenith_thingy.dart | 349 +++++++++--------- pubspec.lock | 216 ++++++----- pubspec.yaml | 2 +- 17 files changed, 486 insertions(+), 431 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index f2101c6..316d341 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -777,7 +777,9 @@ class ZenithResults{ speedrun = json['speedrun']; speedrunSeen = json['speedrun_seen']; splits = []; - for (int ms in json['splits']) splits.add(Duration(milliseconds: ms)); + for (int ms in json['splits']) { + splits.add(Duration(milliseconds: ms)); + } } } @@ -935,7 +937,9 @@ class TetraLeagueBetaStream{ TetraLeagueBetaStream.fromJson(List json, String userID) { id = userID; - for (var entry in json) records.add(BetaRecord.fromJson(entry)); + for (var entry in json) { + records.add(BetaRecord.fromJson(entry)); + } } addFromAlphaStream(List r){ @@ -991,7 +995,7 @@ class TetraLeagueBetaStream{ naturalorder: entry.endContext[0].naturalOrder, active: false, alive: false, - lifetime: Duration(milliseconds: -1), + lifetime: const Duration(milliseconds: -1), stats: BetaLeagueStats( apm: entry.endContext[0].secondaryTracking[i], pps: entry.endContext[0].tertiaryTracking[i], @@ -1008,7 +1012,7 @@ class TetraLeagueBetaStream{ naturalorder: entry.endContext[1].naturalOrder, active: false, alive: false, - lifetime: Duration(milliseconds: -1), + lifetime: const Duration(milliseconds: -1), stats: BetaLeagueStats( apm: entry.endContext[1].secondaryTracking[i], pps: entry.endContext[1].tertiaryTracking[i], @@ -1072,10 +1076,14 @@ class BetaLeagueResults{ BetaLeagueResults({required this.leaderboard, required this.rounds}); BetaLeagueResults.fromJson(Map json){ - for (var lbEntry in json['leaderboard']) leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry)); + 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)); + for (var r in roundEntry) { + round.add(BetaLeagueRound.fromJson(r)); + } rounds.add(round); } } @@ -1477,7 +1485,9 @@ class ZenithExtras extends RecordExtras{ List mods = []; ZenithExtras.fromJson(Map json){ - for (var mod in json["mods"]) mods.add(mod); + for (var mod in json["mods"]) { + mods.add(mod); + } } } @@ -2407,8 +2417,8 @@ class TetrioPlayerFromLeaderboard { 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; + glicko = json['league']['glicko']?.toDouble(); + rd = json['league']['rd']?.toDouble(); rank = json['league']['rank']; bestRank = json['league']['bestrank']; apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00; diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 9b521ad..9b2a5ac 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -208,12 +208,12 @@ class ReplayData{ stats = []; roundWinners = []; int roundID = 0; - List APMmultipliedByWeights = [0, 0]; - List PPSmultipliedByWeights = [0, 0]; - List VSmultipliedByWeights = [0, 0]; - List SPPmultipliedByWeights = [0, 0]; - List KPPmultipliedByWeights = [0, 0]; - List KPSmultipliedByWeights = [0, 0]; + List apmMultipliedByWeights = [0, 0]; + List ppsMultipliedByWeights = [0, 0]; + List vsMultipliedByWeights = [0, 0]; + List sppMultipliedByWeights = [0, 0]; + List kppMultipliedByWeights = [0, 0]; + List kpsMultipliedByWeights = [0, 0]; totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()]; for(var round in json['data']) { int firstInEndContext = round['replays'][0]["events"].last['data']['export']['options']['username'].startsWith(endcontext[0].username) ? 0 : 1; @@ -221,30 +221,30 @@ class ReplayData{ int roundLength = max(round['replays'][0]['frames'], round['replays'][1]['frames']); roundLengths.add(roundLength); totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']); - APMmultipliedByWeights[0] += endcontext[0].secondaryTracking[roundID]*roundLength; - APMmultipliedByWeights[1] += endcontext[1].secondaryTracking[roundID]*roundLength; - PPSmultipliedByWeights[0] += endcontext[0].tertiaryTracking[roundID]*roundLength; - PPSmultipliedByWeights[1] += endcontext[1].tertiaryTracking[roundID]*roundLength; - VSmultipliedByWeights[0] += endcontext[0].extraTracking[roundID]*roundLength; - VSmultipliedByWeights[1] += endcontext[1].extraTracking[roundID]*roundLength; + apmMultipliedByWeights[0] += endcontext[0].secondaryTracking[roundID]*roundLength; + apmMultipliedByWeights[1] += endcontext[1].secondaryTracking[roundID]*roundLength; + ppsMultipliedByWeights[0] += endcontext[0].tertiaryTracking[roundID]*roundLength; + ppsMultipliedByWeights[1] += endcontext[1].tertiaryTracking[roundID]*roundLength; + vsMultipliedByWeights[0] += endcontext[0].extraTracking[roundID]*roundLength; + vsMultipliedByWeights[1] += endcontext[1].extraTracking[roundID]*roundLength; int winner = round['board'].indexWhere((element) => element['success'] == true); roundWinners.add([round['board'][winner]['id']??round['board'][winner]['user']['_id'], round['board'][winner]['username']??round['board'][winner]['user']['username']]); ReplayStats playerOne = ReplayStats.fromJson(round['replays'][firstInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][secondInEndContext]['events']), round['replays'][firstInEndContext]['frames']); // (events contain recived attacks) ReplayStats playerTwo = ReplayStats.fromJson(round['replays'][secondInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][firstInEndContext]['events']), round['replays'][secondInEndContext]['frames']); - SPPmultipliedByWeights[0] += playerOne.spp*roundLength; - SPPmultipliedByWeights[1] += playerTwo.spp*roundLength; - KPPmultipliedByWeights[0] += playerOne.kpp*roundLength; - KPPmultipliedByWeights[1] += playerTwo.kpp*roundLength; - KPSmultipliedByWeights[0] += playerOne.kps*roundLength; - KPSmultipliedByWeights[1] += playerTwo.kps*roundLength; + sppMultipliedByWeights[0] += playerOne.spp*roundLength; + sppMultipliedByWeights[1] += playerTwo.spp*roundLength; + kppMultipliedByWeights[0] += playerOne.kpp*roundLength; + kppMultipliedByWeights[1] += playerTwo.kpp*roundLength; + kpsMultipliedByWeights[0] += playerOne.kps*roundLength; + kpsMultipliedByWeights[1] += playerTwo.kps*roundLength; stats.add([playerOne, playerTwo]); totalStats[0] = totalStats[0] + playerOne; totalStats[1] = totalStats[1] + playerTwo; roundID ++; } timeWeightedStats = [ - AggregateStats(APMmultipliedByWeights[0]/totalLength, PPSmultipliedByWeights[0]/totalLength, VSmultipliedByWeights[0]/totalLength, SPPmultipliedByWeights[0]/totalLength, KPPmultipliedByWeights[0]/totalLength, KPSmultipliedByWeights[0]/totalLength), - AggregateStats(APMmultipliedByWeights[1]/totalLength, PPSmultipliedByWeights[1]/totalLength, VSmultipliedByWeights[1]/totalLength, SPPmultipliedByWeights[1]/totalLength, KPPmultipliedByWeights[1]/totalLength, KPSmultipliedByWeights[1]/totalLength) + AggregateStats(apmMultipliedByWeights[0]/totalLength, ppsMultipliedByWeights[0]/totalLength, vsMultipliedByWeights[0]/totalLength, sppMultipliedByWeights[0]/totalLength, kppMultipliedByWeights[0]/totalLength, kpsMultipliedByWeights[0]/totalLength), + AggregateStats(apmMultipliedByWeights[1]/totalLength, ppsMultipliedByWeights[1]/totalLength, vsMultipliedByWeights[1]/totalLength, sppMultipliedByWeights[1]/totalLength, kppMultipliedByWeights[1]/totalLength, kpsMultipliedByWeights[1]/totalLength) ]; } diff --git a/lib/main.dart b/lib/main.dart index 8306737..1345932 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,7 +32,13 @@ ThemeData theme = ThemeData( surface: Color.fromARGB(255, 10, 10, 10), secondary: Colors.white, ), - cardTheme: CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), + cardTheme: const CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), + drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), + searchBarTheme: const SearchBarThemeData( + shadowColor: WidgetStatePropertyAll(Colors.black), + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(12.0), right: Radius.circular(12.0)))), + elevation: WidgetStatePropertyAll(8.0) + ), scaffoldBackgroundColor: Colors.black ); diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 80318f0..3f23eff 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; diff --git a/lib/utils/relative_timestamps.dart b/lib/utils/relative_timestamps.dart index 7425441..5176221 100644 --- a/lib/utils/relative_timestamps.dart +++ b/lib/utils/relative_timestamps.dart @@ -1,3 +1,5 @@ +// ignore_for_file: curly_braces_in_flow_control_structures + import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; @@ -87,7 +89,8 @@ String countdown(Duration difference){ } 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"; + 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 d195de7..c74a52b 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -11,8 +11,6 @@ import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show prefs, teto; @@ -25,8 +23,6 @@ import 'package:tetra_stats/views/singleplayer_record_view.dart'; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; import 'package:tetra_stats/views/zenith_record_view.dart'; import 'package:tetra_stats/widgets/finesse_thingy.dart'; -import 'package:tetra_stats/widgets/gauget_num.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/recent_sp_games.dart'; @@ -501,7 +497,7 @@ class _MainState extends State with TickerProviderStateMixin { Container( width: MediaQuery.of(context).size.width-450, constraints: const BoxConstraints(maxWidth: 1024), - child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX) + child: SingleChildScrollView(child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)) ), SizedBox( width: 450.0, @@ -531,7 +527,7 @@ class _MainState extends State with TickerProviderStateMixin { ), _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![1].league.gamesPlayed > 0), - ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX), + SingleChildScrollView(child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)), _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true), SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")), SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "Blitz")), @@ -1153,8 +1149,8 @@ class _TwoRecordsThingy extends StatelessWidget { 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!))), - if (sprint!.rank != null) const TextSpan(text: " • "), + TextSpan(text: "№${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank))), + const TextSpan(text: " • "), TextSpan(text: timestamp(sprint!.timestamp)), ] ), @@ -1237,8 +1233,8 @@ class _TwoRecordsThingy extends StatelessWidget { color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent )), TextSpan(text: timestamp(blitz!.timestamp)), - if (blitz!.rank != null) const TextSpan(text: " • "), - if (blitz!.rank != null) TextSpan(text: "№${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank!))), + const TextSpan(text: " • "), + TextSpan(text: "№${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank))), ] ), ), @@ -1554,10 +1550,10 @@ class _OtherThingy extends StatelessWidget { Text("${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), Text("${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: const TextStyle(fontSize: 18)), Container( - constraints: BoxConstraints(maxWidth: 300.0), + constraints: const BoxConstraints(maxWidth: 300.0), child: Row(children: [ - Text("Score requirement to level up:"), - Spacer(), + const Text("Score requirement to level up:"), + const Spacer(), Text(intf.format(zen!.scoreRequirement)) ],), ) diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index f6e0dd9..dc56884 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -9,12 +9,9 @@ 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 { @@ -45,7 +42,7 @@ TetrioPlayer testPlayer = TetrioPlayer( registrationTime: DateTime(2002, 2, 25, 9, 30, 01), avatarRevision: 1704835194288, bannerRevision: 1661462402700, - role: "sysop", + role: "user", country: "BY", state: DateTime(1970), badges: [ @@ -63,13 +60,13 @@ TetrioPlayer testPlayer = TetrioPlayer( friendCount: 69, gamesPlayed: 13747, gamesWon: 6523, - gameTime: Duration(days: 79, minutes: 28, seconds: 23, microseconds: 637591), + gameTime: const 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"), + //distinguishment: Distinguishment(type: "twc", detail: "2023"), bio: "кровбер не в палку, без последнего тспина - 32 атаки. кровбер не в палку, без первого тсм и последнего тспина - 30 атаки. кровбер в палку с б2б - 38 атаки.(5 б2б)(не знаю от чего зависит) кровбер в палку с б2б - 36 атаки.(5 б2б)(не знаю от чего зависит)" ); News testNews = News("6098518e3d5155e6ec429cdc", [ @@ -82,6 +79,7 @@ late ScrollController controller; class _MainState extends State with TickerProviderStateMixin { @override void initState() { + teto.open(); controller = ScrollController(); super.initState(); } @@ -95,6 +93,7 @@ class _MainState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( + drawer: const SearchDrawer(), body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Row( @@ -103,7 +102,7 @@ class _MainState extends State with TickerProviderStateMixin { leading: FloatingActionButton( elevation: 0, onPressed: () { - // Add your onPressed code here! + Scaffold.of(context).openDrawer(); }, child: const Icon(Icons.search), ), @@ -154,8 +153,8 @@ class _MainState extends State with TickerProviderStateMixin { 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))))))) + Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.person_add), label: Text(t.track), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(12.0), right: Radius.zero)))))), + Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.balance), label: Text(t.compare), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.zero, right: Radius.circular(12.0))))))) ], ), ), @@ -166,8 +165,8 @@ class _MainState extends State with TickerProviderStateMixin { padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), child: Row( children: [ - Text("Badges", style: TextStyle(fontFamily: "Eurostile Round Extended")), - Spacer(), + const Text("Badges", style: TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer(), Text(intf.format(testPlayer.badges.length)) ], ), @@ -241,9 +240,9 @@ class _MainState extends State with TickerProviderStateMixin { children: [ Row( children: [ - Spacer(), - Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended")), - Spacer() + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() ], ), Padding( @@ -267,13 +266,13 @@ class _MainState extends State with TickerProviderStateMixin { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Spacer(), - Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - Spacer() + const Spacer(), + Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() ], ), ), - Card(), + const Card(), SegmentedButton( segments: const >[ ButtonSegment( @@ -305,7 +304,7 @@ class _MainState extends State with TickerProviderStateMixin { // label: Text('Other'), // icon: Icon(Icons.calendar_today)), ], - selected: {Cards.tetraLeague}, + selected: const {Cards.tetraLeague}, onSelectionChanged: (Set newSelection) { setState(() { // By default there is only a single segment that can be @@ -328,7 +327,7 @@ class _MainState extends State with TickerProviderStateMixin { class NewsThingy extends StatelessWidget{ final News news; - NewsThingy(this.news); + const NewsThingy(this.news, {super.key}); ListTile getNewsTile(NewsEntry news){ Map gametypes = { @@ -480,9 +479,9 @@ class NewsThingy extends StatelessWidget{ children: [ Row( children: [ - Spacer(), - Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended")), - Spacer() + const Spacer(), + Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() ] ), for (NewsEntry entry in news.news) getNewsTile(entry) @@ -497,7 +496,7 @@ class NewsThingy extends StatelessWidget{ class DistinguishmentThingy extends StatelessWidget{ final Distinguishment distinguishment; - DistinguishmentThingy(this.distinguishment, {super.key}); + const DistinguishmentThingy(this.distinguishment, {super.key}); List getDistinguishmentTitle(String? text) { // TWC champions don't have header in their distinguishments @@ -544,23 +543,23 @@ class DistinguishmentThingy extends StatelessWidget{ 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); + case "founder": return const Color(0xAAFD82D4); + case "kagarin": return const Color(0xAAFF0060); + case "team": return const Color(0xAAFACC2E); + case "team-minor": return const Color(0xAAF5BD45); + case "administrator": return const Color(0xAAFF4E8A); + case "globalmod": return const Color(0xAAE878FF); + case "communitymod": return const Color(0xAA4E68FB); + case "alumni": return const Color(0xAA6057DB); default: return theme.colorScheme.surface; } case "champion": switch (detail){ case "blitz": - case "40l": return Color(0xAACCF5F6); - case "league": return Color(0xAAFFDB31); + case "40l": return const Color(0xAACCF5F6); + case "league": return const Color(0xAAFFDB31); } - case "twc": return Color(0xAAFFDB31); + case "twc": return const Color(0xAAFFDB31); default: return theme.colorScheme.surface; } return theme.colorScheme.surface; @@ -574,9 +573,9 @@ class DistinguishmentThingy extends StatelessWidget{ children: [ Row( children: [ - Spacer(), - Text(t.distinguishment, style: TextStyle(fontFamily: "Eurostile Round Extended")), - Spacer() + const Spacer(), + Text(t.distinguishment, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() ], ), RichText( @@ -603,17 +602,17 @@ class NewUserThingy extends StatelessWidget { Color roleColor(String role){ switch (role){ case "sysop": - return Color.fromARGB(255, 23, 165, 133); + return const Color.fromARGB(255, 23, 165, 133); case "admin": - return Color.fromARGB(255, 255, 78, 138); + return const Color.fromARGB(255, 255, 78, 138); case "mod": - return Color.fromARGB(255, 204, 128, 242); + return const Color.fromARGB(255, 204, 128, 242); case "halfmod": - return Color.fromARGB(255, 95, 118, 254); + return const Color.fromARGB(255, 95, 118, 254); case "bot": - return Color.fromARGB(255, 60, 93, 55); + return const Color.fromARGB(255, 60, 93, 55); case "banned": - return Color.fromARGB(255, 248, 28, 28); + return const Color.fromARGB(255, 248, 28, 28); default: return Colors.white10; } @@ -623,7 +622,7 @@ class NewUserThingy extends StatelessWidget { Widget build(BuildContext context) { final t = Translations.of(context); return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; + //bool bigScreen = constraints.maxWidth > 768; double pfpHeight = 128; int xpTableID = 0; @@ -638,7 +637,7 @@ class NewUserThingy extends StatelessWidget { child: Column( children: [ Container( - constraints: BoxConstraints(maxWidth: 960), + constraints: const BoxConstraints(maxWidth: 960), height: player.bannerRevision != null ? 218.0 : 138.0, child: Stack( //clipBehavior: Clip.none, @@ -673,7 +672,7 @@ class NewUserThingy extends StatelessWidget { child: Text(player.username, //softWrap: true, overflow: TextOverflow.fade, - style: TextStyle( + style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 28, ) @@ -686,14 +685,14 @@ class NewUserThingy extends StatelessWidget { 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))), + child: Chip(label: Text(player.role.toUpperCase(), style: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(player.role))), ), RichText( text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round"), + style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - if (player.friendCount > 0) WidgetSpan(child: Icon(Icons.person), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (player.friendCount > 0) const 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)), @@ -708,10 +707,10 @@ class NewUserThingy extends StatelessWidget { left: 160.0, child: RichText( text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round"), + style: const 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)) + TextSpan(text: player.registrationTime == null ? t.wasFromBeginning : timestamp(player.registrationTime!), style: const TextStyle(color: Colors.grey)) ] ) ) @@ -722,7 +721,7 @@ class NewUserThingy extends StatelessWidget { child: RichText( textAlign: TextAlign.end, text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round"), + style: const TextStyle(fontFamily: "Eurostile Round"), children: [ TextSpan(text: "Level ${intf.format(player.level.floor())}", recognizer: TapGestureRecognizer()..onTap = (){ showDialog( @@ -756,14 +755,14 @@ class NewUserThingy extends StatelessWidget { ), actions: [ TextButton( - child: Text("OK"), + child: const Text("OK"), onPressed: () {Navigator.of(context).pop();} ) ] ) ); }), - TextSpan(text:"\n"), + const 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, @@ -781,16 +780,16 @@ class NewUserThingy extends StatelessWidget { ), actions: [ TextButton( - child: Text("OK"), + child: const 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)), + const 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: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), ] ) ) @@ -812,4 +811,62 @@ class NewUserThingy extends StatelessWidget { ); }); } +} + +class SearchDrawer extends StatefulWidget{ + const SearchDrawer({super.key}); + + @override + State createState() => _SearchDrawerState(); +} + +class _SearchDrawerState extends State { + @override + Widget build(BuildContext context) { + return Drawer( + child: StreamBuilder( + stream: teto.allPlayers, + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.done: + case ConnectionState.active: + final allPlayers = (snapshot.data != null) + ? snapshot.data as Map + : {}; + allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted + List keys = allPlayers.keys.toList(); + return NestedScrollView( + headerSliverBuilder: (BuildContext context, bool value){ + return [ + SliverToBoxAdapter( + child: SearchBar( + hintText: "Hello", + hintStyle: const WidgetStatePropertyAll(TextStyle(color: Colors.grey)), + trailing: [ + IconButton(onPressed: (){print("sas");}, icon: const Icon(Icons.search)) + ], + ), + ) + ]; + }, + body: ListView.builder( // Builds list of tracked players. + itemCount: allPlayers.length, + itemBuilder: (context, index) { + var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. + return ListTile( + title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states + onTap: () { + //widget.changePlayer(keys[i]); // changes to chosen player + Navigator.of(context).pop(); // and closes itself. + }, + ); + }) + ); + } + } + ) + ); + } } \ No newline at end of file diff --git a/lib/views/mathes_view.dart b/lib/views/mathes_view.dart index 270ff8d..fa709c8 100644 --- a/lib/views/mathes_view.dart +++ b/lib/views/mathes_view.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/views/tl_match_view.dart'; import 'package:window_manager/window_manager.dart'; late String oldWindowTitle; diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 1e8e8f3..18f3d2a 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -41,7 +41,7 @@ class TlMatchResultState extends State { late Duration time; late String readableTime; late String reason; - Duration totalTime = Duration(); + Duration totalTime = const Duration(); List roundLengths = []; List timeWeightedStats = []; late bool initPlayerWon; @@ -53,9 +53,9 @@ class TlMatchResultState extends State { if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch))); greenSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id == widget.initPlayerId); redSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id != widget.initPlayerId); - List APMmultipliedByWeights = [0, 0]; - List PPSmultipliedByWeights = [0, 0]; - List VSmultipliedByWeights = [0, 0]; + List apmMultipliedByWeights = [0, 0]; + List ppsMultipliedByWeights= [0, 0]; + List vsMultipliedByWeights = [0, 0]; for (var round in widget.record.results.rounds){ var longerLifetime = round[0].lifetime.compareTo(round[1].lifetime) == 1 ? round[0].lifetime : round[1].lifetime; roundLengths.add(longerLifetime); @@ -64,18 +64,18 @@ class TlMatchResultState extends State { BetaLeagueRound greenSide = round.firstWhere((element) => element.id == widget.initPlayerId); BetaLeagueRound redSide = round.firstWhere((element) => element.id != widget.initPlayerId); - APMmultipliedByWeights[0] += greenSide.stats.apm*longerLifetime.inMilliseconds; - APMmultipliedByWeights[1] += redSide.stats.apm*longerLifetime.inMilliseconds; - PPSmultipliedByWeights[0] += greenSide.stats.pps*longerLifetime.inMilliseconds; - PPSmultipliedByWeights[1] += redSide.stats.pps*longerLifetime.inMilliseconds; - VSmultipliedByWeights[0] += greenSide.stats.vs*longerLifetime.inMilliseconds; - VSmultipliedByWeights[1] += redSide.stats.vs*longerLifetime.inMilliseconds; + apmMultipliedByWeights[0] += greenSide.stats.apm*longerLifetime.inMilliseconds; + apmMultipliedByWeights[1] += redSide.stats.apm*longerLifetime.inMilliseconds; + ppsMultipliedByWeights[0] += greenSide.stats.pps*longerLifetime.inMilliseconds; + ppsMultipliedByWeights[1] += redSide.stats.pps*longerLifetime.inMilliseconds; + vsMultipliedByWeights[0] += greenSide.stats.vs*longerLifetime.inMilliseconds; + vsMultipliedByWeights[1] += redSide.stats.vs*longerLifetime.inMilliseconds; } timeWeightedStats = [ BetaLeagueStats( - apm: APMmultipliedByWeights[0]/totalTime.inMilliseconds, - pps: PPSmultipliedByWeights[0]/totalTime.inMilliseconds, - vs: VSmultipliedByWeights[0]/totalTime.inMilliseconds, + apm: apmMultipliedByWeights[0]/totalTime.inMilliseconds, + pps: ppsMultipliedByWeights[0]/totalTime.inMilliseconds, + vs: vsMultipliedByWeights[0]/totalTime.inMilliseconds, garbageSent: widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent, garbageReceived: widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived, kills: widget.record.results.leaderboard[greenSidePlayer].stats.kills, @@ -83,9 +83,9 @@ class TlMatchResultState extends State { rank: widget.record.results.leaderboard[greenSidePlayer].stats.rank ), BetaLeagueStats( - apm: APMmultipliedByWeights[1]/totalTime.inMilliseconds, - pps: PPSmultipliedByWeights[1]/totalTime.inMilliseconds, - vs: VSmultipliedByWeights[1]/totalTime.inMilliseconds, + apm: apmMultipliedByWeights[1]/totalTime.inMilliseconds, + pps: ppsMultipliedByWeights[1]/totalTime.inMilliseconds, + vs: vsMultipliedByWeights[1]/totalTime.inMilliseconds, garbageSent: widget.record.results.leaderboard[redSidePlayer].stats.garbageSent, garbageReceived: widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived, kills: widget.record.results.leaderboard[redSidePlayer].stats.kills, @@ -480,12 +480,12 @@ class TlMatchResultState extends State { OverflowBar( alignment: MainAxisAlignment.spaceEvenly, children: [ - TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, + TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null, onPressed: () { roundSelector = -1; setState(() {}); }, child: Text(t.matchStats)), - TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, + TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null, onPressed: timeWeightedStatsAvaliable ? () { roundSelector = -2; setState(() {}); diff --git a/lib/views/zenith_record_view.dart b/lib/views/zenith_record_view.dart index 3b61276..b9f5c29 100644 --- a/lib/views/zenith_record_view.dart +++ b/lib/views/zenith_record_view.dart @@ -18,26 +18,18 @@ class ZenithRecordView extends StatelessWidget { appBar: AppBar( title: Text("${ switch (record.gamemode){ - "zenith" => "Quick Play", - "zenithex" => "Quick Play Expert", + "zenith" => t.quickPlay, + "zenithex" => "${t.quickPlay} ${t.expert}", String() => "5000000 Blast", } } ${timestamp(record.timestamp)}"), ), body: SafeArea( - child: SingleChildScrollView( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - ZenithThingy(record: record, switchable: false), - // TODO: Insert replay link here - ] - ) - ], - ) + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: SingleChildScrollView( + child: ZenithThingy(record: record, switchable: false), + ), ) ), ); diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index e8f3ebd..194496d 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unused_field, unused_local_variable, invalid_use_of_visible_for_testing_member, implementation_imports, overridden_fields + import 'dart:math'; import 'package:fl_chart/fl_chart.dart'; @@ -196,7 +198,7 @@ class MyRadarChartPainter extends RadarChartPainter{ } class MyRadarChartLeaf extends RadarChartLeaf{ - MyRadarChartLeaf({required super.data, required super.targetData}); + const MyRadarChartLeaf({super.key, required super.data, required super.targetData}); @override RenderRadarChart createRenderObject(BuildContext context) => MyRenderRadarChart( diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 6a9586e..76edf64 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -106,7 +106,7 @@ class StatCellNum extends StatelessWidget { ); }, style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero)), + padding: WidgetStateProperty.all(EdgeInsets.zero)), child: Text( playerStatLabel, textAlign: TextAlign.center, diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index e023ade..22682b0 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -26,7 +25,7 @@ class TLRatingThingy extends StatelessWidget{ List formatedTR = f4.format(tlData.rating).split(decimalSeparator); List formatedGlicko = f4.format(tlData.glicko).split(decimalSeparator); List formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); - DateTime now = DateTime.now(); + //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()); diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index d554ca5..bd04e7f 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -2,7 +2,6 @@ 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'; diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index 14dc7fb..9dbcac5 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -18,7 +18,7 @@ class ZenithThingy extends StatefulWidget{ final RecordSingle? recordEX; final Function? parentZenithToggle; - ZenithThingy({this.record, this.recordEX, this.switchable = true, this.parentZenithToggle, this.initEXvalue = false}); + const ZenithThingy({super.key, this.record, this.recordEX, this.switchable = true, this.parentZenithToggle, this.initEXvalue = false}); @override State createState() => _ZenithThingyState(); @@ -71,188 +71,189 @@ class _ZenithThingyState extends State { ), ); } - return SingleChildScrollView( - child: Padding(padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: "${f2.format(record!.stats.zenith!.altitude)} m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), - ), + return Padding(padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), + RichText(text: TextSpan( + text: "${f2.format(record!.stats.zenith!.altitude)} m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), ), - if ((record!.extras as ZenithExtras).mods.isNotEmpty) RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.white), - children: [ - TextSpan(text: "${t.withMods}: "), - for (String mod in (record!.extras as ZenithExtras).mods) TextSpan(text: "${mod.toUpperCase()} "), - ] - ), - ), - RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (record!.rank != -1) TextSpan(text: "№${record!.rank}"), - if (record!.rank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"), - if (record!.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(widget.record!.timestamp)), - ] - ), - ), - if (widget.switchable) TextButton(onPressed: (){ - if (ex){ - ex = false; - }else{ - ex = true; - } - setState(() { - if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); - record = ex ? widget.recordEX : widget.record; - }); - }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), - Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, + ), + if ((record!.extras as ZenithExtras).mods.isNotEmpty) RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.white), children: [ - StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "CPS\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) - ], + TextSpan(text: "${t.withMods}: "), + for (String mod in (record!.extras as ZenithExtras).mods) TextSpan(text: "${mod.toUpperCase()} "), + ] ), - FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), - LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, + ), + RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (record!.rank != -1) TextSpan(text: "№${record!.rank}"), + if (record!.rank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"), + if (record!.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(widget.record!.timestamp)), + ] + ), + ), + if (widget.switchable) TextButton(onPressed: (){ + if (ex){ + ex = false; + }else{ + ex = true; + } + setState(() { + if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); + record = ex ? widget.recordEX : widget.record; + }); + }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), + Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 20, + children: [ + StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), + StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), + StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), + StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true), + StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "CPS\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) + ], + ), + FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), + LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + Table( + columnWidths: const { + 0: FixedColumnWidth(36) + }, + children: [ + const TableRow( + children: [ + Text("Floor"), + Text("Split", textAlign: TextAlign.right), + Text("Total", textAlign: TextAlign.right), + ] + ), + for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( + children: [ + Text((i+1).toString()), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), + ] + ) + ], + ), + ], + ), + ), + ), + Column( + children: [ + Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Padding( + padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 35, + crossAxisAlignment: WrapCrossAlignment.start, + //clipBehavior: Clip.hardEdge, children: [ - Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), - Table( - children: [ - TableRow( - children: [ - Text("Floor"), - Text("Split"), - Text("Total"), - ] - ), - for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( - children: [ - Text((i+1).toString()), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---"), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---"), - ] - ) - ], - ), - ], - ), + GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ + GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), + GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), + GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), + GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), + GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.appDescription), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}") + ]), + GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ + GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), + GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), + GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), + ], alertWidgets: [ + Text(t.statCellNum.vsapmDescription), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}") + ]) + ]), ), - ), - Column( - children: [ - Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Wrap( + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Wrap( direction: Axis.horizontal, alignment: WrapAlignment.center, - spacing: 35, + spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, + //clipBehavior: Clip.hardEdge, children: [ - GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}") - ]), - GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}") - ]) - ]), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true,), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: false), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true) - ]), - ) - ], - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), - ) - ], - ) - ), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, + alertWidgets: [Text(t.statCellNum.dssDescription), + Text("${t.formula}: (VS / 100) - (APM / 60)"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),], + okText: t.popupActions.ok, + higherIsBetter: true,), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, + alertWidgets: [Text(t.statCellNum.dspDescription), + Text("${t.formula}: DS/S / PPS"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, + alertWidgets: [Text(t.statCellNum.appdspDescription), + Text("${t.formula}: APP + DS/P"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, + alertWidgets: [Text(t.statCellNum.cheeseDescription), + Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),], + okText: t.popupActions.ok, + higherIsBetter: false), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, + alertWidgets: [Text(t.statCellNum.gbeDescription), + Text("${t.formula}: APP * DS/P * 2"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, + alertWidgets: [Text(t.statCellNum.nyaappDescription), + Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")], + okText: t.popupActions.ok, + higherIsBetter: true), + StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, + alertWidgets: [Text(t.statCellNum.areaDescription), + Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), + Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),], + okText: t.popupActions.ok, + higherIsBetter: true) + ]), + ) + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), + ) + ], + ) ); }); } diff --git a/pubspec.lock b/pubspec.lock index b4e8059..e36950e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: coverage - sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + sha256: "576aaab8b1abdd452e0f656c3e73da9ead9d7880e15bdc494189d9c1a1baf0db" url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.9.0" cross_file: dependency: transitive description: @@ -133,18 +133,18 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dev_build: dependency: transitive description: name: dev_build - sha256: e5d575f3de4b0e5f004e065e1e2d98fa012d634b61b5855216b5698ed7f1e443 + sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e url: "https://pub.dev" source: hosted - version: "0.16.4+3" + version: "1.0.0+11" equatable: dependency: transitive description: @@ -197,18 +197,18 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "1cd66575f063b689e041aec836905ba7be18d76c9f0634d0d75daec825f67095" + sha256: d1e8655c1a4850a900a0cfaed55fdd273881d53a4bb78e4736dc170a0b17db78 url: "https://pub.dev" source: hosted - version: "0.5.0+7" + version: "0.5.1+5" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: b015154e6d9fddbc4d08916794df170b44531798c8dd709a026df162d07ad81d + sha256: "38ebf91ecbcfa89a9639a0854ccaed8ab370c75678938eebca7d34184296f0bb" url: "https://pub.dev" source: hosted - version: "0.5.1+8" + version: "0.5.3" file_selector_linux: dependency: transitive description: @@ -221,10 +221,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.4" file_selector_platform_interface: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" fl_chart: dependency: "direct main" description: @@ -266,10 +266,10 @@ packages: dependency: "direct main" description: name: flutter_colorpicker - sha256: "458a6ed8ea480eb16ff892aedb4b7092b2804affd7e046591fb03127e8d8ef8b" + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -282,10 +282,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_localizations: dependency: "direct main" description: flutter @@ -295,18 +295,18 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "87e11b9df25a42e2db315b8b7a51fae8e66f57a4b2f50ec4b822d0fa155e6b52" + sha256: "04c4722cc36ec5af38acc38ece70d22d3c2123c61305d555750a091517bbe504" url: "https://pub.dev" source: hosted - version: "0.6.22" + version: "0.6.23" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.21" flutter_svg: dependency: "direct main" description: @@ -329,10 +329,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -345,10 +345,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" + sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 url: "https://pub.dev" source: hosted - version: "13.2.1" + version: "13.2.5" http: dependency: "direct main" description: @@ -377,18 +377,18 @@ packages: dependency: transitive description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" intl: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -401,10 +401,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json2yaml: dependency: transitive description: @@ -417,34 +417,34 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -489,10 +489,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -553,26 +553,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.9" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -593,10 +593,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -609,10 +609,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -621,14 +621,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -641,10 +633,10 @@ packages: dependency: transitive description: name: process_run - sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e" + sha256: c917dfb5f7afad4c7485bc00a4df038621248fce046105020cea276d1a87c820 url: "https://pub.dev" source: hosted - version: "0.14.2" + version: "1.1.0" pub_semver: dependency: transitive description: @@ -665,42 +657,42 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: @@ -713,10 +705,10 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shelf: dependency: transitive description: @@ -758,18 +750,18 @@ packages: dependency: "direct main" description: name: slang - sha256: "5e08ac915ac27a3508863f37734280d30c3713d56746cd2e4a5da77413da4b95" + sha256: f68f6d6709890f85efabfb0318e9d694be2ebdd333e57fe5cb50eee449e4e3ab url: "https://pub.dev" source: hosted - version: "3.30.1" + version: "3.31.1" slang_flutter: dependency: "direct main" description: name: slang_flutter - sha256: "9ee040b0d364d3a4d692e4af536acff6ef513870689403494ebc6d59b0dccea6" + sha256: f8400292be49c11697d94af58d7f7d054c91af759f41ffe71e4e5413871ffc62 url: "https://pub.dev" source: hosted - version: "3.30.0" + version: "3.31.0" source_map_stack_trace: dependency: transitive description: @@ -798,10 +790,10 @@ packages: dependency: "direct main" description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3+1" sqflite_common: dependency: transitive description: @@ -830,18 +822,18 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" + sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" url: "https://pub.dev" source: hosted - version: "0.5.20" + version: "0.5.24" stack_trace: dependency: transitive description: @@ -910,26 +902,26 @@ packages: dependency: "direct dev" description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" typed_data: dependency: transitive description: @@ -942,26 +934,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.8" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -974,10 +966,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: @@ -998,10 +990,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" vector_graphics: dependency: transitive description: @@ -1038,10 +1030,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -1078,18 +1070,18 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.5.3" window_manager: dependency: "direct main" description: name: window_manager - sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" url: "https://pub.dev" source: hosted - version: "0.3.8" + version: "0.3.9" xdg_directories: dependency: transitive description: @@ -1115,5 +1107,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 023ea82..260bb80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: fl_chart: ^0.66.0 package_info_plus: ^5.0.1 shared_preferences: ^2.1.1 - intl: ^0.18.0 + intl: ^0.19.0 syncfusion_flutter_gauges: ^24.1.41 file_selector: ^1.0.1 file_picker: ^6.1.1 From 3545e67e4a6ab401edd2f1d0bf98fa47a90623b9 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 3 Aug 2024 21:05:04 +0300 Subject: [PATCH 09/33] workflow fix --- .github/workflows/main.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b468862..d395977 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: 'stable' - flutter-version: '3.16.5' + flutter-version: '3.22.3' - name: Install project dependencies run: flutter pub get - name: Build artifacts @@ -49,9 +49,11 @@ jobs: - uses: ashutoshvarma/setup-ninja@master with: channel: 'stable' - flutter-version: '3.16.5' + flutter-version: '3.22.3' - name: Install project dependencies - run: flutter pub get + run: | + flutter pub get + sudo apt-get install -y ninja-build libgtk-3-dev - name: Build artifacts run: flutter build linux --release - name: Archive Release From acaf08d8a2686875c133e2425e7159bc26b88383 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 3 Aug 2024 21:16:41 +0300 Subject: [PATCH 10/33] another workflow fix --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d395977..14bebd5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,7 @@ jobs: with: type: 'zip' filename: TetraStats-${{github.ref_name}}-linux.zip - directory: build/linux/x64/runner/Release/bundle + directory: build/linux/x64/runner/release/bundle - name: Push to Releases uses: ncipollo/release-action@v1 with: From d3b9c6de4aa39905d4fbc7235f673a47347ca8da Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 3 Aug 2024 21:25:14 +0300 Subject: [PATCH 11/33] =?UTF-8?q?=D1=8F=D0=BF=D0=BE=D0=BD=D0=B0=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14bebd5..889b0b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,7 @@ jobs: with: type: 'zip' filename: TetraStats-${{github.ref_name}}-linux.zip - directory: build/linux/x64/runner/release/bundle + directory: build/linux/x64/release/bundle - name: Push to Releases uses: ncipollo/release-action@v1 with: From 4533faf52e619112b4298af0114c8050e990efa6 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 5 Aug 2024 01:23:08 +0300 Subject: [PATCH 12/33] new design progress --- android/app/build.gradle | 166 ++++++------- lib/views/main_view_tiles.dart | 422 ++++++++++++++++++++++----------- 2 files changed, 364 insertions(+), 224 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ebd2bb0..53aeec3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,83 +1,83 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -def keystoreProperties = new Properties() - def keystorePropertiesFile = rootProject.file('key.properties') - if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - } - -android { - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.dan63.tetra_stats" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 19 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - signingConfigs { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null - storePassword keystoreProperties['storePassword'] - } - } - buildTypes { - release { - signingConfig signingConfigs.release - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +def keystoreProperties = new Properties() + def keystorePropertiesFile = rootProject.file('key.properties') + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + } + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.dan63.tetra_stats" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index dc56884..c5e4fba 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -12,6 +12,7 @@ import 'package:tetra_stats/utils/text_shadow.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_rating_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; class MainView extends StatefulWidget { @@ -65,7 +66,23 @@ TetrioPlayer testPlayer = TetrioPlayer( 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), + tlSeason1: TetraLeagueAlpha( + timestamp: DateTime(1970), + gamesPlayed: 28, + gamesWon: 14, + bestRank: "x", + decaying: false, + rating: 23500.6194, + glicko: 3847.2134, + rd: 61.95383, + 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б)(не знаю от чего зависит)" ); @@ -77,6 +94,9 @@ News testNews = News("6098518e3d5155e6ec429cdc", [ late ScrollController controller; class _MainState extends State with TickerProviderStateMixin { + String _searchFor = "6098518e3d5155e6ec429cdc"; + final TextEditingController _searchController = TextEditingController(); + @override void initState() { teto.open(); @@ -84,16 +104,23 @@ class _MainState extends State with TickerProviderStateMixin { super.initState(); } + void changePlayer(String player) { + setState(() { + _searchFor = player; + }); + } + @override void dispose() { controller.dispose(); + _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - drawer: const SearchDrawer(), + drawer: SearchDrawer(changePlayer: changePlayer, controller: _searchController), body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Row( @@ -145,118 +172,80 @@ class _MainState extends State with TickerProviderStateMixin { children: [ 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: const Icon(Icons.person_add), label: Text(t.track), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(12.0), right: Radius.zero)))))), - Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.balance), label: Text(t.compare), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.zero, right: Radius.circular(12.0))))))) - ], - ), - ), - Card( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), - child: Row( - children: [ - const Text("Badges", style: TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer(), - Text(intf.format(testPlayer.badges.length)) - ], + child: FutureBuilder(future: teto.fetchPlayer(_searchFor), builder:(context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + NewUserThingy(player: snapshot.data!, showStateTimestamp: false, setState: setState), + if (snapshot.data!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.badges), + if (snapshot.data!.distinguishment != null) DistinguishmentThingy(snapshot.data!.distinguishment!), + if (snapshot.data!.bio != null) Card( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MarkdownBody(data: snapshot.data!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], + ), ), - ), - 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( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() + //if (testNews != null && testNews!.news.isNotEmpty) + Expanded( + child: FutureBuilder( + future: teto.fetchNews(_searchFor), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return Card(child: Center(child: CircularProgressIndicator())); + case ConnectionState.done: + if (snapshot.hasData){ + return NewsThingy(snapshot.data!); + }else if (snapshot.hasError){ + return Card(child: Column(children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Text(snapshot.stackTrace.toString()) + ] + )); + } + } + return Text("what?"); + } + ), + ) + ], + ); + }else{ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), + ), ], - ), - 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: constraints.maxWidth - 450 - 80, child: Column( @@ -272,7 +261,8 @@ class _MainState extends State with TickerProviderStateMixin { ], ), ), - const Card(), + TetraLeagueThingy(league: testPlayer.tlSeason1!), + //const Card(), SegmentedButton( segments: const >[ ButtonSegment( @@ -333,7 +323,9 @@ class NewsThingy 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 @@ -362,7 +354,16 @@ class NewsThingy 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.", + "zenithex" => "${f2.format(news.data["result"])} m.", + _ => "unknown" + }, + style: const TextStyle(fontWeight: FontWeight.bold) + ), ] ) ), @@ -473,7 +474,6 @@ class NewsThingy extends StatelessWidget{ @override Widget build(BuildContext context) { return Card( - surfaceTintColor: theme.colorScheme.surface, child: SingleChildScrollView( child: Column( children: [ @@ -484,7 +484,8 @@ class NewsThingy extends StatelessWidget{ const Spacer() ] ), - for (NewsEntry entry in news.news) getNewsTile(entry) + if (news.news.isEmpty) Center(child: Text("Empty list")) + else for (NewsEntry entry in news.news) getNewsTile(entry) ], ), ), @@ -592,6 +593,92 @@ class DistinguishmentThingy extends StatelessWidget{ } } +class BadgesThingy extends StatelessWidget{ + final List badges; + + const BadgesThingy({super.key, required this.badges}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), + child: Row( + children: [ + const Text("Badges", style: TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer(), + Text(intf.format(badges.length)) + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var badge in 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); + } + ); + }, + ) + ) + ], + ), + ) + ], + ), + ); + } +} + class NewUserThingy extends StatelessWidget { final TetrioPlayer player; final bool showStateTimestamp; @@ -618,6 +705,12 @@ class NewUserThingy extends StatelessWidget { } } + String fontStyle(int length){ + if (length < 10) return "Eurostile Round Extended"; + else if (length < 13) return "Eurostile Round"; + else return "Eurostile Round Condensed"; + } + @override Widget build(BuildContext context) { final t = Translations.of(context); @@ -632,11 +725,11 @@ class NewUserThingy extends StatelessWidget { return Card( clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Container( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Container( constraints: const BoxConstraints(maxWidth: 960), height: player.bannerRevision != null ? 218.0 : 138.0, child: Stack( @@ -645,7 +738,6 @@ class NewUserThingy extends StatelessWidget { 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(); }, @@ -672,8 +764,8 @@ class NewUserThingy extends StatelessWidget { child: Text(player.username, //softWrap: true, overflow: TextOverflow.fade, - style: const TextStyle( - fontFamily: "Eurostile Round Extended", + style: TextStyle( + fontFamily: fontStyle(player.username.length), fontSize: 28, ) ), @@ -763,7 +855,7 @@ class NewUserThingy extends StatelessWidget { ); }), const 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 = (){ + TextSpan(text: player.gameTime.isNegative ? "-h --m" : playtime(player.gameTime), style: TextStyle(color: player.gameTime.isNegative ? Colors.grey : Colors.white), recognizer: !player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( @@ -772,7 +864,7 @@ class NewUserThingy extends StatelessWidget { 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", + "${intf.format(player.gameTime.inDays)}d ${nonsecs.format(player.gameTime.inHours%24)}h ${nonsecs.format(player.gameTime.inMinutes%60)}m ${nonsecs.format(player.gameTime.inSeconds%60)}s ${nonsecs3.format(player.gameTime.inMilliseconds%1000)}ms ${nonsecs3.format(player.gameTime.inMicroseconds%1000)}μs", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24) ), ] @@ -786,7 +878,7 @@ class NewUserThingy extends StatelessWidget { ] ) ); - }), + }) : null), const 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: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), @@ -795,18 +887,17 @@ class NewUserThingy extends StatelessWidget { ) ) ], - ), + ), ), - // 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)))))) - // ] - // ) - ], - ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.person_add), label: Text(t.track), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0))))))), + Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.balance), label: Text(t.compare), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))))) + ], + ) + ], ), ); }); @@ -814,7 +905,9 @@ class NewUserThingy extends StatelessWidget { } class SearchDrawer extends StatefulWidget{ - const SearchDrawer({super.key}); + final Function changePlayer; + final TextEditingController controller; + const SearchDrawer({super.key, required this.changePlayer, required this.controller}); @override State createState() => _SearchDrawerState(); @@ -842,11 +935,21 @@ class _SearchDrawerState extends State { return [ SliverToBoxAdapter( child: SearchBar( + controller: widget.controller, hintText: "Hello", hintStyle: const WidgetStatePropertyAll(TextStyle(color: Colors.grey)), trailing: [ - IconButton(onPressed: (){print("sas");}, icon: const Icon(Icons.search)) + IconButton(onPressed: (){setState(() { + widget.changePlayer(widget.controller.value.text); + Navigator.of(context).pop(); + });}, icon: const Icon(Icons.search)) ], + onSubmitted: (value) { + setState(() { + widget.changePlayer(value); + Navigator.of(context).pop(); + }); + }, ), ) ]; @@ -858,7 +961,7 @@ class _SearchDrawerState extends State { return ListTile( title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states onTap: () { - //widget.changePlayer(keys[i]); // changes to chosen player + widget.changePlayer(keys[i]); // changes to chosen player Navigator.of(context).pop(); // and closes itself. }, ); @@ -869,4 +972,41 @@ class _SearchDrawerState extends State { ) ); } +} + +class TetraLeagueThingy extends StatelessWidget{ + final TetraLeagueAlpha league; + + const TetraLeagueThingy({super.key, required this.league}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + TLRatingThingy(userID: "w", tlData: league) + // SfRadialGauge( + // axes: [ + // RadialAxis( + // radiusFactor: 0.7, + // showTicks: false, + // showLabels: false, + // annotations: [ + // GaugeAnnotation(widget: Container(height: 196, child: + // Image.asset("res/tetrio_tl_alpha_ranks/${league.rank}.png")), + // angle: 270,positionFactor: 0.05 + // ), + // GaugeAnnotation(widget: Container(child: + // Text('24803.7921 TR',style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + // angle: 90,positionFactor: 0.9 + // ) + // ], + // ) + // ], + // enableLoadingAnimation: true, + // ) + ], + ), + ); + } } \ No newline at end of file From 636c2ae946ffc24fe85f03dc578ffb9a19d64ff0 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 7 Aug 2024 01:24:31 +0300 Subject: [PATCH 13/33] redesign progress --- lib/main.dart | 13 +- lib/views/main_view_tiles.dart | 266 ++++++++++++++++++++++++++----- lib/widgets/tl_progress_bar.dart | 70 ++++---- lib/widgets/tl_thingy.dart | 2 - res/icons/40l.svg | 111 +++++++++++++ res/icons/blitz.svg | 114 +++++++++++++ res/icons/league.svg | 110 +++++++++++++ res/icons/qp.svg | 111 +++++++++++++ 8 files changed, 712 insertions(+), 85 deletions(-) create mode 100644 res/icons/40l.svg create mode 100644 res/icons/blitz.svg create mode 100644 res/icons/league.svg create mode 100644 res/icons/qp.svg diff --git a/lib/main.dart b/lib/main.dart index 1345932..3f81e0f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,7 +30,7 @@ ThemeData theme = ThemeData( colorScheme: const ColorScheme.dark( primary: Colors.cyanAccent, surface: Color.fromARGB(255, 10, 10, 10), - secondary: Colors.white, + secondary: Color(0xFF00838F), ), cardTheme: const CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), @@ -39,6 +39,17 @@ ThemeData theme = ThemeData( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(12.0), right: Radius.circular(12.0)))), elevation: WidgetStatePropertyAll(8.0) ), + chipTheme: ChipThemeData( + side: BorderSide(color: Colors.transparent), + ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide(color: Colors.transparent)), + surfaceTintColor: WidgetStatePropertyAll(Colors.cyanAccent), + iconColor: WidgetStatePropertyAll(Colors.cyanAccent), + shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200), + ) + ), scaffoldBackgroundColor: Colors.black ); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index c5e4fba..cb748e2 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -9,9 +9,13 @@ 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/tl_match_view.dart'; +import 'package:tetra_stats/widgets/list_tile_trailing_stats.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_progress_bar.dart'; import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; @@ -26,7 +30,9 @@ class MainView extends StatefulWidget { State createState() => _MainState(); } +enum Page {home, leaderboards, leagueAverages, calculator, settings} enum Cards {overview, tetraLeague, quickPlay, quickPlayExpert, sprint, blitz, other} +enum CardMod {info, recent} Map cardsTitles = { Cards.overview: "Overview", Cards.tetraLeague: t.tetraLeague, @@ -75,12 +81,15 @@ TetrioPlayer testPlayer = TetrioPlayer( rating: 23500.6194, glicko: 3847.2134, rd: 61.95383, + apm: 62.48, + pps: 1.85, + vs: 134.32, rank: "x", percentileRank: "x", percentile: 0.00, standing: 1, standingLocal: 1, - nextAt: -1, + nextAt: 1, prevAt: 500 ), //distinguishment: Distinguishment(type: "twc", detail: "2023"), @@ -95,6 +104,7 @@ late ScrollController controller; class _MainState extends State with TickerProviderStateMixin { String _searchFor = "6098518e3d5155e6ec429cdc"; + Cards rightCard = Cards.tetraLeague; final TextEditingController _searchController = TextEditingController(); @override @@ -263,48 +273,66 @@ class _MainState extends State with TickerProviderStateMixin { ), TetraLeagueThingy(league: testPlayer.tlSeason1!), //const Card(), + SegmentedButton( + showSelectedIcon: false, + selected: {CardMod.info}, + segments: >[ + ButtonSegment( + value: CardMod.info, + label: Text('PB'), + //icon: Icon(Icons.calendar_view_day) + ), + ButtonSegment( + value: CardMod.recent, + label: Text('Recent'), + //icon: Icon(Icons.calendar_view_day) + ), + ] + ), SegmentedButton( - segments: const >[ + showSelectedIcon: false, + segments: >[ ButtonSegment( value: Cards.overview, - label: Text('Overview'), + //label: Text('Overview'), icon: Icon(Icons.calendar_view_day)), ButtonSegment( value: Cards.tetraLeague, - label: Text('Tetra League'), - icon: Icon(Icons.calendar_view_week)), + //label: Text('Tetra League'), + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), ButtonSegment( value: Cards.quickPlay, - label: Text('Quick Play'), - icon: Icon(Icons.calendar_view_month)), + //label: Text('Quick Play'), + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), // ButtonSegment( // value: Cards.quickPlayExpert, // label: Text('QP Expert'), // icon: Icon(Icons.calendar_today)), ButtonSegment( value: Cards.sprint, - label: Text('40 Lines'), - icon: Icon(Icons.calendar_today)), + //label: Text('40 Lines'), + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), ButtonSegment( value: Cards.blitz, - label: Text('Blitz'), - icon: Icon(Icons.calendar_today)), + //label: Text('Blitz'), + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), // ButtonSegment( // value: Cards.other, // label: Text('Other'), // icon: Icon(Icons.calendar_today)), ], - selected: const {Cards.tetraLeague}, + selected: {rightCard}, onSelectionChanged: (Set newSelection) { setState(() { - // By default there is only a single segment that can be - // selected at one time, so its value is always the first - // item in the selected set. - //calendarView = newSelection.first; + rightCard = newSelection.first; });}) ], ), ), + // SizedBox( + // width: 450, + // child: _TLRecords(userID: "snapshot.data![0].userId", changePlayer: changePlayer, data: [], wasActiveInTL: true, oldMathcesHere: false, separateScrollController: true) + // ) ], ), ], @@ -815,11 +843,11 @@ class NewUserThingy extends StatelessWidget { text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - TextSpan(text: "Level ${intf.format(player.level.floor())}", recognizer: TapGestureRecognizer()..onTap = (){ + TextSpan(text: "Level ${intf.format(player.level.floor())}", style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: TapGestureRecognizer()..onTap = (){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( - title: Text("Level ${intf.format(player.level.floor())}"), + title: Text("Level ${intf.format(player.level.floor())}", textAlign: TextAlign.center), content: SingleChildScrollView( child: ListBody(children: [ Text( @@ -855,18 +883,25 @@ class NewUserThingy extends StatelessWidget { ); }), const TextSpan(text:"\n"), - TextSpan(text: player.gameTime.isNegative ? "-h --m" : playtime(player.gameTime), style: TextStyle(color: player.gameTime.isNegative ? Colors.grey : Colors.white), recognizer: !player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ + TextSpan(text: player.gameTime.isNegative ? "-h --m" : playtime(player.gameTime), style: TextStyle(color: player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( - title: Text(t.exactGametime), + title: Text(t.exactGametime, textAlign: TextAlign.center), 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(player.gameTime.inDays)}d ${nonsecs.format(player.gameTime.inHours%24)}h ${nonsecs.format(player.gameTime.inMinutes%60)}m ${nonsecs.format(player.gameTime.inSeconds%60)}s ${nonsecs3.format(player.gameTime.inMilliseconds%1000)}ms ${nonsecs3.format(player.gameTime.inMicroseconds%1000)}μs", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24) ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text("It's ${f4.format(player.gameTime.inSeconds/31536000)} years,"), + ), + Text("${f4.format(player.gameTime.inSeconds/2628000)} monts,"), + Text("${f4.format(player.gameTime.inSeconds/3600)} hours,"), + Text("${f2.format(player.gameTime.inMilliseconds/60000)} minutes,"), + Text("${intf.format(player.gameTime.inSeconds)} seconds"), ] ), ), @@ -984,29 +1019,176 @@ class TetraLeagueThingy extends StatelessWidget{ return Card( child: Column( children: [ - TLRatingThingy(userID: "w", tlData: league) - // SfRadialGauge( - // axes: [ - // RadialAxis( - // radiusFactor: 0.7, - // showTicks: false, - // showLabels: false, - // annotations: [ - // GaugeAnnotation(widget: Container(height: 196, child: - // Image.asset("res/tetrio_tl_alpha_ranks/${league.rank}.png")), - // angle: 270,positionFactor: 0.05 - // ), - // GaugeAnnotation(widget: Container(child: - // Text('24803.7921 TR',style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), - // angle: 90,positionFactor: 0.9 - // ) - // ], - // ) - // ], - // enableLoadingAnimation: true, - // ) + TLRatingThingy(userID: "w", tlData: league), + TLProgress(tlData: league,), + Wrap( + spacing: 25.0, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Table( + defaultColumnWidth:IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text("APM: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.apm) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + //Text(" APM", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text("PPS: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.pps) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + //Text(" PPS", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text("VS: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.vs) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + // Text(" VS", style: TextStyle(fontSize: 21)) + ]) + ], + ), + SizedBox( + height: 128.0, + width: 128.0, + child: SfRadialGauge( + axes: [ + RadialAxis( + // startAngle: 180, + // endAngle: 0, + minimum: 0.4, + maximum: 0.6, + //radiusFactor: 1.5, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: league.winrate, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + Text('${f2l.format(league.winrate*100)}%\nWR', textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + angle: 90,positionFactor: 0.1 + ), + // GaugeAnnotation(widget: Container(child: + // Text('50.03%\nWR', textAlign: TextAlign.center, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 22))), + // angle: 90,positionFactor: 0.5 + // ) + ], + ) + ] + ), + ), + Table( + defaultColumnWidth:IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + //Text("APM: ", style: TextStyle(fontSize: 21)), + Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" GP", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + //Text("PPS: ", style: TextStyle(fontSize: 21)), + Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" GW", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + //Text("VS: ", style: TextStyle(fontSize: 21)), + Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" in BY", style: TextStyle(fontSize: 21)) + ]) + ], + ), + // RichText( + // textAlign: TextAlign.right, + // text: TextSpan( + // style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + // children: [ + // TextSpan(text: "${league.apm != null ? f2.format(league.apm) : "---"} APM"), + // const TextSpan(text: "\n"), + // TextSpan(text: "${league.pps != null ? f2.format(league.pps) : "---"} PPS"), + // const TextSpan(text: "\n"), + // TextSpan(text: "${league.vs != null ? f2.format(league.vs) : "---"} VS"), + // ] + // ) + // ), + // StatCellNum(playerStat: league.apm??0.00, fractionDigits: 2, playerStatLabel: "APM", isScreenBig: true, higherIsBetter: true), + // StatCellNum(playerStat: league.pps??0.00, fractionDigits: 2, playerStatLabel: "PPS", isScreenBig: true, higherIsBetter: true), + // StatCellNum(playerStat: league.vs??0.00, fractionDigits: 2, playerStatLabel: "VS", isScreenBig: true, higherIsBetter: true), + + ], + ) ], ), ); } +} + +class _TLRecords extends StatelessWidget { + final String userID; + final Function changePlayer; + final List data; + final bool wasActiveInTL; + final bool oldMathcesHere; + final bool separateScrollController; + + /// Widget, that displays Tetra League records. + /// Accepts list of TL records ([data]) and [userID] of player from the view + const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere, this.separateScrollController = false}); + + @override + Widget build(BuildContext context) { + if (data.isEmpty) { + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text(t.errors.actionSuggestion), + if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) + ], + )); + } + bool bigScreen = MediaQuery.of(context).size.width >= 768; + int length = data.length; + return ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + controller: separateScrollController ? ScrollController() : null, + itemCount: oldMathcesHere ? length : length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == length) { + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + if (wasActiveInTL) Text(t.errors.actionSuggestion), + if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) + ], + )); + } + + 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( + stops: const [0, 0.05], + colors: [accentColor, Colors.transparent] + ) + ), + child: ListTile( + 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].results.leaderboard.firstWhere((element) => element.id != userID).username}"), + subtitle: Text(timestamp(data[index].ts), style: const TextStyle(color: Colors.grey)), + trailing: TrailingStats( + 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))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), + ), + ); + }); + } } \ No newline at end of file diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index e1afece..59fddcf 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -10,8 +10,6 @@ import 'package:tetra_stats/utils/numers_formats.dart'; class TLProgress extends StatelessWidget{ final TetraLeagueAlpha tlData; - final String? nextRank; - final String? previousRank; final double? nextRankTRcutoff; final double? previousRankTRcutoff; final double? nextRankGlickoCutoff; @@ -19,7 +17,7 @@ class TLProgress extends StatelessWidget{ final double? nextRankTRcutoffTarget; final double? previousRankTRcutoffTarget; - const TLProgress({super.key, required this.tlData, this.nextRank, this.previousRank, this.nextRankTRcutoff, this.previousRankTRcutoff, this.nextRankGlickoCutoff, this.previousGlickoCutoff, this.nextRankTRcutoffTarget, this.previousRankTRcutoffTarget}); + const TLProgress({super.key, required this.tlData, this.nextRankTRcutoff, this.previousRankTRcutoff, this.nextRankGlickoCutoff, this.previousGlickoCutoff, this.nextRankTRcutoffTarget, this.previousRankTRcutoffTarget}); double getBarPosition(){ return min(max(0, 1 - (tlData.standing - tlData.nextAt)/(tlData.prevAt - tlData.nextAt)), 1); @@ -31,51 +29,43 @@ class TLProgress extends StatelessWidget{ @override Widget build(BuildContext context) { - if (nextRank == null && previousRank == null && nextRankTRcutoff == null && previousRankTRcutoff == null && nextRankGlickoCutoff == null && previousGlickoCutoff == null && nextRankTRcutoffTarget == null && previousRankTRcutoffTarget == null) return Container(); + if (tlData.prevAt < 0 && tlData.nextAt < 0 && nextRankTRcutoff == null && previousRankTRcutoff == null && nextRankGlickoCutoff == null && previousGlickoCutoff == null && nextRankTRcutoffTarget == null && previousRankTRcutoffTarget == null) return Container(); final glickoForWin = rate(tlData.glicko!, tlData.rd!, 0.06, [[tlData.glicko!, tlData.rd!, 1]], {})[0]-tlData.glicko!; return Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: MediaQuery.of(context).size.width, - height: 48, - child: Stack( - alignment: AlignmentDirectional.bottomCenter, - fit: StackFit.expand, - children: [ - Positioned(left: 0, - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), - children: [ - if (tlData.prevAt > 0) TextSpan(text: "№ ${f0.format(tlData.prevAt)}"), - if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"), - if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"), - if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"), - if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null)) - ] - ) - ), + Row( + children: [ + RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + children: [ + if (tlData.prevAt > 0) TextSpan(text: "№ ${f0.format(tlData.prevAt)}"), + if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"), + if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"), + if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null)) + ] + ) ), - Positioned(right: 0, - child: RichText( - textAlign: TextAlign.right, - text: TextSpan( - style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), - children: [ - if (tlData.nextAt > 0) TextSpan(text: "№ ${f0.format(tlData.nextAt)}"), - if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), - if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), - if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), - if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) - ] - ) - ), + Spacer(), + RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + children: [ + if (tlData.nextAt > 0) TextSpan(text: "№ ${f0.format(tlData.nextAt)}"), + if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), + if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), + if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) + ] + ) ) - ],), + ], ), SfLinearGauge( minimum: 0, diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index bd04e7f..370e25f 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -165,12 +165,10 @@ class _TLThingyState extends State with TickerProviderStateMixin { tlData: currentTl, previousRankTRcutoff: widget.thatRankCutoff, previousGlickoCutoff: widget.thatRankCutoffGlicko, - previousRank: widget.tl.prevRank, previousRankTRcutoffTarget: widget.thatRankTarget, nextRankTRcutoff: widget.nextRankCutoff, nextRankGlickoCutoff: widget.nextRankCutoffGlicko, nextRankTRcutoffTarget: widget.nextRankTarget, - nextRank: widget.tl.nextRank ), if (currentTl.gamesPlayed < 10) Text(t.gamesUntilRanked(left: 10 - currentTl.gamesPlayed), diff --git a/res/icons/40l.svg b/res/icons/40l.svg new file mode 100644 index 0000000..5822c98 --- /dev/null +++ b/res/icons/40l.svg @@ -0,0 +1,111 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/res/icons/blitz.svg b/res/icons/blitz.svg new file mode 100644 index 0000000..26b7a5e --- /dev/null +++ b/res/icons/blitz.svg @@ -0,0 +1,114 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/res/icons/league.svg b/res/icons/league.svg new file mode 100644 index 0000000..38e5006 --- /dev/null +++ b/res/icons/league.svg @@ -0,0 +1,110 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/res/icons/qp.svg b/res/icons/qp.svg new file mode 100644 index 0000000..2ff874f --- /dev/null +++ b/res/icons/qp.svg @@ -0,0 +1,111 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + From 79bbf2e71d4b9a12f9f954f76b9fcf116ee56383 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 8 Aug 2024 01:42:04 +0300 Subject: [PATCH 14/33] still thinking --- lib/gen/strings.g.dart | 6 +- lib/views/main_view_tiles.dart | 329 +++++++++++++++++++++++++-------- res/i18n/strings.i18n.json | 2 +- 3 files changed, 257 insertions(+), 80 deletions(-) diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 6b2d7c8..4d8805c 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 2 /// Strings: 1216 (608 per locale) /// -/// Built on 2024-07-31 at 20:51 UTC +/// Built on 2024-08-07 at 15:58 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -720,7 +720,7 @@ class _StringsStatCellNumEn { String get lbpcShort => '№ in local LB'; String get gamesPlayed => 'Games\nplayed'; String get gamesWonTL => 'Games\nWon'; - String get winrate => 'Winrate\nprecentage'; + String get winrate => 'Winrate'; String get level => 'Level'; String get score => 'Score'; String get spp => 'Score\nPer Piece'; @@ -1824,7 +1824,7 @@ extension on Translations { case 'statCellNum.lbpcShort': return '№ in local LB'; case 'statCellNum.gamesPlayed': return 'Games\nplayed'; case 'statCellNum.gamesWonTL': return 'Games\nWon'; - case 'statCellNum.winrate': return 'Winrate\nprecentage'; + case 'statCellNum.winrate': return 'Winrate'; case 'statCellNum.level': return 'Level'; case 'statCellNum.score': return 'Score'; case 'statCellNum.spp': return 'Score\nPer Piece'; diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index cb748e2..fda44c4 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -75,7 +75,7 @@ TetrioPlayer testPlayer = TetrioPlayer( tlSeason1: TetraLeagueAlpha( timestamp: DateTime(1970), gamesPlayed: 28, - gamesWon: 14, + gamesWon: 15, bestRank: "x", decaying: false, rating: 23500.6194, @@ -106,6 +106,7 @@ class _MainState extends State with TickerProviderStateMixin { String _searchFor = "6098518e3d5155e6ec429cdc"; Cards rightCard = Cards.tetraLeague; final TextEditingController _searchController = TextEditingController(); + Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override void initState() { @@ -155,6 +156,11 @@ class _MainState extends State with TickerProviderStateMixin { selectedIcon: Icon(Icons.home), label: Text('First'), ), + NavigationRailDestination( + icon: Icon(Icons.data_thresholding_outlined), + selectedIcon: Icon(Icons.data_thresholding_outlined), + label: Text('First'), + ), NavigationRailDestination( icon: Icon(Icons.leaderboard), selectedIcon: Icon(Icons.leaderboard), @@ -261,18 +267,47 @@ class _MainState extends State with TickerProviderStateMixin { child: Column( //crossAxisAlignment: CrossAxisAlignment.center, children: [ - Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], + SizedBox( + height: constraints.maxHeight - 64, + child: SingleChildScrollView( + child: Column( + //mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.seasonStarts), + Center(child: Text(countdown(postSeasonLeft), textAlign: TextAlign.center, style: const TextStyle(fontSize: 32.0))), + ], + ), + ), + TetraLeagueThingy(league: testPlayer.tlSeason1!), + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + NerdStatsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!) + ], + ), ), ), - TetraLeagueThingy(league: testPlayer.tlSeason1!), - //const Card(), SegmentedButton( showSelectedIcon: false, selected: {CardMod.info}, @@ -986,6 +1021,15 @@ class _SearchDrawerState extends State { }); }, ), + ), + SliverToBoxAdapter( + child: ListTile( + title: Text(prefs.getString("player") ?? "dan63"), + onTap: () { + widget.changePlayer("6098518e3d5155e6ec429cdc"); + Navigator.of(context).pop(); + }, + ), ) ]; }, @@ -1021,30 +1065,34 @@ class TetraLeagueThingy extends StatelessWidget{ children: [ TLRatingThingy(userID: "w", tlData: league), TLProgress(tlData: league,), - Wrap( - spacing: 25.0, - alignment: WrapAlignment.spaceAround, - crossAxisAlignment: WrapCrossAlignment.center, + Row( + // spacing: 25.0, + // alignment: WrapAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Table( - defaultColumnWidth:IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text("APM: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.apm) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - //Text(" APM", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.pps) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - //Text(" PPS", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - Text("VS: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.vs) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - // Text(" VS", style: TextStyle(fontSize: 21)) - ]) - ], + Expanded( + child: Center( + child: Table( + defaultColumnWidth:IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text("APM: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.apm) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + //Text(" APM", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text("PPS: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.pps) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + //Text(" PPS", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text("VS: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.vs) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + // Text(" VS", style: TextStyle(fontSize: 21)) + ]) + ], + ), + ), ), SizedBox( height: 128.0, @@ -1066,63 +1114,192 @@ class TetraLeagueThingy extends StatelessWidget{ ], annotations: [ GaugeAnnotation(widget: Container(child: - Text('${f2l.format(league.winrate*100)}%\nWR', textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + Text(percentage.format(league.winrate), textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), angle: 90,positionFactor: 0.1 ), - // GaugeAnnotation(widget: Container(child: - // Text('50.03%\nWR', textAlign: TextAlign.center, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 22))), - // angle: 90,positionFactor: 0.5 - // ) + GaugeAnnotation(widget: Container(child: + Text(t.statCellNum.winrate, textAlign: TextAlign.center)), + angle: 270,positionFactor: 0.4 + ) ], ) ] ), ), - Table( - defaultColumnWidth:IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - //Text("APM: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" GP", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - //Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" GW", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - //Text("VS: ", style: TextStyle(fontSize: 21)), - Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" in BY", style: TextStyle(fontSize: 21)) - ]) - ], + Expanded( + child: Center( + child: Table( + defaultColumnWidth:IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + //Text("APM: ", style: TextStyle(fontSize: 21)), + Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" Games", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + //Text("PPS: ", style: TextStyle(fontSize: 21)), + Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" Won", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + //Text("VS: ", style: TextStyle(fontSize: 21)), + Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" in BY", style: TextStyle(fontSize: 21)) + ]) + ], + ), + ), ), - // RichText( - // textAlign: TextAlign.right, - // text: TextSpan( - // style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), - // children: [ - // TextSpan(text: "${league.apm != null ? f2.format(league.apm) : "---"} APM"), - // const TextSpan(text: "\n"), - // TextSpan(text: "${league.pps != null ? f2.format(league.pps) : "---"} PPS"), - // const TextSpan(text: "\n"), - // TextSpan(text: "${league.vs != null ? f2.format(league.vs) : "---"} VS"), - // ] - // ) - // ), - // StatCellNum(playerStat: league.apm??0.00, fractionDigits: 2, playerStatLabel: "APM", isScreenBig: true, higherIsBetter: true), - // StatCellNum(playerStat: league.pps??0.00, fractionDigits: 2, playerStatLabel: "PPS", isScreenBig: true, higherIsBetter: true), - // StatCellNum(playerStat: league.vs??0.00, fractionDigits: 2, playerStatLabel: "VS", isScreenBig: true, higherIsBetter: true), - ], - ) + ), ], ), ); } } +class NerdStatsThingy extends StatelessWidget{ + final NerdStats nerdStats; + + const NerdStatsThingy({super.key, required this.nerdStats}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Row( + children: [ + SizedBox( + height: 256.0, + width: 256.0, + child: SfRadialGauge( + axes: [ + RadialAxis( + startAngle: 120, + endAngle: 240, + minimum: 0.0, + maximum: 1.0, + //radiusFactor: 1.5, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: nerdStats.app, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round"), + children: [ + TextSpan(text: "APP\n"), + TextSpan(text: f3.format(nerdStats.app), style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), + //TextSpan(text: "\nAPP"), + ] + ))), + angle: 180,positionFactor: 0.5 + ), + ], + ), + RadialAxis( + startAngle: 300, + endAngle: 60, + isInversed: true, + minimum: 1.8, + maximum: 2.4, + //radiusFactor: 1.5, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: nerdStats.vsapm, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round"), + children: [ + TextSpan(text: "VS/APM\n"), + TextSpan(text: f3.format(nerdStats.vsapm), style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), + ] + ))), + angle: 0,positionFactor: 0.5 + ) + ], + ) + ] + ), + ), + Wrap( + children: [ + GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, label: t.statCellNum.dss, sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, label: t.statCellNum.dsp, sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.appdsp, min: 0, max: 1.2, label: t.statCellNum.appdsp, sideSize: 128.0, fractionDigits: 3) + ], + ) + ] + ), + ], + ) + ); + } + +} + +class GaugetThingy extends StatelessWidget{ + final double value; + final double min; + final double max; + final String label; + final double sideSize; + final int fractionDigits; + + GaugetThingy({super.key, required this.value, required this.min, required this.max, required this.label, required this.sideSize, required this.fractionDigits}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: sideSize, + width: sideSize, + child: SfRadialGauge( + axes: [ + RadialAxis( + // startAngle: 180, + // endAngle: 0, + minimum: min, + maximum: max, + //radiusFactor: 1.5, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: value, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + Text(f3.format(value), textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + angle: 90,positionFactor: 0.25 + ), + GaugeAnnotation(widget: Container(child: + Text(label, textAlign: TextAlign.center, style: TextStyle(height: .9))), + angle: 270,positionFactor: 0.4 + ) + ], + ) + ] + ), + ); + } + +} + class _TLRecords extends StatelessWidget { final String userID; final Function changePlayer; diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 89a9dcd..789e25e 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -272,7 +272,7 @@ "lbpcShort": "№ in local LB", "gamesPlayed": "Games\nplayed", "gamesWonTL": "Games\nWon", - "winrate": "Winrate\nprecentage", + "winrate": "Winrate", "level": "Level", "score": "Score", "spp": "Score\nPer Piece", From dd385ad71362823fc85a80048263bf8447de734c Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 9 Aug 2024 02:01:46 +0300 Subject: [PATCH 15/33] Oh yeah I'm gonna have shitton of leaderboards to look at --- lib/main.dart | 2 +- lib/views/main_view.dart | 1 - lib/views/main_view_tiles.dart | 798 +++++++++++++++++++++++++-------- lib/widgets/stat_sell_num.dart | 30 +- test/api_test.dart | 364 +++++++-------- 5 files changed, 797 insertions(+), 398 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 3f81e0f..5890c20 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index c74a52b..b590e9a 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -871,7 +871,6 @@ class _History extends StatelessWidget{ )); } bool bigScreen = MediaQuery.of(context).size.width > 768; - //List<_HistoryChartSpot> selectedGraph = _gamesPlayedInsteadOfDateAndTime ? chartsDataGamesPlayed[_chartsIndex].value! : chartsData[_chartsIndex].value!; List<_HistoryChartSpot> selectedGraph = chartsData[_chartsIndex].value!; return SingleChildScrollView( scrollDirection: Axis.vertical, diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index fda44c4..41edc67 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart' hide Badge; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/services/crud_exceptions.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'; @@ -103,10 +105,9 @@ News testNews = News("6098518e3d5155e6ec429cdc", [ late ScrollController controller; class _MainState extends State with TickerProviderStateMixin { + int destination = 0; String _searchFor = "6098518e3d5155e6ec429cdc"; - Cards rightCard = Cards.tetraLeague; final TextEditingController _searchController = TextEditingController(); - Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override void initState() { @@ -154,22 +155,22 @@ class _MainState extends State with TickerProviderStateMixin { NavigationRailDestination( icon: Icon(Icons.home), selectedIcon: Icon(Icons.home), - label: Text('First'), + label: Text('Home'), ), NavigationRailDestination( icon: Icon(Icons.data_thresholding_outlined), selectedIcon: Icon(Icons.data_thresholding_outlined), - label: Text('First'), + label: Text('Graphs'), ), NavigationRailDestination( icon: Icon(Icons.leaderboard), selectedIcon: Icon(Icons.leaderboard), - label: Text('Second'), + label: Text('Leaderboards'), ), NavigationRailDestination( icon: Icon(Icons.compress), selectedIcon: Icon(Icons.compress), - label: Text('Third'), + label: Text('Cutoffs'), ), NavigationRailDestination( icon: Icon(Icons.calculate), @@ -179,201 +180,581 @@ class _MainState extends State with TickerProviderStateMixin { NavigationRailDestination( icon: Icon(Icons.settings), selectedIcon: Icon(Icons.settings), - label: Text('Third'), + label: Text('Settings'), ) ], - selectedIndex: 0 + selectedIndex: destination, + onDestinationSelected: (value) { + setState(() { + destination = value; + }); + }, ), - Row( + switch (destination){ + 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), + 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), + 2 => DestinationLeaderboards(constraints: constraints), + _ => Text("Unknown destination $destination") + } + ]); + }, + )); + } +} + +class DestinationLeaderboards extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationLeaderboards({super.key, required this.constraints}); + + @override + State createState() => _DestinationLeaderboardsState(); +} + +class _DestinationLeaderboardsState extends State { + Cards rightCard = Cards.tetraLeague; + Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + final List leaderboards = ["Tetra League", "Quick Play", "Quick Play Expert"]; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 350.0, + height: widget.constraints.maxHeight, + child: Column( + children: [ + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text("Leaderboards", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + const Spacer() + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: leaderboards.length, + itemBuilder: (BuildContext context, int index) { + return Card( + surfaceTintColor: theme.colorScheme.primary, + child: ListTile( + title: Text(leaderboards[index]), + ), + ); + } + ), + ), + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 350 - 88, + child: Column( + children: [ + + ], + ), + ), + ], + ); + } +} + +class DestinationGraphs extends StatefulWidget{ + final String searchFor; + //final Function setState; + final BoxConstraints constraints; + + const DestinationGraphs({super.key, required this.searchFor, required this.constraints}); + + @override + State createState() => _DestinationGraphsState(); +} + +class _DestinationGraphsState extends State { + Cards rightCard = Cards.tetraLeague; + bool fetchData = false; + bool _gamesPlayedInsteadOfDateAndTime = false; + late ZoomPanBehavior _zoomPanBehavior; + late TooltipBehavior _tooltipBehavior; + String yAxisTitle = ""; + bool _smooth = false; + final List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; + int _chartsIndex = 0; + late List>> chartsData; + Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + + @override + void initState(){ + _tooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: 450.0, - child: FutureBuilder(future: teto.fetchPlayer(_searchFor), builder:(context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return Column( + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${f4.format(data.stat)} $yAxisTitle", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + ), + ), + Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) + ], + ), + ); + } + ); + _zoomPanBehavior = ZoomPanBehavior( + enablePinching: true, + enableSelectionZooming: true, + enableMouseWheelZooming : true, + enablePanning: true, + ); + super.initState(); + } + + Future>>> getChartsData(bool fetchHistory) async { + List states = []; + Set uniqueTL = {}; + + if(fetchHistory){ + try{ + var history = await teto.fetchAndsaveTLHistory(widget.searchFor); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); + }on TetrioHistoryNotExist{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noHistorySaved))); + }on P1nkl0bst3rForbidden { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); + }on P1nkl0bst3rInternalProblem { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); + }on P1nkl0bst3rTooManyRequests{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); + } + } + + states.addAll(await teto.getPlayer(widget.searchFor)); + for (var element in states) { + if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); + if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); + } + + if (uniqueTL.length >= 2){ + chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rating)], child: Text(t.statCellNum.tr)), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), + ]; + }else{ + chartsData = []; + } + + fetchData = false; + + return chartsData; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>>>( + future: getChartsData(fetchData), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + List<_HistoryChartSpot> selectedGraph = snapshot.data![_chartsIndex].value!; + yAxisTitle = _historyShortTitles[_chartsIndex]; + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Wrap( + spacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, children: [ - NewUserThingy(player: snapshot.data!, showStateTimestamp: false, setState: setState), - if (snapshot.data!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.badges), - if (snapshot.data!.distinguishment != null) DistinguishmentThingy(snapshot.data!.distinguishment!), - if (snapshot.data!.bio != null) Card( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ], - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: MarkdownBody(data: snapshot.data!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), - ) - ], - ), + const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: const [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], + value: _gamesPlayedInsteadOfDateAndTime, + onChanged: (value) { + setState(() { + _gamesPlayedInsteadOfDateAndTime = value!; + }); + } ), - //if (testNews != null && testNews!.news.isNotEmpty) - Expanded( - child: FutureBuilder( - future: teto.fetchNews(_searchFor), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return Card(child: Center(child: CircularProgressIndicator())); - case ConnectionState.done: - if (snapshot.hasData){ - return NewsThingy(snapshot.data!); - }else if (snapshot.hasError){ - return Card(child: Column(children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Text(snapshot.stackTrace.toString()) - ] - )); - } - } - return Text("what?"); - } - ), - ) ], - ); - }else{ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), - ), - ], - ) - ); - } - } - }, - )), - SizedBox( - width: constraints.maxWidth - 450 - 80, - child: Column( - //crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: constraints.maxHeight - 64, - child: SingleChildScrollView( - child: Column( - //mainAxisSize: MainAxisSize.min, + ), + Row( + mainAxisSize: MainAxisSize.min, children: [ - Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: chartsData, + value: chartsData[_chartsIndex].value, + onChanged: (value) { + setState(() { + _chartsIndex = chartsData.indexWhere((element) => element.value == value); + }); + } ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.seasonStarts), - Center(child: Text(countdown(postSeasonLeft), textAlign: TextAlign.center, style: const TextStyle(fontSize: 32.0))), - ], - ), - ), - TetraLeagueThingy(league: testPlayer.tlSeason1!), - Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - NerdStatsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!) ], ), + if (selectedGraph.length > 300) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox(value: _smooth, + checkColor: Colors.black, + onChanged: ((value) { + setState(() { + _smooth = value!; + }); + })), + Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) + ], + ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) + ], + ), + ), + if(chartsData[_chartsIndex].value!.length > 1) Card( + child: SizedBox( + width: MediaQuery.of(context).size.width - 88, + height: MediaQuery.of(context).size.height - 60, + child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), + child: SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( + rangePadding: ChartRangePadding.additional, + ), + margin: const EdgeInsets.all(0), + series: [ + if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( + enableTooltip: true, + dataSource: chartsData[_chartsIndex].value!, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (chartsData[_chartsIndex].value!.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ) + else StepLineSeries<_HistoryChartSpot, DateTime>( + enableTooltip: true, + dataSource: chartsData[_chartsIndex].value!, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (chartsData[_chartsIndex].value!.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ), + ], + ), + ) + ), + ) + else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + Text(t.errors.actionSuggestion), + TextButton(onPressed: (){setState(() { + fetchData = true; + });}, child: Text(t.fetchAndsaveTLHistory)) + ], + )) + ], + ), + ); + } + if (snapshot.hasError){ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + ], + )); + }, + ); + } +} + +class _HistoryChartSpot{ + final DateTime timestamp; + final int gamesPlayed; + final String rank; + final double stat; + const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat); +} + +class DestinationHome extends StatefulWidget{ + final String searchFor; + //final Function setState; + final BoxConstraints constraints; + + const DestinationHome({super.key, required this.searchFor, required this.constraints}); + + @override + State createState() => _DestinationHomeState(); +} + +class _DestinationHomeState extends State { + Cards rightCard = Cards.tetraLeague; + Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 450.0, + child: FutureBuilder(future: teto.fetchPlayer(widget.searchFor), builder:(context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + NewUserThingy(player: snapshot.data!, showStateTimestamp: false, setState: setState), + if (snapshot.data!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.badges), + if (snapshot.data!.distinguishment != null) DistinguishmentThingy(snapshot.data!.distinguishment!), + if (snapshot.data!.bio != null) Card( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MarkdownBody(data: snapshot.data!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], ), ), - SegmentedButton( - showSelectedIcon: false, - selected: {CardMod.info}, - segments: >[ - ButtonSegment( - value: CardMod.info, - label: Text('PB'), - //icon: Icon(Icons.calendar_view_day) - ), - ButtonSegment( - value: CardMod.recent, - label: Text('Recent'), - //icon: Icon(Icons.calendar_view_day) - ), - ] + //if (testNews != null && testNews!.news.isNotEmpty) + Expanded( + child: FutureBuilder( + future: teto.fetchNews(widget.searchFor), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return Card(child: Center(child: CircularProgressIndicator())); + case ConnectionState.done: + if (snapshot.hasData){ + return NewsThingy(snapshot.data!); + }else if (snapshot.hasError){ + return Card(child: Column(children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Text(snapshot.stackTrace.toString()) + ] + )); + } + } + return Text("what?"); + } + ), + ) + ], + ); + }else{ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + }, + )), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: Column( + //crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: widget.constraints.maxHeight - 64, + child: SingleChildScrollView( + child: Column( + //mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - ButtonSegment( - value: Cards.overview, - //label: Text('Overview'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Cards.tetraLeague, - //label: Text('Tetra League'), - icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.quickPlay, - //label: Text('Quick Play'), - icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - // ButtonSegment( - // value: Cards.quickPlayExpert, - // label: Text('QP Expert'), - // icon: Icon(Icons.calendar_today)), - ButtonSegment( - value: Cards.sprint, - //label: Text('40 Lines'), - icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.blitz, - //label: Text('Blitz'), - icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - // ButtonSegment( - // value: Cards.other, - // label: Text('Other'), - // icon: Icon(Icons.calendar_today)), - ], - selected: {rightCard}, - onSelectionChanged: (Set newSelection) { - setState(() { - rightCard = newSelection.first; - });}) + Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.seasonStarts), + Center(child: Text(countdown(postSeasonLeft), textAlign: TextAlign.center, style: const TextStyle(fontSize: 32.0))), + ], + ), + ), + TetraLeagueThingy(league: testPlayer.tlSeason1!), + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + NerdStatsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!) ], ), ), - // SizedBox( - // width: 450, - // child: _TLRecords(userID: "snapshot.data![0].userId", changePlayer: changePlayer, data: [], wasActiveInTL: true, oldMathcesHere: false, separateScrollController: true) - // ) - ], ), - ], - ); - }, - )); + SegmentedButton( + showSelectedIcon: false, + selected: {CardMod.info}, + segments: >[ + ButtonSegment( + value: CardMod.info, + label: Text('PB'), + //icon: Icon(Icons.calendar_view_day) + ), + ButtonSegment( + value: CardMod.recent, + label: Text('Recent'), + //icon: Icon(Icons.calendar_view_day) + ), + ] + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: Cards.overview, + //label: Text('Overview'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + //label: Text('Tetra League'), + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.quickPlay, + //label: Text('Quick Play'), + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + // ButtonSegment( + // value: Cards.quickPlayExpert, + // label: Text('QP Expert'), + // icon: Icon(Icons.calendar_today)), + ButtonSegment( + value: Cards.sprint, + //label: Text('40 Lines'), + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.blitz, + //label: Text('Blitz'), + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + // ButtonSegment( + // value: Cards.other, + // label: Text('Other'), + // icon: Icon(Icons.calendar_today)), + ], + selected: {rightCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + rightCard = newSelection.first; + });}) + ], + ), + ), + // SizedBox( + // width: 450, + // child: _TLRecords(userID: "snapshot.data![0].userId", changePlayer: changePlayer, data: [], wasActiveInTL: true, oldMathcesHere: false, separateScrollController: true) + // ) + ], + ); } } @@ -778,7 +1159,6 @@ class NewUserThingy extends StatelessWidget { 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; @@ -1131,6 +1511,11 @@ class TetraLeagueThingy extends StatelessWidget{ child: Table( defaultColumnWidth:IntrinsicColumnWidth(), children: [ + TableRow(children: [ + //Text("VS: ", style: TextStyle(fontSize: 21)), + Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" in BY", style: TextStyle(fontSize: 21)) + ]), TableRow(children: [ //Text("APM: ", style: TextStyle(fontSize: 21)), Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), @@ -1140,11 +1525,6 @@ class TetraLeagueThingy extends StatelessWidget{ //Text("PPS: ", style: TextStyle(fontSize: 21)), Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), Text(" Won", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - //Text("VS: ", style: TextStyle(fontSize: 21)), - Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" in BY", style: TextStyle(fontSize: 21)) ]) ], ), @@ -1169,6 +1549,7 @@ class NerdStatsThingy extends StatelessWidget{ child: Column( children: [ Row( + mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: 256.0, @@ -1236,34 +1617,53 @@ class NerdStatsThingy extends StatelessWidget{ ] ), ), - Wrap( - children: [ - GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, label: t.statCellNum.dss, sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, label: t.statCellNum.dsp, sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.appdsp, min: 0, max: 1.2, label: t.statCellNum.appdsp, sideSize: 128.0, fractionDigits: 3) - ], + Expanded( + child: Wrap( + children: [ + GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2), + GaugetThingy(value: nerdStats.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1), + ], + ), ) ] ), ], ) ); - } + } +} + +class EstTrThingy extends StatelessWidget{ + final EstTr estTr; + + const EstTrThingy({super.key, required this.estTr}); + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } } class GaugetThingy extends StatelessWidget{ final double value; final double min; final double max; + final double tickInterval; final String label; final double sideSize; final int fractionDigits; - GaugetThingy({super.key, required this.value, required this.min, required this.max, required this.label, required this.sideSize, required this.fractionDigits}); + GaugetThingy({super.key, required this.value, required this.min, required this.max, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits}); @override Widget build(BuildContext context) { + NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits); return SizedBox( height: sideSize, width: sideSize, @@ -1277,14 +1677,14 @@ class GaugetThingy extends StatelessWidget{ //radiusFactor: 1.5, showTicks: true, showLabels: false, - interval: 0.1, + interval: tickInterval, //labelsPosition: ElementsPosition.outside, ranges:[ GaugeRange(startValue: 0, endValue: value, color: theme.colorScheme.primary) ], annotations: [ GaugeAnnotation(widget: Container(child: - Text(f3.format(value), textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + Text(f.format(value), textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), angle: 90,positionFactor: 0.25 ), GaugeAnnotation(widget: Container(child: diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 76edf64..f837bb4 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -88,21 +88,21 @@ class StatCellNum extends StatelessWidget { : TextButton( onPressed: () { showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(alertTitle??playerStatLabel.replaceAll(RegExp(r'\n'), " "), - style: const TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: alertWidgets!), - ), - actions: [ - TextButton( - child: Text(okText??"OK"), - onPressed: () {Navigator.of(context).pop();} - ) - ], - ) + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(alertTitle??playerStatLabel.replaceAll(RegExp(r'\n'), " "), + style: const TextStyle( + fontFamily: "Eurostile Round Extended")), + content: SingleChildScrollView( + child: ListBody(children: alertWidgets!), + ), + actions: [ + TextButton( + child: Text(okText??"OK"), + onPressed: () {Navigator.of(context).pop();} + ) + ], + ) ); }, style: ButtonStyle( diff --git a/test/api_test.dart b/test/api_test.dart index 52f3e87..4f4f8d6 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -1,186 +1,186 @@ -import 'dart:io'; -import 'dart:ui'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; -import 'package:test/test.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; -import 'package:tetra_stats/services/crud_exceptions.dart'; -import 'package:tetra_stats/services/tetrio_crud.dart'; +// import 'dart:io'; +// import 'dart:ui'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; +// import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +// import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; +// import 'package:test/test.dart'; +// import 'package:tetra_stats/data_objects/tetrio.dart'; +// import 'package:tetra_stats/services/crud_exceptions.dart'; +// import 'package:tetra_stats/services/tetrio_crud.dart'; -void main() { - WidgetsFlutterBinding.ensureInitialized(); - DartPluginRegistrant.ensureInitialized(); - late TetrioService teto; - setUp(() { - if (kIsWeb) { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfiWeb; - } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - } - teto = TetrioService(); - }); +// void main() { +// WidgetsFlutterBinding.ensureInitialized(); +// DartPluginRegistrant.ensureInitialized(); +// late TetrioService teto; +// setUp(() { +// if (kIsWeb) { +// sqfliteFfiInit(); +// databaseFactory = databaseFactoryFfiWeb; +// } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { +// sqfliteFfiInit(); +// databaseFactory = databaseFactoryFfi; +// } +// teto = TetrioService(); +// }); - test("Initialize TetrioServise", () async { - teto.open(); - }); // a fucking MissingPluginException how does that even happening? - // i guess i will be unable to test iteractions with DB +// test("Initialize TetrioServise", () async { +// teto.open(); +// }); // a fucking MissingPluginException how does that even happening? +// // i guess i will be unable to test iteractions with DB - group("Test fetchPlayer with different players", () { - // those tests exist in order to detect a tiny little change in Tetra Channel API in case of some update. - test("dan63047 (user who have activity in tetra league)", () async { - TetrioPlayer dan63047 = await teto.fetchPlayer("6098518e3d5155e6ec429cdc"); - expect(dan63047.userId, "6098518e3d5155e6ec429cdc"); - expect(dan63047.registrationTime != null, true); - expect(dan63047.avatarRevision != null, true); - expect(dan63047.connections != null, true); - expect(dan63047.role, "user"); - expect(dan63047.distinguishment, null); // imagine if that one fails one day lol - expect(dan63047.tlSeason1.glicko != null, true); - //expect(dan63047.tlSeason1.rank != "z", true); lol - expect(dan63047.tlSeason1.percentileRank != "z", true); - expect(dan63047.tlSeason1.rating > -1, true); - expect(dan63047.tlSeason1.gamesPlayed > 9, true); - expect(dan63047.tlSeason1.gamesWon > 0, true); - //expect(dan63047.tlSeason1.standing, -1); - //expect(dan63047.tlSeason1.standingLocal, -1); - expect(dan63047.tlSeason1.apm != null, true); - expect(dan63047.tlSeason1.pps != null, true); - expect(dan63047.tlSeason1.vs != null, true); - expect(dan63047.tlSeason1.nerdStats != null, true); - expect(dan63047.tlSeason1.estTr != null, true); - expect(dan63047.tlSeason1.esttracc != null, true); - expect(dan63047.tlSeason1.playstyle != null, true); - }); - test("osk (sysop who have activity in tetra league)", () async { - TetrioPlayer osk = await teto.fetchPlayer("5e32fc85ab319c2ab1beb07c"); - expect(osk.userId, "5e32fc85ab319c2ab1beb07c"); - expect(osk.registrationTime, null); - expect(osk.country, "XM"); - expect(osk.avatarRevision != null, true); - expect(osk.bannerRevision != null, true); - expect(osk.connections != null, true); - expect(osk.verified, true); - expect(osk.role, "sysop"); - expect(osk.distinguishment != null, true); - expect(osk.tlSeason1.glicko != null, true); - expect(osk.tlSeason1.glicko != null, true); - expect(osk.tlSeason1.rank == "z", true); - expect(osk.tlSeason1.percentileRank != "z", true); - expect(osk.tlSeason1.rating > -1, true); - expect(osk.tlSeason1.gamesPlayed > 9, true); - expect(osk.tlSeason1.gamesWon > 0, true); - expect(osk.tlSeason1.standing, -1); - expect(osk.tlSeason1.standingLocal, -1); - expect(osk.tlSeason1.apm != null, true); - expect(osk.tlSeason1.pps != null, true); - expect(osk.tlSeason1.vs != null, true); - expect(osk.tlSeason1.nerdStats != null, true); - expect(osk.tlSeason1.estTr != null, true); - expect(osk.tlSeason1.esttracc != null, true); - expect(osk.tlSeason1.playstyle != null, true); - }); - test("kagari (sysop who have zero activity)", () async { - TetrioPlayer kagari = await teto.fetchPlayer("5e331c3ce24a5a3e258f7a1b"); - expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b"); - expect(kagari.registrationTime, null); - expect(kagari.country, "XM"); - expect(kagari.xp, 0); - expect(kagari.gamesPlayed, -1); - expect(kagari.gamesWon, -1); - expect(kagari.gameTime, const Duration(seconds: -1)); - expect(kagari.avatarRevision != null, true); - expect(kagari.bannerRevision != null, true); - expect(kagari.connections, null); - expect(kagari.verified, true); - expect(kagari.distinguishment != null, true); - expect(kagari.distinguishment!.detail, "kagarin"); - expect(kagari.friendCount, 1); - expect(kagari.tlSeason1.glicko, null); - expect(kagari.tlSeason1.rank, "z"); - expect(kagari.tlSeason1.percentileRank, "z"); - expect(kagari.tlSeason1.rating, -1); - expect(kagari.tlSeason1.decaying, false); - expect(kagari.tlSeason1.gamesPlayed, 0); - expect(kagari.tlSeason1.gamesWon, 0); - expect(kagari.tlSeason1.standing, -1); - expect(kagari.tlSeason1.standingLocal, -1); - expect(kagari.tlSeason1.apm, null); - expect(kagari.tlSeason1.pps, null); - expect(kagari.tlSeason1.vs, null); - expect(kagari.tlSeason1.nerdStats, null); - expect(kagari.tlSeason1.estTr, null); - expect(kagari.tlSeason1.esttracc, null); - expect(kagari.tlSeason1.playstyle, null); - }); - test("furry (banned account)", () async { - TetrioPlayer furry = await teto.fetchPlayer("5eea0ff69a1ba76c20347086"); - expect(furry.userId, "5eea0ff69a1ba76c20347086"); - expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z")); - expect(furry.role, "banned"); - expect(furry.badges.isEmpty, true); - expect(furry.badstanding, false); - expect(furry.xp, 0); - expect(furry.supporterTier, 0); - expect(furry.verified, false); - expect(furry.connections, null); - expect(furry.gamesPlayed, 0); - expect(furry.gamesWon, 0); - expect(furry.gameTime, Duration.zero); - expect(furry.tlSeason1.glicko, null); - expect(furry.tlSeason1.rank, "z"); - expect(furry.tlSeason1.percentileRank, "z"); - expect(furry.tlSeason1.rating, -1); - expect(furry.tlSeason1.decaying, false); - expect(furry.tlSeason1.gamesPlayed, 0); - expect(furry.tlSeason1.gamesWon, 0); - expect(furry.tlSeason1.standing, -1); - expect(furry.tlSeason1.standingLocal, -1); - expect(furry.tlSeason1.apm, null); - expect(furry.tlSeason1.pps, null); - expect(furry.tlSeason1.vs, null); - expect(furry.tlSeason1.nerdStats, null); - expect(furry.tlSeason1.estTr, null); - expect(furry.tlSeason1.esttracc, null); - expect(furry.tlSeason1.playstyle, null); - }); - test("oskwarefan (anon account)", () async { - TetrioPlayer oskwarefan = await teto.fetchPlayer("646cb8273e887a054d64febe"); - expect(oskwarefan.userId, "646cb8273e887a054d64febe"); - expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z")); - expect(oskwarefan.role, "anon"); - expect(oskwarefan.xp > 0, true); - expect(oskwarefan.gamesPlayed > -1, true); - expect(oskwarefan.gamesWon > -1, true); - expect(oskwarefan.gameTime.isNegative, false); - expect(oskwarefan.country, null); - expect(oskwarefan.verified, false); - expect(oskwarefan.connections, null); - expect(oskwarefan.friendCount, 0); - expect(oskwarefan.tlSeason1.glicko, null); - expect(oskwarefan.tlSeason1.rank, "z"); - expect(oskwarefan.tlSeason1.percentileRank, "z"); - expect(oskwarefan.tlSeason1.rating, -1); - expect(oskwarefan.tlSeason1.decaying, true); // ??? why true? - expect(oskwarefan.tlSeason1.gamesPlayed, 0); - expect(oskwarefan.tlSeason1.gamesWon, 0); - expect(oskwarefan.tlSeason1.standing, -1); - expect(oskwarefan.tlSeason1.standingLocal, -1); - expect(oskwarefan.tlSeason1.apm, null); - expect(oskwarefan.tlSeason1.pps, null); - expect(oskwarefan.tlSeason1.vs, null); - expect(oskwarefan.tlSeason1.nerdStats, null); - expect(oskwarefan.tlSeason1.estTr, null); - expect(oskwarefan.tlSeason1.esttracc, null); - expect(oskwarefan.tlSeason1.playstyle, null); - }); +// group("Test fetchPlayer with different players", () { +// // those tests exist in order to detect a tiny little change in Tetra Channel API in case of some update. +// test("dan63047 (user who have activity in tetra league)", () async { +// TetrioPlayer dan63047 = await teto.fetchPlayer("6098518e3d5155e6ec429cdc"); +// expect(dan63047.userId, "6098518e3d5155e6ec429cdc"); +// expect(dan63047.registrationTime != null, true); +// expect(dan63047.avatarRevision != null, true); +// expect(dan63047.connections != null, true); +// expect(dan63047.role, "user"); +// expect(dan63047.distinguishment, null); // imagine if that one fails one day lol +// expect(dan63047.tlSeason1.glicko != null, true); +// //expect(dan63047.tlSeason1.rank != "z", true); lol +// expect(dan63047.tlSeason1.percentileRank != "z", true); +// expect(dan63047.tlSeason1.rating > -1, true); +// expect(dan63047.tlSeason1.gamesPlayed > 9, true); +// expect(dan63047.tlSeason1.gamesWon > 0, true); +// //expect(dan63047.tlSeason1.standing, -1); +// //expect(dan63047.tlSeason1.standingLocal, -1); +// expect(dan63047.tlSeason1.apm != null, true); +// expect(dan63047.tlSeason1.pps != null, true); +// expect(dan63047.tlSeason1.vs != null, true); +// expect(dan63047.tlSeason1.nerdStats != null, true); +// expect(dan63047.tlSeason1.estTr != null, true); +// expect(dan63047.tlSeason1.esttracc != null, true); +// expect(dan63047.tlSeason1.playstyle != null, true); +// }); +// test("osk (sysop who have activity in tetra league)", () async { +// TetrioPlayer osk = await teto.fetchPlayer("5e32fc85ab319c2ab1beb07c"); +// expect(osk.userId, "5e32fc85ab319c2ab1beb07c"); +// expect(osk.registrationTime, null); +// expect(osk.country, "XM"); +// expect(osk.avatarRevision != null, true); +// expect(osk.bannerRevision != null, true); +// expect(osk.connections != null, true); +// expect(osk.verified, true); +// expect(osk.role, "sysop"); +// expect(osk.distinguishment != null, true); +// expect(osk.tlSeason1.glicko != null, true); +// expect(osk.tlSeason1.glicko != null, true); +// expect(osk.tlSeason1.rank == "z", true); +// expect(osk.tlSeason1.percentileRank != "z", true); +// expect(osk.tlSeason1.rating > -1, true); +// expect(osk.tlSeason1.gamesPlayed > 9, true); +// expect(osk.tlSeason1.gamesWon > 0, true); +// expect(osk.tlSeason1.standing, -1); +// expect(osk.tlSeason1.standingLocal, -1); +// expect(osk.tlSeason1.apm != null, true); +// expect(osk.tlSeason1.pps != null, true); +// expect(osk.tlSeason1.vs != null, true); +// expect(osk.tlSeason1.nerdStats != null, true); +// expect(osk.tlSeason1.estTr != null, true); +// expect(osk.tlSeason1.esttracc != null, true); +// expect(osk.tlSeason1.playstyle != null, true); +// }); +// test("kagari (sysop who have zero activity)", () async { +// TetrioPlayer kagari = await teto.fetchPlayer("5e331c3ce24a5a3e258f7a1b"); +// expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b"); +// expect(kagari.registrationTime, null); +// expect(kagari.country, "XM"); +// expect(kagari.xp, 0); +// expect(kagari.gamesPlayed, -1); +// expect(kagari.gamesWon, -1); +// expect(kagari.gameTime, const Duration(seconds: -1)); +// expect(kagari.avatarRevision != null, true); +// expect(kagari.bannerRevision != null, true); +// expect(kagari.connections, null); +// expect(kagari.verified, true); +// expect(kagari.distinguishment != null, true); +// expect(kagari.distinguishment!.detail, "kagarin"); +// expect(kagari.friendCount, 1); +// expect(kagari.tlSeason1.glicko, null); +// expect(kagari.tlSeason1.rank, "z"); +// expect(kagari.tlSeason1.percentileRank, "z"); +// expect(kagari.tlSeason1.rating, -1); +// expect(kagari.tlSeason1.decaying, false); +// expect(kagari.tlSeason1.gamesPlayed, 0); +// expect(kagari.tlSeason1.gamesWon, 0); +// expect(kagari.tlSeason1.standing, -1); +// expect(kagari.tlSeason1.standingLocal, -1); +// expect(kagari.tlSeason1.apm, null); +// expect(kagari.tlSeason1.pps, null); +// expect(kagari.tlSeason1.vs, null); +// expect(kagari.tlSeason1.nerdStats, null); +// expect(kagari.tlSeason1.estTr, null); +// expect(kagari.tlSeason1.esttracc, null); +// expect(kagari.tlSeason1.playstyle, null); +// }); +// test("furry (banned account)", () async { +// TetrioPlayer furry = await teto.fetchPlayer("5eea0ff69a1ba76c20347086"); +// expect(furry.userId, "5eea0ff69a1ba76c20347086"); +// expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z")); +// expect(furry.role, "banned"); +// expect(furry.badges.isEmpty, true); +// expect(furry.badstanding, false); +// expect(furry.xp, 0); +// expect(furry.supporterTier, 0); +// expect(furry.verified, false); +// expect(furry.connections, null); +// expect(furry.gamesPlayed, 0); +// expect(furry.gamesWon, 0); +// expect(furry.gameTime, Duration.zero); +// expect(furry.tlSeason1.glicko, null); +// expect(furry.tlSeason1.rank, "z"); +// expect(furry.tlSeason1.percentileRank, "z"); +// expect(furry.tlSeason1.rating, -1); +// expect(furry.tlSeason1.decaying, false); +// expect(furry.tlSeason1.gamesPlayed, 0); +// expect(furry.tlSeason1.gamesWon, 0); +// expect(furry.tlSeason1.standing, -1); +// expect(furry.tlSeason1.standingLocal, -1); +// expect(furry.tlSeason1.apm, null); +// expect(furry.tlSeason1.pps, null); +// expect(furry.tlSeason1.vs, null); +// expect(furry.tlSeason1.nerdStats, null); +// expect(furry.tlSeason1.estTr, null); +// expect(furry.tlSeason1.esttracc, null); +// expect(furry.tlSeason1.playstyle, null); +// }); +// test("oskwarefan (anon account)", () async { +// TetrioPlayer oskwarefan = await teto.fetchPlayer("646cb8273e887a054d64febe"); +// expect(oskwarefan.userId, "646cb8273e887a054d64febe"); +// expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z")); +// expect(oskwarefan.role, "anon"); +// expect(oskwarefan.xp > 0, true); +// expect(oskwarefan.gamesPlayed > -1, true); +// expect(oskwarefan.gamesWon > -1, true); +// expect(oskwarefan.gameTime.isNegative, false); +// expect(oskwarefan.country, null); +// expect(oskwarefan.verified, false); +// expect(oskwarefan.connections, null); +// expect(oskwarefan.friendCount, 0); +// expect(oskwarefan.tlSeason1.glicko, null); +// expect(oskwarefan.tlSeason1.rank, "z"); +// expect(oskwarefan.tlSeason1.percentileRank, "z"); +// expect(oskwarefan.tlSeason1.rating, -1); +// expect(oskwarefan.tlSeason1.decaying, true); // ??? why true? +// expect(oskwarefan.tlSeason1.gamesPlayed, 0); +// expect(oskwarefan.tlSeason1.gamesWon, 0); +// expect(oskwarefan.tlSeason1.standing, -1); +// expect(oskwarefan.tlSeason1.standingLocal, -1); +// expect(oskwarefan.tlSeason1.apm, null); +// expect(oskwarefan.tlSeason1.pps, null); +// expect(oskwarefan.tlSeason1.vs, null); +// expect(oskwarefan.tlSeason1.nerdStats, null); +// expect(oskwarefan.tlSeason1.estTr, null); +// expect(oskwarefan.tlSeason1.esttracc, null); +// expect(oskwarefan.tlSeason1.playstyle, null); +// }); - test("not existing account", () async { - var future = teto.fetchPlayer("hasdbashdbs"); - await expectLater(future, throwsA(isA())); - }); - }); -} \ No newline at end of file +// test("not existing account", () async { +// var future = teto.fetchPlayer("hasdbashdbs"); +// await expectLater(future, throwsA(isA())); +// }); +// }); +// } \ No newline at end of file From cda6dc790c3edea9084514cca15c364fce68e669 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 10 Aug 2024 01:52:50 +0300 Subject: [PATCH 16/33] Redesign still WIP --- lib/views/main_view_tiles.dart | 645 +++++++++++++++++++++++++++------ res/icons/allspin.png | Bin 0 -> 1820 bytes res/icons/doublehole.png | Bin 0 -> 2133 bytes res/icons/expert.png | Bin 0 -> 1608 bytes res/icons/gravity.png | Bin 0 -> 1133 bytes res/icons/invisible.png | Bin 0 -> 1166 bytes res/icons/messy.png | Bin 0 -> 1008 bytes res/icons/nohold.png | Bin 0 -> 1446 bytes res/icons/volatile.png | Bin 0 -> 1441 bytes 9 files changed, 542 insertions(+), 103 deletions(-) create mode 100644 res/icons/allspin.png create mode 100644 res/icons/doublehole.png create mode 100644 res/icons/expert.png create mode 100644 res/icons/gravity.png create mode 100644 res/icons/invisible.png create mode 100644 res/icons/messy.png create mode 100644 res/icons/nohold.png create mode 100644 res/icons/volatile.png diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 41edc67..43bb7d7 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -8,10 +8,12 @@ import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.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'; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/tl_match_view.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; @@ -33,16 +35,16 @@ class MainView extends StatefulWidget { } enum Page {home, leaderboards, leagueAverages, calculator, settings} -enum Cards {overview, tetraLeague, quickPlay, quickPlayExpert, sprint, blitz, other} -enum CardMod {info, recent} +enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} +enum CardMod {info, recent, top, ex, exRecent, exTop} Map cardsTitles = { Cards.overview: "Overview", Cards.tetraLeague: t.tetraLeague, Cards.quickPlay: t.quickPlay, - Cards.quickPlayExpert: "${t.quickPlay} ${t.expert}", + //Cards.quickPlayExpert: "${t.quickPlay} ${t.expert}", Cards.sprint: t.sprint, Cards.blitz: t.blitz, - Cards.other: t.other + //Cards.other: t.other }; TetrioPlayer testPlayer = TetrioPlayer( @@ -177,6 +179,11 @@ class _MainState extends State with TickerProviderStateMixin { selectedIcon: Icon(Icons.calculate), label: Text('Calc'), ), + NavigationRailDestination( + icon: Icon(Icons.storage), + selectedIcon: Icon(Icons.storage), + label: Text('Saved Data'), + ), NavigationRailDestination( icon: Icon(Icons.settings), selectedIcon: Icon(Icons.settings), @@ -225,13 +232,13 @@ class _DestinationLeaderboardsState extends State { height: widget.constraints.maxHeight, child: Column( children: [ - Card( + const Card( child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Spacer(), - Text("Leaderboards", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), - const Spacer() + Spacer(), + Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Spacer() ], ), ), @@ -253,10 +260,12 @@ class _DestinationLeaderboardsState extends State { ), SizedBox( width: widget.constraints.maxWidth - 350 - 88, - child: Column( - children: [ - - ], + child: const Card( + child: Column( + children: [ + + ], + ), ), ), ], @@ -533,10 +542,10 @@ class _DestinationGraphsState extends State { ); } } - return Center(child: Column( + return const Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text("lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), + Text("lol", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), ], )); }, @@ -565,7 +574,258 @@ class DestinationHome extends StatefulWidget{ class _DestinationHomeState extends State { Cards rightCard = Cards.tetraLeague; + CardMod cardMod = CardMod.info; Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + late Map>> modeButtons; + late MapEntry closestAverageBlitz; + late bool blitzBetterThanClosestAverage; + late MapEntry closestAverageSprint; + late bool sprintBetterThanClosestAverage; + bool? sprintBetterThanRankAverage; + bool? blitzBetterThanRankAverage; + + Widget getOverviewCard(Summaries summaries){ + return const Column( + children: [ + Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Overview", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + ), + ), + ), + Card( + child: Padding( + padding: EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + child: Column( + children: [ + Row( + children: [ + Text("Title"), + Spacer(), + Text("Value"), + ], + ) + ], + ), + ), + ), + ] + ); + } + + Widget getTetraLeagueCard(TetraLeagueAlpha data){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + TetraLeagueThingy(league: testPlayer.tlSeason1!), + Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + NerdStatsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!), + GraphsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!, playstyle: testPlayer.tlSeason1!.playstyle!, apm: testPlayer.tlSeason1!.apm!, pps: testPlayer.tlSeason1!.pps!, vs: testPlayer.tlSeason1!.vs!) + ], + ); + } + + Widget getZenithCard(RecordSingle? record){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), + ], + ), + ), + ), + ), + ZenithThingy(zenith: record), + if (record != null) Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + if (record != null) NerdStatsThingy(nerdStats: record.aggregateStats.nerdStats), + if (record != null) GraphsThingy(nerdStats: record.aggregateStats.nerdStats, playstyle: record.aggregateStats.playstyle, apm: record.aggregateStats.apm, pps: record.aggregateStats.pps, vs: record.aggregateStats.vs) + ], + ); + } + + Widget getRecordCard(RecordSingle? record){ + return Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 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!.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: [ + RichText(text: TextSpan( + text: record!.gamemode == "40l" ? get40lTime(record.stats.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record.stats.score), + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + // 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!.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!.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!.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 != -1) TextSpan(text: "№${record.rank}", style: TextStyle(color: getColorOfRank(record.rank))), + if (record.rank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(record.timestamp)), + ] + ), + ) + ],), + ], + ), + ] + ); + } + + @override + initState(){ + // bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.score > blitzAverages[rank]! : null; + // 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; + // } + modeButtons = { + Cards.overview: [ + const ButtonSegment( + value: CardMod.info, + label: Text('General'), + ), + ], + Cards.tetraLeague: [ + const ButtonSegment( + value: CardMod.info, + label: Text('Standing'), + ), + const ButtonSegment( + value: CardMod.recent, + label: Text('Recent Matches'), + ), + ], + Cards.quickPlay: [ + const ButtonSegment( + value: CardMod.info, + label: Text('Normal'), + ), + const ButtonSegment( + value: CardMod.recent, + label: Text('Recent Normal'), + ), + const ButtonSegment( + value: CardMod.top, + label: Text('Top Normal'), + ), + const ButtonSegment( + value: CardMod.ex, + label: Text('Expert'), + ), + const ButtonSegment( + value: CardMod.exRecent, + label: Text('Recent Expert'), + ), + const ButtonSegment( + value: CardMod.exTop, + label: Text('Top Expert'), + ), + ], + Cards.blitz: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.recent, + label: Text('Recent'), + ), + const ButtonSegment( + value: CardMod.top, + label: Text('Top'), + ), + ], + Cards.sprint: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.recent, + label: Text('Recent'), + ), + const ButtonSegment( + value: CardMod.top, + label: Text('Top'), + ), + ] + }; + super.initState(); + } @override Widget build(BuildContext context) { @@ -586,6 +846,9 @@ class _DestinationHomeState extends State { NewUserThingy(player: snapshot.data!, showStateTimestamp: false, setState: setState), if (snapshot.data!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.badges), if (snapshot.data!.distinguishment != null) DistinguishmentThingy(snapshot.data!.distinguishment!), + if (snapshot.data!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.botmaster), + if (snapshot.data!.role == "banned") FakeDistinguishmentThingy(banned: true) + else if (snapshot.data!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), if (snapshot.data!.bio != null) Card( child: Column( children: [ @@ -612,7 +875,7 @@ class _DestinationHomeState extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - return Card(child: Center(child: CircularProgressIndicator())); + return const Card(child: Center(child: CircularProgressIndicator())); case ConnectionState.done: if (snapshot.hasData){ return NewsThingy(snapshot.data!); @@ -624,13 +887,30 @@ class _DestinationHomeState extends State { )); } } - return Text("what?"); + return const Text("what?"); } ), ) ], ); - }else{ + } + if (snapshot.hasError){ + if (snapshot.error.runtimeType == TetrioPlayerNotExist) { + return Card( + child: Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), + ), + ], + ) + ), + ); + } return Center(child: Column( mainAxisSize: MainAxisSize.min, @@ -644,6 +924,7 @@ class _DestinationHomeState extends State { ) ); } + return Text("huh?"); } }, )), @@ -655,64 +936,58 @@ class _DestinationHomeState extends State { SizedBox( height: widget.constraints.maxHeight - 64, child: SingleChildScrollView( - child: Column( - //mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.seasonStarts), - Center(child: Text(countdown(postSeasonLeft), textAlign: TextAlign.center, style: const TextStyle(fontSize: 32.0))), - ], - ), - ), - TetraLeagueThingy(league: testPlayer.tlSeason1!), - Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - NerdStatsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!) - ], + child: FutureBuilder( + future: teto.fetchSummaries(widget.searchFor), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return switch (rightCard){ + Cards.overview => getOverviewCard(snapshot.data!), + Cards.tetraLeague => getTetraLeagueCard(snapshot.data!.league), + Cards.quickPlay => getZenithCard(cardMod == CardMod.ex ? snapshot.data?.zenithEx : snapshot.data?.zenith), + Cards.sprint => getRecordCard(snapshot.data?.sprint), + Cards.blitz => getRecordCard(snapshot.data?.blitz), + }; + } + if (snapshot.hasError){ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), + ), + ], + ) + ); + } + return const Text("lol"); + } + } ), ), ), - SegmentedButton( + if (modeButtons[rightCard]!.length > 1) SegmentedButton( showSelectedIcon: false, - selected: {CardMod.info}, - segments: >[ - ButtonSegment( - value: CardMod.info, - label: Text('PB'), - //icon: Icon(Icons.calendar_view_day) - ), - ButtonSegment( - value: CardMod.recent, - label: Text('Recent'), - //icon: Icon(Icons.calendar_view_day) - ), - ] + selected: {cardMod}, + segments: modeButtons[rightCard]!, + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + }); + }, ), SegmentedButton( showSelectedIcon: false, segments: >[ - ButtonSegment( + const ButtonSegment( value: Cards.overview, //label: Text('Overview'), icon: Icon(Icons.calendar_view_day)), @@ -724,10 +999,6 @@ class _DestinationHomeState extends State { value: Cards.quickPlay, //label: Text('Quick Play'), icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - // ButtonSegment( - // value: Cards.quickPlayExpert, - // label: Text('QP Expert'), - // icon: Icon(Icons.calendar_today)), ButtonSegment( value: Cards.sprint, //label: Text('40 Lines'), @@ -736,17 +1007,14 @@ class _DestinationHomeState extends State { value: Cards.blitz, //label: Text('Blitz'), icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - // ButtonSegment( - // value: Cards.other, - // label: Text('Other'), - // icon: Icon(Icons.calendar_today)), ], selected: {rightCard}, onSelectionChanged: (Set newSelection) { setState(() { + cardMod = CardMod.info; rightCard = newSelection.first; });}) - ], + ] ), ), // SizedBox( @@ -928,7 +1196,7 @@ class NewsThingy extends StatelessWidget{ const Spacer() ] ), - if (news.news.isEmpty) Center(child: Text("Empty list")) + if (news.news.isEmpty) const Center(child: Text("Empty list")) else for (NewsEntry entry in news.news) getNewsTile(entry) ], ), @@ -1037,6 +1305,59 @@ class DistinguishmentThingy extends StatelessWidget{ } } +class FakeDistinguishmentThingy extends StatelessWidget{ + final bool banned; + final bool badStanding; + final bool bot; + final String? botMaintainers; + + FakeDistinguishmentThingy({super.key, this.banned = false, this.badStanding = false, this.bot = false, this.botMaintainers}); + + Color getCardTint(){ + if (banned) return Colors.red; + if (badStanding) return Colors.redAccent; + if (bot) return Color.fromARGB(255, 60, 93, 55); + return theme.colorScheme.surface; + } + + InlineSpan getDistinguishmentTitle() { + String text = ""; + if (banned) text = "banned"; + if (badStanding) text = "bad standing"; + if (bot) text = "bot account"; + return TextSpan(text: text.toUpperCase(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white)); + } + + String getDistinguishmentSubtitle(){ + if (banned) return "Bans are placed when TETR.IO rules or terms of service are broken"; + if (badStanding) return "One or more recent bans on record"; + if (bot) return "Operated by $botMaintainers"; + return ""; + } + + @override + Widget build(BuildContext context) { + return Card( + surfaceTintColor: getCardTint(), + child: Column( + children: [ + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [getDistinguishmentTitle()], + ), + ), + ), + Text(getDistinguishmentSubtitle(), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), + ], + ), + ); + } + +} + class BadgesThingy extends StatelessWidget{ final List badges; @@ -1258,7 +1579,7 @@ class NewUserThingy extends StatelessWidget { text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - TextSpan(text: "Level ${intf.format(player.level.floor())}", style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: TapGestureRecognizer()..onTap = (){ + TextSpan(text: "Level ${intf.format(player.level.floor())}", style: const TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: TapGestureRecognizer()..onTap = (){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( @@ -1453,21 +1774,21 @@ class TetraLeagueThingy extends StatelessWidget{ Expanded( child: Center( child: Table( - defaultColumnWidth:IntrinsicColumnWidth(), + defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ - Text("APM: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.apm) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + const Text("APM: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.apm) : "---", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), //Text(" APM", style: TextStyle(fontSize: 21)) ]), TableRow(children: [ - Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.pps) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + const Text("PPS: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.pps) : "---", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), //Text(" PPS", style: TextStyle(fontSize: 21)) ]), TableRow(children: [ - Text("VS: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.vs) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + const Text("VS: ", style: TextStyle(fontSize: 21)), + Text(league.apm != null ? f2.format(league.vs) : "---", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), // Text(" VS", style: TextStyle(fontSize: 21)) ]) ], @@ -1494,7 +1815,7 @@ class TetraLeagueThingy extends StatelessWidget{ ], annotations: [ GaugeAnnotation(widget: Container(child: - Text(percentage.format(league.winrate), textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + Text(percentage.format(league.winrate), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), angle: 90,positionFactor: 0.1 ), GaugeAnnotation(widget: Container(child: @@ -1509,22 +1830,22 @@ class TetraLeagueThingy extends StatelessWidget{ Expanded( child: Center( child: Table( - defaultColumnWidth:IntrinsicColumnWidth(), + defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ //Text("VS: ", style: TextStyle(fontSize: 21)), - Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" in BY", style: TextStyle(fontSize: 21)) + Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" in BY", style: TextStyle(fontSize: 21)) ]), TableRow(children: [ //Text("APM: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" Games", style: TextStyle(fontSize: 21)) + Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Games", style: TextStyle(fontSize: 21)) ]), TableRow(children: [ //Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" Won", style: TextStyle(fontSize: 21)) + Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Won", style: TextStyle(fontSize: 21)) ]) ], ), @@ -1574,10 +1895,10 @@ class NerdStatsThingy extends StatelessWidget{ RichText( textAlign: TextAlign.center, text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round"), + style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - TextSpan(text: "APP\n"), - TextSpan(text: f3.format(nerdStats.app), style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), + const TextSpan(text: "APP\n"), + TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), //TextSpan(text: "\nAPP"), ] ))), @@ -1604,10 +1925,10 @@ class NerdStatsThingy extends StatelessWidget{ RichText( textAlign: TextAlign.center, text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round"), + style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - TextSpan(text: "VS/APM\n"), - TextSpan(text: f3.format(nerdStats.vsapm), style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), + const TextSpan(text: "VS/APM\n"), + TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), ] ))), angle: 0,positionFactor: 0.5 @@ -1645,11 +1966,33 @@ class EstTrThingy extends StatelessWidget{ @override Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); + return const Card( + //child: , + ); } } +class GraphsThingy extends StatelessWidget{ + final double apm; + final double pps; + final double vs; + final NerdStats nerdStats; + final Playstyle playstyle; + + const GraphsThingy({super.key, required this.nerdStats, required this.playstyle, required this.apm, required this.pps, required this.vs}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Center(child: Graphs(apm, pps, vs, nerdStats, playstyle)), + ), + ); + } + +} + class GaugetThingy extends StatelessWidget{ final double value; final double min; @@ -1684,11 +2027,11 @@ class GaugetThingy extends StatelessWidget{ ], annotations: [ GaugeAnnotation(widget: Container(child: - Text(f.format(value), textAlign: TextAlign.center, style: TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + Text(f.format(value), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), angle: 90,positionFactor: 0.25 ), GaugeAnnotation(widget: Container(child: - Text(label, textAlign: TextAlign.center, style: TextStyle(height: .9))), + Text(label, textAlign: TextAlign.center, style: const TextStyle(height: .9))), angle: 270,positionFactor: 0.4 ) ], @@ -1697,6 +2040,102 @@ class GaugetThingy extends StatelessWidget{ ), ); } +} + +class ZenithThingy extends StatelessWidget{ + final RecordSingle? zenith; + + const ZenithThingy({super.key, required this.zenith}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + text: zenith != null ? "${f2.format(zenith!.stats.zenith!.altitude)} m" : "--- m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: zenith != null ? Colors.white : Colors.grey), + ), + ), + if (zenith != null) RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (zenith!.rank != -1) TextSpan(text: "№${zenith!.rank}", style: TextStyle(color: getColorOfRank(zenith!.rank))), + if (zenith!.rank != -1) const TextSpan(text: " • "), + if (zenith!.countryRank != -1) TextSpan(text: "№${zenith!.countryRank} local", style: TextStyle(color: getColorOfRank(zenith!.countryRank))), + if (zenith!.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(zenith!.timestamp)), + ] + ), + ), + ], + ), + if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) Container(width: 16.0), + if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) for (String mod in (zenith!.extras as ZenithExtras).mods) Image.asset("res/icons/${mod}.png", height: 64.0) + ], + ), + if (zenith != null) Row( + children: [ + Expanded( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + const Text("APM: ", style: TextStyle(fontSize: 21)), + Text(f2.format(zenith!.aggregateStats.apm), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + const Text("PPS: ", style: TextStyle(fontSize: 21)), + Text(f2.format(zenith!.aggregateStats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + const Text("VS: ", style: TextStyle(fontSize: 21)), + Text(f2.format(zenith!.aggregateStats.vs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + ), + Expanded( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text("${intf.format(zenith!.stats.kills)}", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" KO's", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text(f2.format(zenith!.stats.cps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" CPS", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text(f2.format(zenith!.stats.zenith!.peakrank), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Peak CPS", style: TextStyle(fontSize: 21)) + ]) + ], + ), + ), + ), + ], + ) + ] + ), + ) + ); + } } diff --git a/res/icons/allspin.png b/res/icons/allspin.png new file mode 100644 index 0000000000000000000000000000000000000000..f40c752dfe21fc7f297745d45acc44f2eb3a495d GIT binary patch literal 1820 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 literal 0 HcmV?d00001 diff --git a/res/icons/expert.png b/res/icons/expert.png new file mode 100644 index 0000000000000000000000000000000000000000..678ce4fba9a83c190a8abf7cc1fad5c169d7a61a GIT binary patch literal 1608 zcmeAS@N?(olHy`uVBq!ia0vp^+d-Iv8AzsYuwM(L7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1&4)6(a1=1VeZX1^9P$cg;p!YmVg8YIRto%%w*f*VA_d&3C1()t;E-apNP;gJh$?cvljv*Dd-rju~B;_m7_Rw8qs!GOZ%#o`{7d~S~0a-^zA^1wp0_%CzRCP3e)-eJ+AQ&{2T)O%8NX`g;u*Sk)3%j*Mx}4N9b{PJ zJV{QdarR9G&sWB=;aY9`ub2KlnzQnBNv$VPLz$VgXOMb=__?{OcV(I;h##_^v_#Qx z$6VfB?`9MppY?i5;jFI0_F1oke-sI{-oCkqOCo(%wt-~4@mqte1v}3**?$o&+%fm8 z_uY~sE3{eH`X39wa4cm7Tdd(L5A(e$eA_oay#FD0@2T|frs4MTb)x^2`xdg~rGIvN z%(v{J;_k(Bta^5xNS4pIBYg_!l#EN8D}T5e#Bj4cIBN5Irdp${TzdO?-Nomew{b}y zb}lpEJM|!}tm9Sh!^>}!pNjN6_l+&*W3YWay;Qr*Z1${IdJH+9m(tF;gj@LL`E7ik zwThdkfLZ#}vwLefe>4j0F(Kq$^{G58PbhP z*gm*8@^3iUnW<#M8h?^q%Tj+Fm&)Oed3v|` zRe|tswwi#}SXsLb8?q1b=ImHJN#{+~hkc9wGBEGSPq^M1edB}oMxHmfKKu{8Q@i27 zed!xt9|msY%uBr9{$HckL(A^*cb_-E3Pe->R!Z--s|o%2S82QV)9-grtm6+CymM&U z)L8j_(H!O*%GUzZxUFcZ`X?XOw>NKo)YJ48tZ@A%^)&WWh+AZT$c+tB(f!l5FsROZ z^r2|mt;i3bE&24*>+>=jr=EIvUv%YK*4+1tJ_LQ+eN9Y2>)QR);D~B&v+W|Qi&sTF z*VUU4+IuTz!k;ki@7u1jK8V^;CVr=UOTye~<`x+TZ`}*{lz1a@!Nc}tW*?UD1}}ZH zW&OFMc5dvy-PV5Ca&Sk(+;0obInH0QOM- z?cvKiM}S1k1rX~ti1i%A`UGPA0}}T-fkfO?5NiR5wWfqWQq$({=IQTDby}BKTVJ+a zx%5K`f5EQuog(wziTZ!vG5yd}0|&j=nxfyNzj|fWGR_L}1Qv@7p00i_>zopr0E>J- A)Bpeg literal 0 HcmV?d00001 diff --git a/res/icons/gravity.png b/res/icons/gravity.png new file mode 100644 index 0000000000000000000000000000000000000000..54fe2ffe3ab7ad08aab4aa0d73e2fcaeab263d8f GIT binary patch literal 1133 zcmeAS@N?(olHy`uVBq!ia0vp^+d-Iv8AzsYuwM(L7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`252Ka=y0_lx!w+#}MbSLXK&_VhoL4Lsu|5+;5P2T?O>#^(e?@wq^x%w;C z^V5bD3#k`X#RVVylClkD`z}4?d}2CPV%KY9t3@xx9T*sxojqL~Ln>~)y~~~@<0#^K zu{>L3PVu>$UctZP=N)8IR@wbH_#*$SJO}oJ20oYn*WXQeFDg0VWM7N3ij2}j0nT<0 z3ztKRLVOboI;bx+fkXdJit1!uBGJ)vbt={UY!CFz{!H09|3S-ixBb2h>U$i7Eg~MW z&N^_AOaB53Usy$>TET`x-!HJ7%cwrP!b#XdcvYU+mIUY0%{v!|-aW{*-sfG2wiS!| z9;3y}qwWM39MayL^`&FWteY2Fc(0y$=Fn|*fOGws_nXDONcMaSY>9WKI;nSv{h&S#!miq?%KbHv!?XsHt)aK zQl?k-*nin%-K!eqQ`dZ$>vp^HM%<6&-fuS!l^vF@J%0OPsPf*DKb-b|Vw7bgZ`AE@ z`?TTj2ebVWe?AEFZu<^o2lJM_O8os$T_NojLY% z_@nNyFWdb0Lv_@Ry`nknbJU|Q2#0O({lIi^G?Hz~jxt_b`S~$dO7i-m?KlF8h{*xUs z_ir09yyvd_cjdc7FH3D=#C`4SLifI_ZEN|ycY*H1+V{EM7587?0SZmq4ix&M`|$5O z7w?Mwt1k)Nd(XDD<@@f4zkD|}_T0bux}4Yj;orRU{gbV@-j_H0zWX$=V!!si_dEIz iiApRuac_0`*7(3xejC2{=ZOGwB7>)^pUXO@geCx>+dv5b literal 0 HcmV?d00001 diff --git a/res/icons/invisible.png b/res/icons/invisible.png new file mode 100644 index 0000000000000000000000000000000000000000..13409410870b77848e5578c8ccda06f1cea53780 GIT binary patch literal 1166 zcmeAS@N?(olHy`uVBq!ia0vp^+d-Iv8AzsYuwM(L7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0F1o(uw0_lx!x6vQ{{yQlEXt#PvkY6x^m6L68!3 z$NBN!e|sJMw;odq{BqJnyzi}-?EEUg$9d_7(T3h7JRYaomgG>IurT{}^X19n<>~Hh z@n;^cc;Is9^Fux!cNXwew|O@V&)rx-)nXlo$d8Yzy0oKLFy`L*|T>2JHq`c=F}?5|IK@CTB7pX z&E?8#v5$uz=Lmk?>8kX>BHG0^Pf*_G+`g^nZ@g2rXOKOZmOXFN-AlI(8P-YE8lStF z_p_ml;-UGBzXmr7E122$Y}VazuLz));T#DmNzdHZCdy=#rgEfwrP`j zb=9S#{LHohCAe=Um7i^l?Y?VNq5NXsqXX;yyyMGvxjS9vM(Pb;@7L3&owJmKTMUGvg<}!Ki`h7BM%DAa(F8y+`TOD``y2XJAdz%h&ej(*Ui7G zN(G|fuhPrEpZ-wJxNYvPn|I&V_pja&tiOBr-GZqOuTsjli?0*%s(tfr+r#-XZv_5$ zrzh`zyMGQ}8PC6CDapI9IRb&wM`T=H6M+ zYTv!`X3jf2H{rqK%ilikd@G!0DqfPajmzLS>SdycB c_5WYS5bsAnU2ON)1M@F~r>mdKI;Vst0H+>jWB>pF literal 0 HcmV?d00001 diff --git a/res/icons/messy.png b/res/icons/messy.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc79640a95620d6e98a626012a7219c35d68403 GIT binary patch literal 1008 zcmeAS@N?(olHy`uVBq!ia0vp^+d-Iv8AzsYuwM(L7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1=1o(uw0_lx!x6udPHmy(x+NWF+sLR;dLE3wBb#S& zdXH)8S9PiG85P2_-pX6W@K0OBz`*p`)5S5Q;?~={;d#=I0xb{aq7GW7M^Cx>|8M`2 z-9H@7cwQXj7CWjbYEat5&$7|F`(=RonMzkSj#Ot2X{Vb4r`iI{S~e+8;a!+Rg3zBg z9u_W#6ovRE7Id^YtH>xl6yR*fDx`9VefgOaDTm=`#-Y0Z50yXY$M26h*Zex?Ug*^y z2V?DO*R4vDib>zRdiS2H+Q9llCOgihZU2~LbeO&AHorVao_&qNj=vuoAAG|hppj=? zqp{r{l}}g{A%aJLzZC)+y%|MSF`N6dw1)^Z9C@s?kF($bMF=F`H%0!pYQ)S zol`t(*6o7E%#zpQ$Lh+pOIho_N6JnyJeaHDAb96ollRw~1(ulxR`{Ws2jaOSpo=+{r2Iq7Fr zZ#ArE4BPN*&e@jrmUYGtv;Cg>{FrsvN_gKhp7TJVTOgr7K0j_X8wl_FR&%Chy{#Eg z$p7-K?ya2h)%UN>+w1$|mh#STWqF6L$KBe>UIA1VvwrWR*&nu=*Q@PODvs0s{#iQL X>F@{Zk5LA|+`{1L>gTe~DWM4f!O9U0 literal 0 HcmV?d00001 diff --git a/res/icons/nohold.png b/res/icons/nohold.png new file mode 100644 index 0000000000000000000000000000000000000000..38811ec4986fd1ad8e953cb37c5e230d3ad1205c GIT binary patch literal 1446 zcmeAS@N?(olHy`uVBq!ia0vp^+d-Iv8AzsYuwM(L7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1Y4e$wZ1=1VeZW|#eDPjT((8GQuL4LsuR#nE|Kk%q9vCkJ2d!}g{{55uE zT1BH_z%3Vhi{jGw*Gg^KGcL7sbMj4|B`g0&@9nR359@sjmfeXsI3X(3y;Dj;c;U$; z9aB%I%+q}=Qo94}7#NuUdb&7F-;pR?4r^ov>X{hxk& zyn;k%`CQ{v`NWm?v@1+5tyr2jeeHp5Q;iffr6+Atqz5r?t`MK(!`2gCer8kVM*4a|=mwQv+vu?zA7icrtedB ze}QuPkFJ;R$~$tFN=oMUO21C1-tqXMTZ6S+9t)6h?B$(m@v!x~eAY(qU#lf6v-3c2 z@UHLC@*VOk?(McX(0h5;|LbW#bg%vv{`IhJa`c=R`d4>z{ViC0Kem1U@~Z!i_ICi@7cZ_<`p;JZn||pP?^Qf zol7!f+v|2beD??*kxxcfku_OBnffwt7# zd7dEr=fG^;wKjLGH%Qh^E?d39vd@3T?(MIp%FV{#yDzx{&11CR{(-sueTfH1r?t+X z1y63rf`tF?JIL1nw1nr!%pdH*!gmeW_g!G#x^4E#(hYYXZa@0P>c+hf)@NSyGq0NZ z(eHcdkBSSit9#1-t2b{{cy#ud+(grZ5f+~}ZJfS>QJ#BE!W+wIA-iusbUEd#wWhc9 z+ph6GbbQ`J)rZOb-zI(7s9xv!V@LA_m6|(0BZTbU-O%BbFQ0WRc8yxiom%PgZ?irW zx)im literal 0 HcmV?d00001 diff --git a/res/icons/volatile.png b/res/icons/volatile.png new file mode 100644 index 0000000000000000000000000000000000000000..aef5dcd2a9282f3a651d517466aadfbe001f2407 GIT binary patch literal 1441 zcmeAS@N?(olHy`uVBq!ia0vp^+d-Iv8AzsYuwM(L7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O@EW3h)VW1&VKcyKN++|5nGMfuRsq666=m@ZZXKnfjd5?h(QdPpSmuY1;3f za7m)={`~6|+pDgYE-AJ!{cadPOG*C1Eipk`zLv9{jVpgmJ@(b*#f-^%v3`FxbZ=s+ zf3nBtAm^@v4^gr_ub*Xpa$ZI#oYL)+?x;F|M`t-=RyJ?rra6jGJb=V^5*e;zq`}uD}254}aObd;N*HnekEA zrdu6hIDcZ_-<;PiYAp*qa z?w8#g*u64480R&gE%$mU#rNj|bLU)})F6H1DPO+* zp2bbK`QM*-UgyZRqqjDallKIJ`Np5FoIq}8uh1-I;XTT$tEbs~=-ZQbf?@l{_^HL0 zef!TYRLk$=DUhDMebyf19S_U4{r~Xo@ZTJp>0j7q`^(!+Si;PF=eXU{lh(Wp_G{yA z{CFN;d!^Li0B_Fi%$+;7Sod~MJ9oa#c4sv&+m8O;J9Al;_bgr}?L2q--hfu#x&3Fq zm)^MW^Umo{Uwd!He*G}dbo1(eb822E?0!&x;_`)q-wv)naWR_xHs}A?KA9Z-Jwms6 zr|)$<%Kf3+XnVltw&#=ja^Ef4cctd4^qKu%JhRtt&)ZjJdD5^X;(J@2e^m6HUF9jV zxxvT37jI+N_$T-3$D22QK0LR)!TP}R2H%eVs<$0`*&qCyo|FG-Pr-+Mzw5HnrZ>L+ z{5IfW>#X~>>1pCk{L5-S$8p(3x9gsL@ao)`8^&I%=Kp%Qw`pnIrvrcArH7R{{@7=7 z%T#-}N=@A2wb3_SAKGUuiPhe%6SFw-X6wVy8+AXtM7PP;#m_IfctVe_KA|GwP5SSL zzjk)_u>?t~b7}y`8sBKCeLk#*L{D(tY^5 zU)O)AI{Ib9j(_aOc3ZtVE|v2?+~a)io2~-c)THO#8`nnNoRoFQTqV=~owT7d-#gQu&X%Q~loCIGI$21)<` literal 0 HcmV?d00001 From b3fd96e58ce494f54d8d0fc3ff392c173ff3ea49 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 13 Aug 2024 02:07:59 +0300 Subject: [PATCH 17/33] ugh --- lib/views/main_view.dart | 1 + lib/views/main_view_tiles.dart | 498 +++++++++++++++++++++------------ 2 files changed, 327 insertions(+), 172 deletions(-) diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index b590e9a..8cc1a14 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -573,6 +573,7 @@ class _MainState extends State with TickerProviderStateMixin { break; default: errText = snapshot.error.toString(); + subText = snapshot.stackTrace.toString(); } return Center(child: Column( diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 43bb7d7..fe9e537 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -656,6 +656,59 @@ class _DestinationHomeState extends State { ); } + Widget getRecentTLrecords(String searchFor, BoxConstraints constraints){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + ), + ), + ), + Card( + child: FutureBuilder( + future: teto.fetchTLStream(searchFor), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return SizedBox(height: constraints.maxHeight, child: _TLRecords(userID: searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); + } + if (snapshot.hasError){ + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + return Text("what?"); + }, + ), + ), + ], + ); + } + Widget getZenithCard(RecordSingle? record){ return Column( children: [ @@ -691,51 +744,119 @@ class _DestinationHomeState extends State { ); } - Widget getRecordCard(RecordSingle? record){ + Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ return Column( children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - // 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!.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, + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(switch(record!.gamemode){ + "40l" => t.sprint, + "blitz" => t.blitz, + "5mblast" => "5,000,000 Blast", + _ => record.gamemode + }, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)) + ], + ), + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + child: Row( mainAxisSize: MainAxisSize.min, children: [ - RichText(text: TextSpan( - text: record!.gamemode == "40l" ? get40lTime(record.stats.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record.stats.score), - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText(text: TextSpan( + text: switch(record.gamemode){ + "40l" => get40lTime(record.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record.stats.score), + "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), + _ => record.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + 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: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), + _ => record.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), + "blitz" => readableIntDifference(record.stats.score, closestAverage.value), + _ => record.stats.score.toString() + }, verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record.rank != -1) TextSpan(text: "№ ${intf.format(record.rank)}", style: TextStyle(color: getColorOfRank(record.rank))), + if (record.rank != -1) const TextSpan(text: " • "), + if (record.countryRank != -1) TextSpan(text: "№ ${intf.format(record.countryRank)} local", style: TextStyle(color: getColorOfRank(record.countryRank))), + if (record.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(record.timestamp)), + ] + ), ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - // 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!.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!.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!.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 != -1) TextSpan(text: "№${record.rank}", style: TextStyle(color: getColorOfRank(record.rank))), - if (record.rank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(record.timestamp)), - ] - ), - ) - ],), - ], + ],), + Spacer(), + Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => record.stats.piecesPlaced.toString(), + "blitz" => record.stats.level.toString(), + "5mblast" => NumberFormat.decimalPattern().format(record.stats.spp), + _ => "What if " + }, textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " Pieces", + "blitz" => " Level", + "5mblast" => " SPP", + _ => " i wanted to" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" PPS", textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => f2.format(record.stats.kpp), + "blitz" => f2.format(record.stats.spp), + "5mblast" => record.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " KPP", + "blitz" => " SPP", + "5mblast" => " Pieces", + _ => " no" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ], + ), + ), ), ] ); @@ -946,12 +1067,26 @@ class _DestinationHomeState extends State { return const Center(child: CircularProgressIndicator()); case ConnectionState.done: if (snapshot.hasData){ + blitzBetterThanRankAverage = (snapshot.data!.league.rank != "z" && snapshot.data!.blitz != null) ? snapshot.data!.blitz!.stats.score > blitzAverages[snapshot.data!.league.rank]! : null; + sprintBetterThanRankAverage = (snapshot.data!.league.rank != "z" && snapshot.data!.sprint != null) ? snapshot.data!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.league.rank]! : null; + if (snapshot.data!.sprint != null) { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.sprint!.stats.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = snapshot.data!.sprint!.stats.finalTime < closestAverageSprint.value; + } + if (snapshot.data!.blitz != null){ + closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.blitz!.stats.score).abs() < (b -snapshot.data!.blitz!.stats.score).abs() ? a : b)); + blitzBetterThanClosestAverage = snapshot.data!.blitz!.stats.score > closestAverageBlitz.value; + } return switch (rightCard){ Cards.overview => getOverviewCard(snapshot.data!), - Cards.tetraLeague => getTetraLeagueCard(snapshot.data!.league), + Cards.tetraLeague => switch (cardMod){ + CardMod.info => getTetraLeagueCard(snapshot.data!.league), + CardMod.recent => getRecentTLrecords(widget.searchFor, widget.constraints), + _ => Center(child: Text("huh?")) + }, Cards.quickPlay => getZenithCard(cardMod == CardMod.ex ? snapshot.data?.zenithEx : snapshot.data?.zenith), - Cards.sprint => getRecordCard(snapshot.data?.sprint), - Cards.blitz => getRecordCard(snapshot.data?.blitz), + Cards.sprint => getRecordCard(snapshot.data?.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.league.rank), + Cards.blitz => getRecordCard(snapshot.data?.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.league.rank), }; } if (snapshot.hasError){ @@ -1339,19 +1474,30 @@ class FakeDistinguishmentThingy extends StatelessWidget{ Widget build(BuildContext context) { return Card( surfaceTintColor: getCardTint(), - child: Column( - children: [ - Center( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [getDistinguishmentTitle()], + child: Container( + decoration: banned ? BoxDecoration( + gradient: LinearGradient( + colors: [Colors.transparent, const Color.fromARGB(171, 244, 67, 54), Color.fromARGB(171, 244, 67, 54)], + stops: [0.1, 0.9, 0.01], + tileMode: TileMode.mirror, + begin: Alignment.topLeft, + end: AlignmentDirectional(-0.95, -0.95) + ) + ) : null, + child: Column( + children: [ + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [getDistinguishmentTitle()], + ), ), ), - ), - Text(getDistinguishmentSubtitle(), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), - ], + Text(getDistinguishmentSubtitle(), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), + ], + ), ), ); } @@ -1798,33 +1944,34 @@ class TetraLeagueThingy extends StatelessWidget{ SizedBox( height: 128.0, width: 128.0, - child: SfRadialGauge( - axes: [ - RadialAxis( - // startAngle: 180, - // endAngle: 0, - minimum: 0.4, - maximum: 0.6, - //radiusFactor: 1.5, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: league.winrate, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - Text(percentage.format(league.winrate), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), - angle: 90,positionFactor: 0.1 - ), - GaugeAnnotation(widget: Container(child: - Text(t.statCellNum.winrate, textAlign: TextAlign.center)), - angle: 270,positionFactor: 0.4 - ) - ], - ) - ] + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: SfRadialGauge( + backgroundColor: Colors.black, + axes: [ + RadialAxis( + minimum: 0.4, + maximum: 0.6, + radiusFactor: 1.01, + showTicks: true, + showLabels: false, + interval: 0.1, + ranges:[ + GaugeRange(startValue: 0, endValue: league.winrate, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + Text(percentage.format(league.winrate), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + angle: 90,positionFactor: 0.1 + ), + GaugeAnnotation(widget: Container(child: + Text(t.statCellNum.winrate, textAlign: TextAlign.center)), + angle: 270,positionFactor: 0.4 + ) + ], + ) + ] + ), ), ), Expanded( @@ -1875,71 +2022,76 @@ class NerdStatsThingy extends StatelessWidget{ SizedBox( height: 256.0, width: 256.0, - child: SfRadialGauge( - axes: [ - RadialAxis( - startAngle: 120, - endAngle: 240, - minimum: 0.0, - maximum: 1.0, - //radiusFactor: 1.5, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.app, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "APP\n"), - TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), - //TextSpan(text: "\nAPP"), - ] - ))), - angle: 180,positionFactor: 0.5 - ), - ], - ), - RadialAxis( - startAngle: 300, - endAngle: 60, - isInversed: true, - minimum: 1.8, - maximum: 2.4, - //radiusFactor: 1.5, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.vsapm, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "VS/APM\n"), - TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold)), - ] - ))), - angle: 0,positionFactor: 0.5 - ) - ], - ) - ] + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: SfRadialGauge( + backgroundColor: Colors.black, + axes: [ + RadialAxis( + startAngle: 200, + endAngle: 340, + minimum: 0.0, + maximum: 1.0, + radiusFactor: 1.01, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: nerdStats.app, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + const TextSpan(text: "APP\n"), + TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), + //TextSpan(text: "\nAPP"), + ] + ))), + angle: 270,positionFactor: 0.5 + ), + ], + ), + RadialAxis( + startAngle: 20, + endAngle: 160, + isInversed: true, + minimum: 1.8, + maximum: 2.4, + radiusFactor: 1.01, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: nerdStats.vsapm, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + const TextSpan(text: "VS/APM\n"), + TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), + ] + ))), + angle: 90,positionFactor: 0.5 + ) + ], + ) + ] + ), ), ), Expanded( child: Wrap( + spacing: 10, children: [ GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3), GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3), @@ -2007,36 +2159,38 @@ class GaugetThingy extends StatelessWidget{ @override Widget build(BuildContext context) { NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits); - return SizedBox( - height: sideSize, - width: sideSize, - child: SfRadialGauge( - axes: [ - RadialAxis( - // startAngle: 180, - // endAngle: 0, - minimum: min, - maximum: max, - //radiusFactor: 1.5, - showTicks: true, - showLabels: false, - interval: tickInterval, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: value, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - Text(f.format(value), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), - angle: 90,positionFactor: 0.25 - ), - GaugeAnnotation(widget: Container(child: - Text(label, textAlign: TextAlign.center, style: const TextStyle(height: .9))), - angle: 270,positionFactor: 0.4 - ) - ], - ) - ] + return ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: SizedBox( + height: sideSize, + width: sideSize, + child: SfRadialGauge( + backgroundColor: Colors.black, + axes: [ + RadialAxis( + radiusFactor: 1.01, + minimum: min, + maximum: max, + showTicks: true, + showLabels: false, + interval: tickInterval, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: value, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + Text(f.format(value), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), + angle: 90,positionFactor: 0.10 + ), + GaugeAnnotation(widget: Container(child: + Text(label, textAlign: TextAlign.center, style: const TextStyle(height: .9))), + angle: 270,positionFactor: 0.4 + ) + ], + ) + ] + ), ), ); } From 6d950d30da3976e114abb8a2237710da0be3e485 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 14 Aug 2024 01:45:28 +0300 Subject: [PATCH 18/33] redesign 10% ready (but only for desktop :tf:) --- lib/data_objects/tetrio.dart | 12 +- .../tetrio_multiplayer_replay.dart | 2 +- lib/views/main_view.dart | 4 +- lib/views/main_view_tiles.dart | 369 ++++++++++++++---- lib/widgets/lineclears_thingy.dart | 8 +- lib/widgets/recent_sp_games.dart | 2 +- lib/widgets/singleplayer_record.dart | 2 +- lib/widgets/sp_trailing_stats.dart | 31 +- lib/widgets/zenith_thingy.dart | 27 +- 9 files changed, 366 insertions(+), 91 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 316d341..d8f3658 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -546,6 +546,8 @@ class Clears { late int tSpinMiniZeros; late int tSpinMiniSingles; late int tSpinMiniDoubles; + late int tSpinMiniTriples; + late int tSpinMiniQuads; Clears( {required this.singles, @@ -562,7 +564,9 @@ class Clears { required this.tSpinQuads, required this.tSpinMiniZeros, required this.tSpinMiniSingles, - required this.tSpinMiniDoubles}); + required this.tSpinMiniDoubles, + required this.tSpinMiniTriples, + required this.tSpinMiniQuads}); Clears.fromJson(Map json) { singles = json['singles']; @@ -576,7 +580,9 @@ class Clears { tSpinSingles = json['tspinsingles']; tSpinMiniDoubles = json['minitspindoubles']; tSpinDoubles = json['tspindoubles']; + tSpinMiniTriples = json['minitspintriples']??0; tSpinTriples = json['tspintriples']; + tSpinMiniQuads = json['minitspinquads']??0; tSpinQuads = json['tspinquads']; tSpinPentas = json['tspinpentas']??0; allClears = json['allclear']; @@ -598,7 +604,9 @@ class Clears { tSpinQuads: tSpinQuads + other.tSpinQuads, tSpinMiniZeros: tSpinMiniZeros + other.tSpinMiniZeros, tSpinMiniSingles: tSpinMiniSingles + other.tSpinMiniSingles, - tSpinMiniDoubles: tSpinMiniDoubles + other.tSpinMiniDoubles + tSpinMiniDoubles: tSpinMiniDoubles + other.tSpinMiniDoubles, + tSpinMiniTriples: tSpinMiniTriples + other.tSpinMiniTriples, + tSpinMiniQuads: tSpinMiniQuads + other.tSpinMiniQuads ); } diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 9b2a5ac..e4dd836 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -134,7 +134,7 @@ class ReplayStats{ topSpike = 0; tspins = 0; roundLength = 0.0; - clears = Clears(singles: 0, doubles: 0, triples: 0, quads: 0, pentas: 0, allClears: 0, tSpinZeros: 0, tSpinSingles: 0, tSpinDoubles: 0, tSpinTriples: 0, tSpinPentas: 0, tSpinQuads: 0, tSpinMiniZeros: 0, tSpinMiniSingles: 0, tSpinMiniDoubles: 0); + clears = Clears(singles: 0, doubles: 0, triples: 0, quads: 0, pentas: 0, allClears: 0, tSpinZeros: 0, tSpinSingles: 0, tSpinDoubles: 0, tSpinTriples: 0, tSpinPentas: 0, tSpinQuads: 0, tSpinMiniZeros: 0, tSpinMiniSingles: 0, tSpinMiniDoubles: 0, tSpinMiniTriples: 0, tSpinMiniQuads: 0); garbage = Garbage(sent: 0, recived: 0, attack: 0, cleared: 0); finesse = Finesse(combo: 0, faults: 0, perfectPieces: 0); } diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 8cc1a14..cb22558 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1191,7 +1191,7 @@ class _TwoRecordsThingy extends StatelessWidget { 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].stats, sprintStream.records[i].gamemode) + trailing: SpTrailingStats(sprintStream.records[i], sprintStream.records[i].gamemode) ) ], ), @@ -1277,7 +1277,7 @@ class _TwoRecordsThingy extends StatelessWidget { 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].stats, blitzStream.records[i].gamemode) + trailing: SpTrailingStats(blitzStream.records[i], blitzStream.records[i].gamemode) ) ], ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index fe9e537..908d6bb 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -12,9 +12,13 @@ import 'package:tetra_stats/utils/colors_functions.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/singleplayer_record_view.dart'; import 'package:tetra_stats/views/tl_match_view.dart'; +import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/graphs.dart'; +import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; +import 'package:tetra_stats/widgets/sp_trailing_stats.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'; @@ -577,9 +581,9 @@ class _DestinationHomeState extends State { CardMod cardMod = CardMod.info; Duration postSeasonLeft = seasonStart.difference(DateTime.now()); late Map>> modeButtons; - late MapEntry closestAverageBlitz; + late MapEntry? closestAverageBlitz; late bool blitzBetterThanClosestAverage; - late MapEntry closestAverageSprint; + late MapEntry? closestAverageSprint; late bool sprintBetterThanClosestAverage; bool? sprintBetterThanRankAverage; bool? blitzBetterThanRankAverage; @@ -656,7 +660,7 @@ class _DestinationHomeState extends State { ); } - Widget getRecentTLrecords(String searchFor, BoxConstraints constraints){ + Widget getListOfRecords(String stream, bool isTop, BoxConstraints constraints){ return Column( children: [ Card( @@ -674,8 +678,9 @@ class _DestinationHomeState extends State { ), ), Card( - child: FutureBuilder( - future: teto.fetchTLStream(searchFor), + clipBehavior: Clip.antiAlias, + child: FutureBuilder( + future: teto.fetchStream(widget.searchFor, stream), builder: (context, snapshot) { switch (snapshot.connectionState){ case ConnectionState.none: @@ -684,7 +689,90 @@ class _DestinationHomeState extends State { return const Center(child: CircularProgressIndicator()); case ConnectionState.done: if (snapshot.hasData){ - return SizedBox(height: constraints.maxHeight, child: _TLRecords(userID: searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); + return Column( + children: [ + for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + isTop ? "#${i+1}" : switch (snapshot.data!.records[i].gamemode){ + "40l" => "40L", + "blitz" => "BLZ", + "5mblast" => "5MB", + "zenith" => "QP", + "zenithex" => "QPE", + String() => "huh", + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ); + } + if (snapshot.hasError){ + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + return Text("what?"); + }, + ), + ), + ], + ); + } + + Widget getRecentTLrecords(BoxConstraints constraints){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: FutureBuilder( + future: teto.fetchTLStream(widget.searchFor), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return SizedBox(height: constraints.maxHeight - 145, child: _TLRecords(userID: widget.searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); } if (snapshot.hasError){ return Center( @@ -728,6 +816,78 @@ class _DestinationHomeState extends State { ), ), ZenithThingy(zenith: record), + if (record != null) Row( + children: [ + Expanded( + child: Card( + child: Column( + children: [ + FinesseThingy(record.stats.finesse, record.stats.finessePercentage), + LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins, showMoreClears: true), + if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") + ], + ), + ), + ), + Expanded( + child: Card( + child: SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("T", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text(getMoreNormalTime(record.stats.finalTime), style: TextStyle( + shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + SizedBox( + width: 300.0, + child: Table( + columnWidths: const { + 0: FixedColumnWidth(36) + }, + children: [ + const TableRow( + children: [ + Text("Floor"), + Text("Split", textAlign: TextAlign.right), + Text("Total", textAlign: TextAlign.right), + ] + ), + for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( + children: [ + Text((i+1).toString()), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), + ] + ) + ], + ), + ), + ], + ), + ), + ), + ), + ], + ), if (record != null) Card( child: Row( mainAxisSize: MainAxisSize.min, @@ -745,6 +905,11 @@ class _DestinationHomeState extends State { } Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ + if (record == null) { + return Card( + child: Center(child: Text("No record", style: const TextStyle(fontSize: 42))), + ); + } return Column( children: [ Card( @@ -755,7 +920,7 @@ class _DestinationHomeState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(switch(record!.gamemode){ + Text(switch(record.gamemode){ "40l" => t.sprint, "blitz" => t.blitz, "5mblast" => "5,000,000 Blast", @@ -767,57 +932,62 @@ class _DestinationHomeState extends State { ), ), Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RichText(text: TextSpan( - text: switch(record.gamemode){ - "40l" => get40lTime(record.stats.finalTime.inMicroseconds), - "blitz" => NumberFormat.decimalPattern().format(record.stats.score), - "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), - _ => record.stats.score.toString() - }, - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText(text: TextSpan( + text: switch(record.gamemode){ + "40l" => get40lTime(record.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record.stats.score), + "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), + _ => record.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + 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: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), + _ => record.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), + "blitz" => readableIntDifference(record.stats.score, closestAverage.value), + _ => record.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record.rank != -1) TextSpan(text: "№ ${intf.format(record.rank)}", style: TextStyle(color: getColorOfRank(record.rank))), + if (record.rank != -1) const TextSpan(text: " • "), + if (record.countryRank != -1) TextSpan(text: "№ ${intf.format(record.countryRank)} local", style: TextStyle(color: getColorOfRank(record.countryRank))), + if (record.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(record.timestamp)), + ] ), ), - 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: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), - "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), - _ => record.stats.score.toString() - }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), - "blitz" => readableIntDifference(record.stats.score, closestAverage.value), - _ => record.stats.score.toString() - }, verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )), - if (record.rank != -1) TextSpan(text: "№ ${intf.format(record.rank)}", style: TextStyle(color: getColorOfRank(record.rank))), - if (record.rank != -1) const TextSpan(text: " • "), - if (record.countryRank != -1) TextSpan(text: "№ ${intf.format(record.countryRank)} local", style: TextStyle(color: getColorOfRank(record.countryRank))), - if (record.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(record.timestamp)), - ] - ), - ), - ],), - Spacer(), - Table( + ], + ), + ], + ), + Row( + children: [ + Expanded( + child: Table( defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ @@ -854,25 +1024,58 @@ class _DestinationHomeState extends State { ]) ], ), + ), + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" Key presses", textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(" KPS", textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => " ", + "blitz" => record.stats.piecesPlaced.toString(), + "5mblast" => record.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " ", + "blitz" => " Pieces", + "5mblast" => " Pieces", + _ => " no" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + ], + ) + ], + ), + ), + Card( + child: Center( + child: Column( + children: [ + FinesseThingy(record.stats.finesse, record.stats.finessePercentage), + LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins), + if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") ], ), ), - ), + ) ] ); } @override initState(){ - // bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.score > blitzAverages[rank]! : null; - // 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; - // } modeButtons = { Cards.overview: [ const ButtonSegment( @@ -1071,22 +1274,40 @@ class _DestinationHomeState extends State { sprintBetterThanRankAverage = (snapshot.data!.league.rank != "z" && snapshot.data!.sprint != null) ? snapshot.data!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.league.rank]! : null; if (snapshot.data!.sprint != null) { closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.sprint!.stats.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = snapshot.data!.sprint!.stats.finalTime < closestAverageSprint.value; + sprintBetterThanClosestAverage = snapshot.data!.sprint!.stats.finalTime < closestAverageSprint!.value; } if (snapshot.data!.blitz != null){ closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.blitz!.stats.score).abs() < (b -snapshot.data!.blitz!.stats.score).abs() ? a : b)); - blitzBetterThanClosestAverage = snapshot.data!.blitz!.stats.score > closestAverageBlitz.value; + blitzBetterThanClosestAverage = snapshot.data!.blitz!.stats.score > closestAverageBlitz!.value; } return switch (rightCard){ Cards.overview => getOverviewCard(snapshot.data!), Cards.tetraLeague => switch (cardMod){ CardMod.info => getTetraLeagueCard(snapshot.data!.league), - CardMod.recent => getRecentTLrecords(widget.searchFor, widget.constraints), + CardMod.recent => getRecentTLrecords(widget.constraints), + _ => Center(child: Text("huh?")) + }, + Cards.quickPlay => switch (cardMod){ + CardMod.info => getZenithCard(snapshot.data?.zenith), + CardMod.recent => getListOfRecords("zenith/recent", false, widget.constraints), + CardMod.top => getListOfRecords("zenith/top", true, widget.constraints), + CardMod.ex => getZenithCard(snapshot.data?.zenithEx), + CardMod.exRecent => getListOfRecords("zenithex/recent", false, widget.constraints), + CardMod.exTop => getListOfRecords("zenithex/top", true, widget.constraints), + _ => Center(child: Text("huh?")) + }, + Cards.sprint => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.league.rank), + CardMod.recent => getListOfRecords("40l/recent", false, widget.constraints), + CardMod.top => getListOfRecords("40l/top", true, widget.constraints), + _ => Center(child: Text("huh?")) + }, + Cards.blitz => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.league.rank), + CardMod.recent => getListOfRecords("blitz/recent", false, widget.constraints), + CardMod.top => getListOfRecords("blitz/top", true, widget.constraints), _ => Center(child: Text("huh?")) }, - Cards.quickPlay => getZenithCard(cardMod == CardMod.ex ? snapshot.data?.zenithEx : snapshot.data?.zenith), - Cards.sprint => getRecordCard(snapshot.data?.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.league.rank), - Cards.blitz => getRecordCard(snapshot.data?.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.league.rank), }; } if (snapshot.hasError){ @@ -2225,9 +2446,9 @@ class ZenithThingy extends StatelessWidget{ text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (zenith!.rank != -1) TextSpan(text: "№${zenith!.rank}", style: TextStyle(color: getColorOfRank(zenith!.rank))), + if (zenith!.rank != -1) TextSpan(text: "№ ${intf.format(zenith!.rank)}", style: TextStyle(color: getColorOfRank(zenith!.rank))), if (zenith!.rank != -1) const TextSpan(text: " • "), - if (zenith!.countryRank != -1) TextSpan(text: "№${zenith!.countryRank} local", style: TextStyle(color: getColorOfRank(zenith!.countryRank))), + if (zenith!.countryRank != -1) TextSpan(text: "№ ${intf.format(zenith!.countryRank)} local", style: TextStyle(color: getColorOfRank(zenith!.countryRank))), if (zenith!.countryRank != -1) const TextSpan(text: " • "), TextSpan(text: timestamp(zenith!.timestamp)), ] diff --git a/lib/widgets/lineclears_thingy.dart b/lib/widgets/lineclears_thingy.dart index 2536891..78745db 100644 --- a/lib/widgets/lineclears_thingy.dart +++ b/lib/widgets/lineclears_thingy.dart @@ -7,8 +7,9 @@ class LineclearsThingy extends StatelessWidget{ final int lines; final int holds; final int tSpins; + final bool showMoreClears; - const LineclearsThingy(this.clears, this.lines, this.holds, this.tSpins, {super.key}); + const LineclearsThingy(this.clears, this.lines, this.holds, this.tSpins, {super.key, this.showMoreClears = false}); @override Widget build(BuildContext context) { @@ -21,6 +22,7 @@ class LineclearsThingy extends StatelessWidget{ mainAxisSize: MainAxisSize.min, children: [ Text(t.numOfGameActions.lineClears(n: lines), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Pentas"), Text(clears.pentas.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Quads"), Text(clears.quads.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Triples"), Text(clears.triples.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Doubles"), Text(clears.doubles.toString())]), @@ -36,10 +38,14 @@ class LineclearsThingy extends StatelessWidget{ mainAxisSize: MainAxisSize.min, children: [ Text(t.numOfGameActions.tspinsTotal(n: tSpins), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spin pentas"), Text(clears.tSpinPentas.toString())]), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spin quads"), Text(clears.tSpinQuads.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins triples"), Text(clears.tSpinTriples.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins doubles"), Text(clears.tSpinDoubles.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins singles"), Text(clears.tSpinSingles.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins zeros"), Text(clears.tSpinZeros.toString())]), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins quads"), Text(clears.tSpinMiniQuads.toString())]), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins triples"), Text(clears.tSpinMiniTriples.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins doubles"), Text(clears.tSpinMiniDoubles.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins singles"), Text(clears.tSpinMiniSingles.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]), diff --git a/lib/widgets/recent_sp_games.dart b/lib/widgets/recent_sp_games.dart index 76e389c..e3e1b7a 100644 --- a/lib/widgets/recent_sp_games.dart +++ b/lib/widgets/recent_sp_games.dart @@ -42,7 +42,7 @@ class RecentSingleplayerGames extends StatelessWidget{ }, style: const TextStyle(fontSize: 18)), subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(record.stats, record.gamemode) + trailing: SpTrailingStats(record, record.gamemode) ) ], ); diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index 7bb057b..890f59e 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -141,7 +141,7 @@ class SingleplayerRecord extends StatelessWidget { }, 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].stats, stream!.records[i].gamemode) + trailing: SpTrailingStats(stream!.records[i], stream!.records[i].gamemode) ) ] ), diff --git a/lib/widgets/sp_trailing_stats.dart b/lib/widgets/sp_trailing_stats.dart index 9013211..fc679c6 100644 --- a/lib/widgets/sp_trailing_stats.dart +++ b/lib/widgets/sp_trailing_stats.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; class SpTrailingStats extends StatelessWidget{ - final ResultsStats endContext; + final RecordSingle record; final String gamemode; - const SpTrailingStats(this.endContext, this.gamemode, {super.key}); + const SpTrailingStats(this.record, this.gamemode, {super.key}); @override Widget build(BuildContext context) { @@ -15,12 +16,28 @@ class SpTrailingStats extends StatelessWidget{ mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, 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(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", + "40l" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS", + "blitz" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS", + "5mblast" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS", + "zenith" => "${f2.format(record.aggregateStats.apm)} APM, ${f2.format(record.aggregateStats.pps)} PPS", + "zenithex" => "${f2.format(record.aggregateStats.apm)} APM, ${f2.format(record.aggregateStats.pps)} PPS", + String() => "huh" + }, style: style, textAlign: TextAlign.right), + Text(switch(gamemode){ + "40l" => "${intf.format(record.stats.finessePercentage*100)}% F, ${record.stats.finesse?.faults} FF", + "blitz" => "${intf.format(record.stats.finessePercentage*100)}% F, ${record.stats.finesse?.faults} FF", + "5mblast" => "${intf.format(record.stats.finessePercentage*100)}% F, ${record.stats.finesse?.faults} FF", + "zenith" => "${f2.format(record.stats.cps)} CSP (${f2.format(record.stats.zenith!.peakrank)} peak)", + "zenithex" => "${f2.format(record.stats.cps)} CSP (${f2.format(record.stats.zenith!.peakrank)} peak)", + String() => "huh" + }, style: style, textAlign: TextAlign.right), + Text(switch(gamemode){ + "40l" => "${f2.format(record.stats.kps)} KPS, ${f2.format(record.stats.kpp)} KPP", + "blitz" => "${intf.format(record.stats.spp)} SPP, lvl ${record.stats.level}", + "5mblast" => "${intf.format(record.stats.spp)} SPP, ${record.stats.lines} L", + "zenith" => "${record.stats.kills} KO's, ${getMoreNormalTime(record.stats.finalTime)}", + "zenithex" => "${record.stats.kills} KO's, ${getMoreNormalTime(record.stats.finalTime)}", String() => "huh" }, style: style, textAlign: TextAlign.right) ], diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index 9dbcac5..0c0164e 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/gauget_num.dart'; import 'package:tetra_stats/widgets/graphs.dart'; @@ -95,9 +97,9 @@ class _ZenithThingyState extends State { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (record!.rank != -1) TextSpan(text: "№${record!.rank}"), + if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), if (record!.rank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"), + if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), if (record!.countryRank != -1) const TextSpan(text: " • "), TextSpan(text: timestamp(widget.record!.timestamp)), ] @@ -135,6 +137,27 @@ class _ZenithThingyState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("T", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text("${getMoreNormalTime(record!.stats.finalTime)}%", style: TextStyle( + shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), Table( columnWidths: const { From e323bf5898e257eb54781892019748a79e84008e Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 16 Aug 2024 00:55:45 +0300 Subject: [PATCH 19/33] HomeTab ready i guess --- lib/data_objects/tetrio.dart | 142 +++-- lib/services/tetrio_crud.dart | 2 + lib/views/main_view_tiles.dart | 875 ++++++++++++++++++------------ lib/widgets/tl_rating_thingy.dart | 10 +- lib/widgets/tl_thingy.dart | 11 +- 5 files changed, 629 insertions(+), 411 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index d8f3658..b3ec9cb 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -143,48 +143,6 @@ const Map rankColors = { // thanks osk for const rankColors at ht 'z': Color(0xFF375433) }; -// const Map sprintAverages = { // old data, based on https://discord.com/channels/673303546107658242/917098364787650590/1214231970259673098 -// 'x': Duration(seconds: 25, milliseconds: 413), -// 'u': Duration(seconds: 34, milliseconds: 549), -// 'ss': Duration(seconds: 43, milliseconds: 373), -// 's+': Duration(seconds: 54, milliseconds: 027), -// 's': Duration(seconds: 60, milliseconds: 412), -// 's-': Duration(seconds: 67, milliseconds: 381), -// 'a+': Duration(seconds: 73, milliseconds: 694), -// 'a': Duration(seconds: 81, milliseconds: 166), -// 'a-': Duration(seconds: 88, milliseconds: 334), -// 'b+': Duration(seconds: 93, milliseconds: 741), -// 'b': Duration(seconds: 98, milliseconds: 354), -// 'b-': Duration(seconds: 109, milliseconds: 610), -// 'c+': Duration(seconds: 124, milliseconds: 641), -// 'c': Duration(seconds: 126, milliseconds: 104), -// 'c-': Duration(seconds: 145, milliseconds: 865), -// 'd+': Duration(seconds: 154, milliseconds: 338), -// 'd': Duration(seconds: 162, milliseconds: 063), -// //'z': Duration(seconds: 66, milliseconds: 802) -// }; - -// const Map blitzAverages = { -// 'x': 626494, -// 'u': 406059, -// 'ss': 243166, -// 's+': 168636, -// 's': 121594, -// 's-': 107845, -// 'a+': 87142, -// 'a': 73413, -// 'a-': 60799, -// 'b+': 55417, -// 'b': 47608, -// 'b-': 40534, -// 'c+': 34200, -// 'c': 32535, -// 'c-': 25808, -// 'd+': 23345, -// 'd': 23063, -// //'z': 72084 -// }; - const Map sprintAverages = { // based on https://discord.com/channels/673303546107658242/674421736162197515/1244287342965952562 'x': Duration(seconds: 25, milliseconds: 144), 'u': Duration(seconds: 36, milliseconds: 115), @@ -461,6 +419,7 @@ class Summaries{ RecordSingle? blitz; RecordSingle? zenith; RecordSingle? zenithEx; + late List achievements; late TetraLeagueAlpha league; late TetrioZen zen; @@ -472,6 +431,7 @@ class Summaries{ if (json['blitz']['record'] != null) blitz = RecordSingle.fromJson(json['blitz']['record'], json['blitz']['rank'], json['40l']['rank_local']); if (json['zenith']['record'] != null) zenith = RecordSingle.fromJson(json['zenith']['record'], json['zenith']['rank'], json['zenith']['rank_local']); if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); + achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; league = TetraLeagueAlpha.fromJson(json['league'], DateTime.now()); zen = TetrioZen.fromJson(json['zen']); } @@ -892,6 +852,104 @@ class EstTr { } } +class Achievement { + late int k; + int? o; + late int rt; + late int vt; + late int min; + late int deci; + late String name; + late String object; + late String category; + late bool hidden; + late int art; + late bool nolb; + late String desc; + late String n; + String? sId; + double? v; + late int? a; + DateTime? t; + int? pos; + int? total; + int? rank; + + Achievement( + {required this.k, + this.o, + required this.rt, + required this.vt, + required this.min, + required this.deci, + required this.name, + required this.object, + required this.category, + required this.hidden, + required this.art, + required this.nolb, + required this.desc, + required this.n, + this.sId, + this.v, + required this.a, + this.t, + this.pos, + this.total, + this.rank}); + + Achievement.fromJson(Map json) { + k = json['k']; + o = json['o']; + rt = json['rt']; + vt = json['vt']; + min = json['min']; + deci = json['deci']; + name = json['name']; + object = json['object']; + category = json['category']; + hidden = json['hidden']; + art = json['art']; + nolb = json['nolb']; + desc = json['desc']; + n = json['n']; + sId = json['_id']; + v = json['v']?.toDouble(); + a = json['a']; + t = json['t'] != null ? DateTime.parse(json['t']) : null; + pos = json['pos']; + total = json['total']; + rank = json['rank']; + } + + Map toJson() { + final Map data = {}; + data['k'] = k; + data['o'] = o; + data['rt'] = rt; + data['vt'] = vt; + data['min'] = min; + data['deci'] = deci; + data['name'] = name; + data['object'] = object; + data['category'] = category; + data['hidden'] = hidden; + data['art'] = art; + data['nolb'] = nolb; + data['desc'] = desc; + data['n'] = n; + data['_id'] = sId; + data['v'] = v; + data['a'] = a; + data['t'] = t.toString(); + data['pos'] = pos; + data['total'] = total; + data['rank'] = rank; + return data; + } +} + + class Playstyle { final double _apm; final double _pps; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 3f23eff..23b0732 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -1140,6 +1140,8 @@ class TetrioService extends DB { // more exceptions to god of exceptions case 403: throw TetrioForbidden(); + case 404: + throw TetrioPlayerNotExist(); case 429: throw TetrioTooManyRequests(); case 418: diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 908d6bb..3e32811 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -51,63 +51,6 @@ Map cardsTitles = { //Cards.other: t.other }; -TetrioPlayer testPlayer = TetrioPlayer( - userId: "6098518e3d5155e6ec429cdc", - username: "dan63", - registrationTime: DateTime(2002, 2, 25, 9, 30, 01), - avatarRevision: 1704835194288, - bannerRevision: 1661462402700, - role: "user", - 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: const 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: 15, - bestRank: "x", - decaying: false, - rating: 23500.6194, - glicko: 3847.2134, - rd: 61.95383, - apm: 62.48, - pps: 1.85, - vs: 134.32, - 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)), -]); late ScrollController controller; class _MainState extends State with TickerProviderStateMixin { @@ -142,6 +85,7 @@ class _MainState extends State with TickerProviderStateMixin { body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ NavigationRail( leading: FloatingActionButton( @@ -201,12 +145,14 @@ class _MainState extends State with TickerProviderStateMixin { }); }, ), - switch (destination){ - 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), - 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), - 2 => DestinationLeaderboards(constraints: constraints), - _ => Text("Unknown destination $destination") - } + Expanded( + child: switch (destination){ + 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), + 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), + 2 => DestinationLeaderboards(constraints: constraints), + _ => Text("Unknown destination $destination") + }, + ) ]); }, )); @@ -406,7 +352,7 @@ class _DestinationGraphsState extends State { case ConnectionState.active: return const Center(child: CircularProgressIndicator()); case ConnectionState.done: - if (snapshot.hasData){ + if (snapshot.hasData && snapshot.data!.isNotEmpty){ List<_HistoryChartSpot> selectedGraph = snapshot.data![_chartsIndex].value!; yAxisTitle = _historyShortTitles[_chartsIndex]; return SingleChildScrollView( @@ -531,12 +477,13 @@ class _DestinationGraphsState extends State { ), ); } - if (snapshot.hasError){ + if (snapshot.hasError || snapshot.data!.isEmpty){ return Center(child: Column( + crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Text(snapshot.error != null ? snapshot.error.toString() : t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 8.0), child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), @@ -576,8 +523,93 @@ class DestinationHome extends StatefulWidget{ State createState() => _DestinationHomeState(); } +class FetchResults{ + bool success; + TetrioPlayer? player; + Summaries? summaries; + Exception? exception; + + FetchResults(this.success, this.player, this.summaries, this.exception); +} + +class RecordSummary extends StatelessWidget{ + final RecordSingle? record; + final bool hideRank; + final bool? betterThanRankAverage; + final MapEntry? closestAverage; + final bool? betterThanClosestAverage; + final String? rank; + + const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, this.hideRank = false}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null && record != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96)) + else !hideRank ? Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) : Container(), + if (record != null) Column( + crossAxisAlignment: hideRank ? CrossAxisAlignment.center : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + text: TextSpan( + text: switch(record!.gamemode){ + "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), + "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(record!.stats.zenith!.altitude)} m", + "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", + _ => record!.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white, height: 0.9), + ), + ), + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + 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: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), + _ => record!.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), + "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), + _ => record!.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), + if (record!.rank != -1 && record!.countryRank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), + const TextSpan(text: "\n"), + TextSpan(text: timestamp(record!.timestamp)), + ] + ), + ), + ], + ) else if (hideRank) RichText(text: TextSpan( + text: "---", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), + ), + ) + ], + ); + } + +} + class _DestinationHomeState extends State { - Cards rightCard = Cards.tetraLeague; + Cards rightCard = Cards.overview; CardMod cardMod = CardMod.info; Duration postSeasonLeft = seasonStart.difference(DateTime.now()); late Map>> modeButtons; @@ -588,8 +620,23 @@ class _DestinationHomeState extends State { bool? sprintBetterThanRankAverage; bool? blitzBetterThanRankAverage; + Future _getData() async { + TetrioPlayer player; + try{ + if (widget.searchFor.startsWith("ds:")){ + player = await teto.fetchPlayer(widget.searchFor.substring(3), isItDiscordID: true); // we trying to get him with that + }else{ + player = await teto.fetchPlayer(widget.searchFor); // Otherwise it's probably a user id or username + } + }on TetrioPlayerNotExist{ + return FetchResults(false, null, null, TetrioPlayerNotExist()); + } + Summaries summaries = await teto.fetchSummaries(player.userId); + return FetchResults(true, player, summaries, null); + } + Widget getOverviewCard(Summaries summaries){ - return const Column( + return Column( children: [ Card( child: Padding( @@ -605,16 +652,181 @@ class _DestinationHomeState extends State { ), ), ), + Card( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + TLRatingThingy(userID: "", tlData: summaries.league) + ], + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Text("Total runs submitted: ${summaries.achievements.firstWhere((e) => e.k == 5).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 5).v!) : "---"}", style: TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Text("Total score gained: ${summaries.achievements.firstWhere((e) => e.k == 6).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 6).v!) : "---"}", style: TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + RecordSummary(record: summaries.zenith, hideRank: true), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Text("Overall PB: ${summaries.achievements.firstWhere((e) => e.k == 18).v != null ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + RecordSummary(record: summaries.zenithEx, hideRank: true,), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Text("Overall PB: ${summaries.achievements.firstWhere((e) => e.k == 19).v != null ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), + Text("Score ${intf.format(summaries.zen.score)}"), + Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("f", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text("${(summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? + f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: TextStyle( + //shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + Row( + children: [ + Text("Total pieces placed:"), + Spacer(), + Text("${summaries.achievements.firstWhere((e) => e.k == 1).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"}"), + ], + ), + Row( + children: [ + Text(" - Placed with perfect finesse:"), + Spacer(), + Text("${summaries.achievements.firstWhere((e) => e.k == 4).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"}"), + ], + ) + ], + ), + ), + ), + ), + ], + ), Card( child: Padding( - padding: EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), child: Column( children: [ - Row( + if (summaries.achievements.firstWhere((e) => e.k == 16).v != null) Row( children: [ - Text("Title"), + Text("Total height climbed in QP"), Spacer(), - Text("Value"), + Text("${f2.format(summaries.achievements.firstWhere((e) => e.k == 16).v!)} m"), + ], + ), + if (summaries.achievements.firstWhere((e) => e.k == 17).v != null) Row( + children: [ + Text("KO's in QP"), + Spacer(), + Text("${intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)}"), ], ) ], @@ -643,8 +855,8 @@ class _DestinationHomeState extends State { ), ), ), - TetraLeagueThingy(league: testPlayer.tlSeason1!), - Card( + TetraLeagueThingy(league: data), + if (data.nerdStats != null) Card( child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -654,8 +866,8 @@ class _DestinationHomeState extends State { ], ), ), - NerdStatsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!), - GraphsThingy(nerdStats: testPlayer.tlSeason1!.nerdStats!, playstyle: testPlayer.tlSeason1!.playstyle!, apm: testPlayer.tlSeason1!.apm!, pps: testPlayer.tlSeason1!.pps!, vs: testPlayer.tlSeason1!.vs!) + if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!), + if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) ], ); } @@ -671,7 +883,7 @@ class _DestinationHomeState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text(isTop ? t.top : t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), ], ), ), @@ -1153,27 +1365,53 @@ class _DestinationHomeState extends State { @override Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - width: 450.0, - child: FutureBuilder(future: teto.fetchPlayer(widget.searchFor), builder:(context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return Column( + return FutureBuilder( + future: _getData(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasError){ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), + ), + ], + ) + ); + } + if (snapshot.hasData){ + blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null) ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; + sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null) ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; + if (snapshot.data!.summaries!.sprint != null) { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; + } + if (snapshot.data!.summaries!.blitz != null){ + closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); + blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; + } + return Row( + children: [ + SizedBox( + width: 450, + child: Column( children: [ - NewUserThingy(player: snapshot.data!, showStateTimestamp: false, setState: setState), - if (snapshot.data!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.badges), - if (snapshot.data!.distinguishment != null) DistinguishmentThingy(snapshot.data!.distinguishment!), - if (snapshot.data!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.botmaster), - if (snapshot.data!.role == "banned") FakeDistinguishmentThingy(banned: true) - else if (snapshot.data!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), - if (snapshot.data!.bio != null) Card( + NewUserThingy(player: snapshot.data!.player!, showStateTimestamp: false, setState: setState), + if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), + if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), + if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), + if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) + else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), + if (snapshot.data!.player!.bio != null) Card( child: Column( children: [ Row( @@ -1185,7 +1423,7 @@ class _DestinationHomeState extends State { ), Padding( padding: const EdgeInsets.only(bottom: 8.0), - child: MarkdownBody(data: snapshot.data!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), ) ], ), @@ -1216,169 +1454,96 @@ class _DestinationHomeState extends State { ), ) ], - ); - } - if (snapshot.hasError){ - if (snapshot.error.runtimeType == TetrioPlayerNotExist) { - return Card( - child: Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ), - ); - } - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), - ), - ], - ) - ); - } - return Text("huh?"); - } - }, - )), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: Column( - //crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: widget.constraints.maxHeight - 64, - child: SingleChildScrollView( - child: FutureBuilder( - future: teto.fetchSummaries(widget.searchFor), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - blitzBetterThanRankAverage = (snapshot.data!.league.rank != "z" && snapshot.data!.blitz != null) ? snapshot.data!.blitz!.stats.score > blitzAverages[snapshot.data!.league.rank]! : null; - sprintBetterThanRankAverage = (snapshot.data!.league.rank != "z" && snapshot.data!.sprint != null) ? snapshot.data!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.league.rank]! : null; - if (snapshot.data!.sprint != null) { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.sprint!.stats.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = snapshot.data!.sprint!.stats.finalTime < closestAverageSprint!.value; - } - if (snapshot.data!.blitz != null){ - closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.blitz!.stats.score).abs() < (b -snapshot.data!.blitz!.stats.score).abs() ? a : b)); - blitzBetterThanClosestAverage = snapshot.data!.blitz!.stats.score > closestAverageBlitz!.value; - } - return switch (rightCard){ - Cards.overview => getOverviewCard(snapshot.data!), - Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.league), - CardMod.recent => getRecentTLrecords(widget.constraints), - _ => Center(child: Text("huh?")) - }, - Cards.quickPlay => switch (cardMod){ - CardMod.info => getZenithCard(snapshot.data?.zenith), - CardMod.recent => getListOfRecords("zenith/recent", false, widget.constraints), - CardMod.top => getListOfRecords("zenith/top", true, widget.constraints), - CardMod.ex => getZenithCard(snapshot.data?.zenithEx), - CardMod.exRecent => getListOfRecords("zenithex/recent", false, widget.constraints), - CardMod.exTop => getListOfRecords("zenithex/top", true, widget.constraints), - _ => Center(child: Text("huh?")) - }, - Cards.sprint => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.league.rank), - CardMod.recent => getListOfRecords("40l/recent", false, widget.constraints), - CardMod.top => getListOfRecords("40l/top", true, widget.constraints), - _ => Center(child: Text("huh?")) - }, - Cards.blitz => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.league.rank), - CardMod.recent => getListOfRecords("blitz/recent", false, widget.constraints), - CardMod.top => getListOfRecords("blitz/top", true, widget.constraints), - _ => Center(child: Text("huh?")) - }, - }; - } - if (snapshot.hasError){ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error != null ? snapshot.error.toString() : "lol", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), - ), - ], - ) - ); - } - return const Text("lol"); - } - } - ), + ), ), - ), - if (modeButtons[rightCard]!.length > 1) SegmentedButton( - showSelectedIcon: false, - selected: {cardMod}, - segments: modeButtons[rightCard]!, - onSelectionChanged: (p0) { - setState(() { - cardMod = p0.first; - }); - }, - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: Cards.overview, - //label: Text('Overview'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Cards.tetraLeague, - //label: Text('Tetra League'), - icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.quickPlay, - //label: Text('Quick Play'), - icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.sprint, - //label: Text('40 Lines'), - icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.blitz, - //label: Text('Blitz'), - icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ], - selected: {rightCard}, - onSelectionChanged: (Set newSelection) { - setState(() { - cardMod = CardMod.info; - rightCard = newSelection.first; - });}) - ] - ), - ), - // SizedBox( - // width: 450, - // child: _TLRecords(userID: "snapshot.data![0].userId", changePlayer: changePlayer, data: [], wasActiveInTL: true, oldMathcesHere: false, separateScrollController: true) - // ) - ], - ); + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: Column( + children: [ + SizedBox( + height: widget.constraints.maxHeight - 64, + child: SingleChildScrollView( + child: switch (rightCard){ + Cards.overview => getOverviewCard(snapshot.data!.summaries!), + Cards.tetraLeague => switch (cardMod){ + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league), + CardMod.recent => getRecentTLrecords(widget.constraints), + _ => Center(child: Text("huh?")) + }, + Cards.quickPlay => switch (cardMod){ + CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), + CardMod.recent => getListOfRecords("zenith/recent", false, widget.constraints), + CardMod.top => getListOfRecords("zenith/top", true, widget.constraints), + CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), + CardMod.exRecent => getListOfRecords("zenithex/recent", false, widget.constraints), + CardMod.exTop => getListOfRecords("zenithex/top", true, widget.constraints), + _ => Center(child: Text("huh?")) + }, + Cards.sprint => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.recent => getListOfRecords("40l/recent", false, widget.constraints), + CardMod.top => getListOfRecords("40l/top", true, widget.constraints), + _ => Center(child: Text("huh?")) + }, + Cards.blitz => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.recent => getListOfRecords("blitz/recent", false, widget.constraints), + CardMod.top => getListOfRecords("blitz/top", true, widget.constraints), + _ => Center(child: Text("huh?")) + }, + }, + ), + ), + if (modeButtons[rightCard]!.length > 1) SegmentedButton( + showSelectedIcon: false, + selected: {cardMod}, + segments: modeButtons[rightCard]!, + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + }); + }, + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: Cards.overview, + //label: Text('Overview'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + //label: Text('Tetra League'), + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.quickPlay, + //label: Text('Quick Play'), + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.sprint, + //label: Text('40 Lines'), + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.blitz, + //label: Text('Blitz'), + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ], + selected: {rightCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + cardMod = CardMod.info; + rightCard = newSelection.first; + });}) + ], + ) + ) + ], + ); + } + } + return Text("End of FutureBuilder"); + }, + ); } } @@ -2145,18 +2310,15 @@ class TetraLeagueThingy extends StatelessWidget{ children: [ TableRow(children: [ const Text("APM: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.apm) : "---", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - //Text(" APM", style: TextStyle(fontSize: 21)) + Text(f2.format(league.apm??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), ]), TableRow(children: [ const Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.pps) : "---", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - //Text(" PPS", style: TextStyle(fontSize: 21)) + Text(f2.format(league.pps??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), ]), TableRow(children: [ const Text("VS: ", style: TextStyle(fontSize: 21)), - Text(league.apm != null ? f2.format(league.vs) : "---", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - // Text(" VS", style: TextStyle(fontSize: 21)) + Text(f2.format(league.vs??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), ]) ], ), @@ -2202,8 +2364,8 @@ class TetraLeagueThingy extends StatelessWidget{ children: [ TableRow(children: [ //Text("VS: ", style: TextStyle(fontSize: 21)), - Text("№ ${intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" in BY", style: TextStyle(fontSize: 21)) + Text("№ ${league.standingLocal.isNegative ? "---" : intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), + Text(" local", style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)) ]), TableRow(children: [ //Text("APM: ", style: TextStyle(fontSize: 21)), @@ -2237,94 +2399,99 @@ class NerdStatsThingy extends StatelessWidget{ return Card( child: Column( children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 256.0, - width: 256.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: SfRadialGauge( - backgroundColor: Colors.black, - axes: [ - RadialAxis( - startAngle: 200, - endAngle: 340, - minimum: 0.0, - maximum: 1.0, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.app, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "APP\n"), - TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), - //TextSpan(text: "\nAPP"), - ] - ))), - angle: 270,positionFactor: 0.5 - ), - ], - ), - RadialAxis( - startAngle: 20, - endAngle: 160, - isInversed: true, - minimum: 1.8, - maximum: 2.4, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.vsapm, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "VS/APM\n"), - TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), - ] - ))), - angle: 90,positionFactor: 0.5 - ) - ], - ) - ] + Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 256.0, + width: 256.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: SfRadialGauge( + backgroundColor: Colors.black, + axes: [ + RadialAxis( + startAngle: 200, + endAngle: 340, + minimum: 0.0, + maximum: 1.0, + radiusFactor: 1.01, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: nerdStats.app, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + const TextSpan(text: "APP\n"), + TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), + //TextSpan(text: "\nAPP"), + ] + ))), + angle: 270,positionFactor: 0.5 + ), + ], + ), + RadialAxis( + startAngle: 20, + endAngle: 160, + isInversed: true, + minimum: 1.8, + maximum: 2.4, + radiusFactor: 1.01, + showTicks: true, + showLabels: false, + interval: 0.1, + //labelsPosition: ElementsPosition.outside, + ranges:[ + GaugeRange(startValue: 0, endValue: nerdStats.vsapm, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + const TextSpan(text: "VS/APM\n"), + TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), + ] + ))), + angle: 90,positionFactor: 0.5 + ) + ], + ) + ] + ), ), ), - ), - Expanded( - child: Wrap( - spacing: 10, - children: [ - GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2), - GaugetThingy(value: nerdStats.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1), - ], - ), - ) - ] + Expanded( + child: Wrap( + alignment: WrapAlignment.center, + spacing: 10, + children: [ + GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2), + GaugetThingy(value: nerdStats.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3), + GaugetThingy(value: nerdStats.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1), + ], + ), + ) + ] + ), ), ], ) diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index 22682b0..a0702f6 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -23,7 +23,7 @@ class TLRatingThingy extends StatelessWidget{ bool bigScreen = MediaQuery.of(context).size.width >= 768; String decimalSeparator = f4.symbols.DECIMAL_SEP; List formatedTR = f4.format(tlData.rating).split(decimalSeparator); - List formatedGlicko = f4.format(tlData.glicko).split(decimalSeparator); + List formatedGlicko = tlData.glicko != null ? f4.format(tlData.glicko).split(decimalSeparator) : ["---","--"]; List formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); //DateTime now = DateTime.now(); //bool beforeS1end = now.isBefore(seasonEnd); @@ -43,7 +43,7 @@ class TLRatingThingy extends StatelessWidget{ RichText( text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white), - children: switch(prefs.getInt("ratingMode")){ + children: (tlData.gamesPlayed > 9) ? switch(prefs.getInt("ratingMode")){ 1 => [ TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), if (formatedGlicko.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedGlicko[1]), @@ -59,7 +59,7 @@ class TLRatingThingy extends StatelessWidget{ if (formatedTR.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedTR[1]), TextSpan(text: " TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) ], - } + } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: TextStyle(color: Colors.grey, fontSize: 14)),] ) ), if (oldTl != null) Text( @@ -75,7 +75,7 @@ class TLRatingThingy extends StatelessWidget{ Colors.green ), ), - Column( + if (tlData.gamesPlayed > 9) Column( children: [ RichText( textAlign: TextAlign.center, @@ -87,7 +87,7 @@ class TLRatingThingy extends StatelessWidget{ if (tlData.bestRank != "z") const TextSpan(text: " • "), if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"), if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"), - TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.rating)} TR • RD: " : "Glicko: ${f2.format(tlData.glicko!)}±"}"), + TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.rating)} TR • RD: " : "Glicko: ${tlData.glicko != null ? 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)) diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 370e25f..4cf6f95 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -160,7 +160,7 @@ class _TLThingyState extends State with TickerProviderStateMixin { }); }, ), - if (currentTl.gamesPlayed > 9) TLRatingThingy(userID: widget.userID, tlData: currentTl, oldTl: oldTl, topTR: widget.topTR, lastMatchPlayed: widget.lastMatchPlayed), + TLRatingThingy(userID: widget.userID, tlData: currentTl, oldTl: oldTl, topTR: widget.topTR, lastMatchPlayed: widget.lastMatchPlayed), if (currentTl.gamesPlayed > 9) TLProgress( tlData: currentTl, previousRankTRcutoff: widget.thatRankCutoff, @@ -170,15 +170,6 @@ class _TLThingyState extends State with TickerProviderStateMixin { nextRankGlickoCutoff: widget.nextRankCutoffGlicko, nextRankTRcutoffTarget: widget.nextRankTarget, ), - if (currentTl.gamesPlayed < 10) - Text(t.gamesUntilRanked(left: 10 - currentTl.gamesPlayed), - softWrap: true, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Eurostile Round", - fontSize: bigScreen ? 42 : 28, - overflow: TextOverflow.visible, - )), Padding( padding: const EdgeInsets.fromLTRB(8, 16, 8, 48), child: Wrap( From 54030def5478a45a6fdb36e8c1825c219a73caac Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 16 Aug 2024 20:26:30 +0300 Subject: [PATCH 20/33] one very small update --- lib/data_objects/tetrio.dart | 4 ++-- lib/main.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index b3ec9cb..093601c 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -648,7 +648,7 @@ class ResultsStats { late int piecesPlaced; late int lines; late int score; - int? seed; + double? seed; late Duration finalTime; late int tSpins; late Clears clears; @@ -680,7 +680,7 @@ class ResultsStats { required this.finesse}); ResultsStats.fromJson(Map json) { - seed = json['seed']; + seed = json['seed']?.toDouble(); lines = json['lines']; inputs = json['inputs']; holds = json['holds'] ?? 0; diff --git a/lib/main.dart b/lib/main.dart index 5890c20..3f81e0f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; import 'package:tetra_stats/views/calc_view.dart'; From 2376c0eb584dfab4667e3d1d6e9861aaf9eb0179 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 17 Aug 2024 01:40:09 +0300 Subject: [PATCH 21/33] ok, 1.6.3, a lot of things i should fix, a lot of things to consider... --- lib/data_objects/tetrio.dart | 70 +++++++++++++++-------------- lib/services/tetrio_crud.dart | 5 ++- lib/views/compare_view.dart | 16 ++++--- lib/views/main_view.dart | 20 ++++----- lib/views/main_view_tiles.dart | 8 ++-- lib/views/rank_averages_view.dart | 4 +- lib/views/state_view.dart | 2 +- lib/views/tl_leaderboard_view.dart | 4 +- lib/views/tl_match_view.dart | 4 +- lib/widgets/tl_progress_bar.dart | 12 ++--- lib/widgets/tl_rating_thingy.dart | 14 +++--- lib/widgets/tl_thingy.dart | 65 +++------------------------ pubspec.yaml | 2 +- res/tetrio_tl_alpha_ranks/x+.png | Bin 0 -> 221023 bytes test/api_test.dart | 10 ++--- 15 files changed, 93 insertions(+), 143 deletions(-) create mode 100644 res/tetrio_tl_alpha_ranks/x+.png diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 093601c..1683e1b 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -21,6 +21,7 @@ const List ranks = [ "d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x" ]; const Map rankCutoffs = { + "x+": 0.002, "x": 0.01, "u": 0.05, "ss": 0.11, @@ -220,7 +221,7 @@ class TetrioPlayer { bool? badstanding; String? botmaster; Connections? connections; - TetraLeagueAlpha? tlSeason1; + TetraLeague? tlSeason1; TetrioZen? zen; Distinguishment? distinguishment; DateTime? cachedUntil; @@ -273,7 +274,7 @@ class TetrioPlayer { country = json['country']; supporterTier = json['supporter_tier'] ?? 0; verified = json['verified'] ?? false; - tlSeason1 = json['league'] != null ? TetraLeagueAlpha.fromJson(json['league'], stateTime) : null; + tlSeason1 = json['league'] != null ? TetraLeague.fromJson(json['league'], stateTime) : null; avatarRevision = json['avatar_revision']; bannerRevision = json['banner_revision']; bio = json['bio']; @@ -339,8 +340,8 @@ class TetrioPlayer { } TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard() => TetrioPlayerFromLeaderboard( - 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); + userId, username, role, xp, country, state, gamesPlayed, gamesWon, + tlSeason1!.tr, tlSeason1!.glicko??0, tlSeason1!.rd??noTrRd, tlSeason1!.rank, tlSeason1!.bestRank, tlSeason1!.apm??0, tlSeason1!.pps??0, tlSeason1!.vs??0, tlSeason1!.decaying); @override String toString() { @@ -350,7 +351,7 @@ class TetrioPlayer { num? getStatByEnum(Stats stat){ switch (stat) { case Stats.tr: - return tlSeason1?.rating; + return tlSeason1?.tr; case Stats.glicko: return tlSeason1?.glicko; case Stats.rd: @@ -420,7 +421,7 @@ class Summaries{ RecordSingle? zenith; RecordSingle? zenithEx; late List achievements; - late TetraLeagueAlpha league; + late TetraLeague league; late TetrioZen zen; Summaries(this.id, this.league, this.zen); @@ -432,7 +433,7 @@ class Summaries{ if (json['zenith']['record'] != null) zenith = RecordSingle.fromJson(json['zenith']['record'], json['zenith']['rank'], json['zenith']['rank_local']); if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; - league = TetraLeagueAlpha.fromJson(json['league'], DateTime.now()); + league = TetraLeague.fromJson(json['league'], DateTime.now()); zen = TetrioZen.fromJson(json['zen']); } } @@ -1360,13 +1361,14 @@ class EndContextMulti { } } -class TetraLeagueAlpha { +class TetraLeague { late DateTime timestamp; late int gamesPlayed; late int gamesWon; late String bestRank; late bool decaying; - late double rating; + late double tr; + late double gxe; late String rank; double? glicko; double? rd; @@ -1386,13 +1388,14 @@ class TetraLeagueAlpha { Playstyle? playstyle; List? records; - TetraLeagueAlpha( + TetraLeague( {required this.timestamp, required this.gamesPlayed, required this.gamesWon, required this.bestRank, required this.decaying, - required this.rating, + required this.tr, + required this.gxe, required this.rank, this.glicko, this.rd, @@ -1415,13 +1418,14 @@ class TetraLeagueAlpha { double get winrate => gamesWon / gamesPlayed; - TetraLeagueAlpha.fromJson(Map json, ts) { + TetraLeague.fromJson(Map json, ts) { timestamp = ts; gamesPlayed = json['gamesplayed'] ?? 0; gamesWon = json['gameswon'] ?? 0; - rating = json['rating'] != null ? json['rating'].toDouble() : -1; + tr = json['tr'] != null ? json['tr'].toDouble() : -1; glicko = json['glicko']?.toDouble(); rd = json['rd'] != null ? json['rd']!.toDouble() : noTrRd; + gxe = json['gxe'].toDouble(); rank = json['rank'] != null ? json['rank']!.toString() : 'z'; bestRank = json['bestrank'] != null ? json['bestrank']!.toString() : 'z'; apm = json['apm']?.toDouble(); @@ -1442,17 +1446,17 @@ class TetraLeagueAlpha { } @override - bool operator ==(covariant TetraLeagueAlpha other) => gamesPlayed == other.gamesPlayed && rd == other.rd; + bool operator ==(covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && rd == other.rd; - bool lessStrictCheck (covariant TetraLeagueAlpha other) => gamesPlayed == other.gamesPlayed && glicko == other.glicko; + bool lessStrictCheck (covariant TetraLeague other) => gamesPlayed == other.gamesPlayed && glicko == other.glicko; - double? get esttracc => (estTr != null) ? estTr!.esttr - rating : null; + double? get esttracc => (estTr != null) ? estTr!.esttr - tr : null; Map toJson() { final Map data = {}; if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed; if (gamesWon > 0) data['gameswon'] = gamesWon; - if (rating >= 0) data['rating'] = rating; + if (tr >= 0) data['tr'] = tr; if (glicko != null) data['glicko'] = glicko; if (rd != null && rd != noTrRd) data['rd'] = rd; if (rank != 'z') data['rank'] = rank; @@ -1868,7 +1872,7 @@ class TetrioPlayersLeaderboard { avgAPM += entry.apm; avgPPS += entry.pps; avgVS += entry.vs; - avgTR += entry.rating; + avgTR += entry.tr; if (entry.glicko != null) avgGlicko += entry.glicko!; if (entry.rd != null) avgRD += entry.rd!; avgAPP += entry.nerdStats.app; @@ -1888,8 +1892,8 @@ class TetrioPlayersLeaderboard { avgInfDS += entry.playstyle.infds; totalGamesPlayed += entry.gamesPlayed; totalGamesWon += entry.gamesWon; - if (entry.rating < lowestTR){ - lowestTR = entry.rating; + if (entry.tr < lowestTR){ + lowestTR = entry.tr; lowestTRid = entry.userId; lowestTRnick = entry.username; } @@ -2008,8 +2012,8 @@ class TetrioPlayersLeaderboard { lowestInfDSid = entry.userId; lowestInfDSnick = entry.username; } - if (entry.rating > highestTR){ - highestTR = entry.rating; + if (entry.tr > highestTR){ + highestTR = entry.tr; highestTRid = entry.userId; highestTRnick = entry.username; } @@ -2152,7 +2156,7 @@ class TetrioPlayersLeaderboard { avgInfDS /= filtredLeaderboard.length; avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor(); avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor(); - return [TetraLeagueAlpha(timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, rating: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), + return [TetraLeague(timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, gxe: -1, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), { "everyone": rank == "", "totalGamesPlayed": totalGamesPlayed, @@ -2317,12 +2321,12 @@ class TetrioPlayersLeaderboard { "avgPlonk": avgPlonk, "avgStride": avgStride, "avgInfDS": avgInfDS, - "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].rating : lowestTR, - "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0, + "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()].tr : lowestTR, + "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()].glicko : 0, "entries": filtredLeaderboard }]; }else{ - return [TetraLeagueAlpha(timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, rating: 0, rank: rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), + return [TetraLeague(timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, tr: 0, rank: rank, percentileRank: rank, gxe: -1, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), {"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0, "toEnterGlicko": 0}]; } } @@ -2352,6 +2356,7 @@ class TetrioPlayersLeaderboard { } Map> get averages => { + 'x+': getAverageOfRank("x+"), 'x': getAverageOfRank("x"), 'u': getAverageOfRank("u"), 'ss': getAverageOfRank("ss"), @@ -2428,11 +2433,10 @@ class TetrioPlayerFromLeaderboard { late String role; late double xp; String? country; - late bool verified; late DateTime timestamp; late int gamesPlayed; late int gamesWon; - late double rating; + late double tr; late double? glicko; late double? rd; late String rank; @@ -2451,11 +2455,10 @@ class TetrioPlayerFromLeaderboard { this.role, this.xp, this.country, - this.verified, this.timestamp, this.gamesPlayed, this.gamesWon, - this.rating, + this.tr, this.glicko, this.rd, this.rank, @@ -2470,7 +2473,7 @@ class TetrioPlayerFromLeaderboard { } double get winrate => gamesWon / gamesPlayed; - double get esttracc => estTr.esttr - rating; + double get esttracc => estTr.esttr - tr; TetrioPlayerFromLeaderboard.fromJson(Map json, DateTime ts) { userId = json['_id']; @@ -2478,11 +2481,10 @@ class TetrioPlayerFromLeaderboard { role = json['role']; xp = json['xp'].toDouble(); country = json['country']; - verified = json['verified']; timestamp = ts; gamesPlayed = json['league']['gamesplayed'] as int; gamesWon = json['league']['gameswon'] as int; - rating = json['league']['rating'] != null ? json['league']['rating'].toDouble() : 0; + tr = json['league']['tr'] != null ? json['league']['tr'].toDouble() : 0; glicko = json['league']['glicko']?.toDouble(); rd = json['league']['rd']?.toDouble(); rank = json['league']['rank']; @@ -2499,7 +2501,7 @@ class TetrioPlayerFromLeaderboard { num getStatByEnum(Stats stat){ switch (stat) { case Stats.tr: - return rating; + return tr; case Stats.glicko: return glicko??-1; case Stats.rd: diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 23b0732..3a39315 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -536,7 +536,7 @@ class TetrioService extends DB { supporterTier: 0, verified: false, connections: null, - tlSeason1: TetraLeagueAlpha( + tlSeason1: TetraLeague( timestamp: DateTime.parse(entry[9]), apm: entry[6] != '' ? entry[6] : null, pps: entry[7] != '' ? entry[7] : null, @@ -547,7 +547,8 @@ class TetrioService extends DB { gamesWon: entry[2], bestRank: "z", decaying: false, - rating: entry[3], + tr: entry[3], + gxe: -1, rank: entry[5], percentileRank: entry[5], percentile: rankCutoffs[entry[5]]!, diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 2916a0e..abc0a81 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -17,7 +17,7 @@ enum Mode{ averages } Mode greenSideMode = Mode.player; -List theGreenSide = [null, null, null]; // TetrioPlayer?, List>?, TetraLeagueAlpha? +List theGreenSide = [null, null, null]; // TetrioPlayer?, List>?, TetraLeague? Mode redSideMode = Mode.player; List theRedSide = [null, null, null]; final DateFormat dateFormat = DateFormat.yMd(LocaleSettings.currentLocale.languageCode).add_Hm(); @@ -82,7 +82,7 @@ class CompareState extends State { double vs = double.parse(threeNumbers[2][0]!); theRedSide = [null, null, - TetraLeagueAlpha( + TetraLeague( timestamp: DateTime.now(), apm: apm, pps: pps, @@ -92,7 +92,8 @@ class CompareState extends State { gamesWon: -1, bestRank: "z", decaying: true, - rating: -1, + tr: -1, + gxe: -1, rank: "z", percentileRank: "z", percentile: 1, @@ -156,7 +157,7 @@ class CompareState extends State { double vs = double.parse(threeNumbers[2][0]!); theGreenSide = [null, null, - TetraLeagueAlpha( + TetraLeague( timestamp: DateTime.now(), apm: apm, pps: pps, @@ -166,7 +167,8 @@ class CompareState extends State { gamesWon: -1, bestRank: "z", decaying: true, - rating: -1, + tr: -1, + gxe: -1, rank: "z", percentileRank: "z", percentile: 1, @@ -395,8 +397,8 @@ class CompareState extends State { redSideMode != Mode.stats) CompareThingy( label: "TR", - greenSide: theGreenSide[2].rating, - redSide: theRedSide[2].rating, + greenSide: theGreenSide[2].tr, + redSide: theRedSide[2].tr, fractionDigits: 2, higherIsBetter: true, ), diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index cb22558..3fa3e66 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -64,7 +64,7 @@ class _MainState extends State with TickerProviderStateMixin { Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up TetrioPlayersLeaderboard? everyone; PlayerLeaderboardPosition? meAmongEveryone; - TetraLeagueAlpha? rankAverages; + TetraLeague? rankAverages; double? thatRankCutoff; double? nextRankCutoff; double? thatRankGlickoCutoff; @@ -208,7 +208,7 @@ class _MainState extends State with TickerProviderStateMixin { // 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)]; + // nextRankCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.tr??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)]; // } @@ -217,13 +217,13 @@ class _MainState extends State with TickerProviderStateMixin { // Making list of Tetra League matches bool isTracking = await teto.isPlayerTracking(me.userId); List states = []; - TetraLeagueAlpha? compareWith; - Set uniqueTL = {}; + TetraLeague? compareWith; + Set uniqueTL = {}; 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); - } + // if (isTracking){ // if tracked - save data to local DB + // await teto.storeState(me); + // //await teto.saveTLMatchesFromStream(tlStream); + // } TetraLeagueAlphaStream? oldMatches; // building list of TL matches if(fetchTLmatches) { @@ -270,7 +270,7 @@ class _MainState extends State with TickerProviderStateMixin { } } - states.addAll(await teto.getPlayer(me.userId)); + //states.addAll(await teto.getPlayer(me.userId)); for (var element in states) { // For graphs I need only unique entries if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); @@ -279,7 +279,7 @@ class _MainState extends State with TickerProviderStateMixin { if (uniqueTL.length >= 2){ compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rating)], child: Text(t.statCellNum.tr)), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 3e32811..4e04035 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -285,7 +285,7 @@ class _DestinationGraphsState extends State { Future>>> getChartsData(bool fetchHistory) async { List states = []; - Set uniqueTL = {}; + Set uniqueTL = {}; if(fetchHistory){ try{ @@ -310,7 +310,7 @@ class _DestinationGraphsState extends State { if (uniqueTL.length >= 2){ chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rating)], child: Text(t.statCellNum.tr)), + DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), @@ -837,7 +837,7 @@ class _DestinationHomeState extends State { ); } - Widget getTetraLeagueCard(TetraLeagueAlpha data){ + Widget getTetraLeagueCard(TetraLeague data){ return Column( children: [ Card( @@ -2287,7 +2287,7 @@ class _SearchDrawerState extends State { } class TetraLeagueThingy extends StatelessWidget{ - final TetraLeagueAlpha league; + final TetraLeague league; const TetraLeagueThingy({super.key, required this.league}); diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index b55fae0..4ff0446 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -360,7 +360,7 @@ class RankState extends State with SingleTickerProviderStateMixin { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - Text("${_f2.format(they[index].rating)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), + Text("${_f2.format(they[index].tr)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), Image.asset("res/tetrio_tl_alpha_ranks/${they[index].rank}.png", height: bigScreen ? 48 : 16), ], ), @@ -412,7 +412,7 @@ class RankState extends State with SingleTickerProviderStateMixin { Text(t.averageValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Expanded( child: ListView(children: [ - _ListEntry(value: widget.rank[0].rating, label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), + _ListEntry(value: widget.rank[0].tr, label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), _ListEntry(value: widget.rank[0].glicko, label: "Glicko", id: "", username: "", approximate: true, fractionDigits: 2), _ListEntry(value: widget.rank[0].rd, label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[0].gamesPlayed, label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 0), diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart index 5e367d2..19b190e 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 [], hidePreSeasonThingy: true,)))); + body: TLThingy(tl: widget.state.tlSeason1!, userID: widget.state.userId, states: const [])))); } } diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index 8b16a28..b3c181d 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -74,7 +74,7 @@ class TLLeaderboardState extends State { return const Center(child: CircularProgressIndicator()); case ConnectionState.done: final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers!.length)}"); + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers != null ? allPlayers.length : 0)}"); bool bigScreen = MediaQuery.of(context).size.width > 768; return NestedScrollView( headerSliverBuilder: (context, value) { @@ -189,7 +189,7 @@ class TLLeaderboardState extends State { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - Text("${f2.format(allPlayers[index].rating)} TR", style: const TextStyle(fontSize: 28)), + Text("${f2.format(allPlayers[index].tr)} TR", style: const TextStyle(fontSize: 28)), Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36), ], ), diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 18f3d2a..fc4b50d 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -382,8 +382,8 @@ class TlMatchResultState extends State { ), VsGraphs( roundSelector == -2 ? timeWeightedStats[0].apm : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, - roundSelector == -2 ? timeWeightedStats[0].pps : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, - roundSelector == -2 ? timeWeightedStats[0].vs : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, + roundSelector == -2 ? timeWeightedStats[0].pps : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, + roundSelector == -2 ? timeWeightedStats[0].vs : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, roundSelector == -2 ? timeWeightedStats[0].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats, roundSelector == -2 ? timeWeightedStats[0].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle, roundSelector == -2 ? timeWeightedStats[1].apm : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index 59fddcf..b59b181 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -9,7 +9,7 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; class TLProgress extends StatelessWidget{ - final TetraLeagueAlpha tlData; + final TetraLeague tlData; final double? nextRankTRcutoff; final double? previousRankTRcutoff; final double? nextRankGlickoCutoff; @@ -45,7 +45,7 @@ class TLProgress extends StatelessWidget{ children: [ if (tlData.prevAt > 0) TextSpan(text: "№ ${f0.format(tlData.prevAt)}"), if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"), - if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"), + if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.tr)}) TR"), if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"), if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null)) ] @@ -59,7 +59,7 @@ class TLProgress extends StatelessWidget{ children: [ if (tlData.nextAt > 0) TextSpan(text: "№ ${f0.format(tlData.nextAt)}"), if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), - if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), + if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.tr)}) TR"), if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) ] @@ -72,14 +72,14 @@ class TLProgress extends StatelessWidget{ maximum: 1, interval: 1, ranges: [ - if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.rating)!, color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross) + if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.tr)!, color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross) else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross), if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent, position: LinearElementPosition.inside), if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside) ], markerPointers: [ - LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), - if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) + LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), + if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) ], isMirrored: true, showTicks: true, diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index a0702f6..3fc69be 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -10,8 +10,8 @@ var fDiff = NumberFormat("+#,###.####;-#,###.####"); class TLRatingThingy extends StatelessWidget{ final String userID; - final TetraLeagueAlpha tlData; - final TetraLeagueAlpha? oldTl; + final TetraLeague tlData; + final TetraLeague? oldTl; final double? topTR; final DateTime? lastMatchPlayed; @@ -22,7 +22,7 @@ class TLRatingThingy extends StatelessWidget{ bool oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; bool bigScreen = MediaQuery.of(context).size.width >= 768; String decimalSeparator = f4.symbols.DECIMAL_SEP; - List formatedTR = f4.format(tlData.rating).split(decimalSeparator); + List formatedTR = f4.format(tlData.tr).split(decimalSeparator); List formatedGlicko = tlData.glicko != null ? f4.format(tlData.glicko).split(decimalSeparator) : ["---","--"]; List formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); //DateTime now = DateTime.now(); @@ -66,11 +66,11 @@ class TLRatingThingy extends StatelessWidget{ switch(prefs.getInt("ratingMode")){ 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", - _ => "${fDiff.format(tlData.rating - oldTl!.rating)} TR" + _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" }, textAlign: TextAlign.center, style: TextStyle( - color: tlData.rating - oldTl!.rating < 0 ? + color: tlData.tr - oldTl!.tr < 0 ? Colors.red : Colors.green ), @@ -83,11 +83,11 @@ class TLRatingThingy extends StatelessWidget{ text: TextSpan( style: DefaultTextStyle.of(context).style, children: [ - TextSpan(text: prefs.getInt("ratingMode") == 2 ? "${f2.format(tlData.rating)} TR • % ${t.rank}: ${tlData.percentileRank.toUpperCase()}" : "${t.top} ${f2.format(tlData.percentile * 100)}% (${tlData.percentileRank.toUpperCase()})"), + TextSpan(text: prefs.getInt("ratingMode") == 2 ? "${f2.format(tlData.tr)} TR • % ${t.rank}: ${tlData.percentileRank.toUpperCase()}" : "${t.top} ${f2.format(tlData.percentile * 100)}% (${tlData.percentileRank.toUpperCase()})"), if (tlData.bestRank != "z") const TextSpan(text: " • "), if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"), if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"), - TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.rating)} TR • RD: " : "Glicko: ${tlData.glicko != null ? f2.format(tlData.glicko) : "---"}±"}"), + TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.tr)} TR • RD: " : "Glicko: ${tlData.glicko != null ? 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)) diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 4cf6f95..0731e10 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -20,16 +20,15 @@ import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; var intFDiff = NumberFormat("+#,###.000;-#,###.000"); class TLThingy extends StatefulWidget { - final TetraLeagueAlpha tl; + final TetraLeague tl; final String userID; final List states; final bool showTitle; final bool bot; - final bool hidePreSeasonThingy; final bool guest; final double? topTR; final PlayerLeaderboardPosition? lbPositions; - final TetraLeagueAlpha? averages; + final TetraLeague? averages; final double? thatRankCutoff; final double? thatRankCutoffGlicko; final double? thatRankTarget; @@ -37,7 +36,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.hidePreSeasonThingy=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.topTR, this.lbPositions, this.averages, this.nextRankCutoff, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget, this.lastMatchPlayed}); @override State createState() => _TLThingyState(); @@ -45,13 +44,10 @@ class TLThingy extends StatefulWidget { class _TLThingyState extends State with TickerProviderStateMixin { late bool oskKagariGimmick; - late TetraLeagueAlpha? oldTl; - late TetraLeagueAlpha currentTl; + late TetraLeague? oldTl; + late TetraLeague currentTl; late RangeValues _currentRangeValues; late List sortedStates; - late Timer _countdownTimer; - //Duration seasonLeft = seasonEnd.difference(DateTime.now()); - Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override void initState() { @@ -60,20 +56,10 @@ class _TLThingyState extends State with TickerProviderStateMixin { oldTl = sortedStates.elementAtOrNull(1)?.tlSeason1; currentTl = widget.tl; super.initState(); - _countdownTimer = Timer.periodic( - Durations.extralong4, - (Timer timer) { - setState(() { - //seasonLeft = seasonEnd.difference(DateTime.now()); - postSeasonLeft = seasonStart.difference(DateTime.now()); - }); - }, - ); } @override void dispose() { - _countdownTimer.cancel(); super.dispose(); } @@ -84,47 +70,6 @@ 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; diff --git a/pubspec.yaml b/pubspec.yaml index 260bb80..f8a3f3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.2+22 +version: 1.6.3+29 environment: sdk: '>=3.0.0' diff --git a/res/tetrio_tl_alpha_ranks/x+.png b/res/tetrio_tl_alpha_ranks/x+.png new file mode 100644 index 0000000000000000000000000000000000000000..f23e262cc9cd66c7b3dd86b7104058af4748ab2b GIT binary patch literal 221023 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%0Af|7r$M7+fl4f{2w8e*ltH>3%t# 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 z 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 literal 0 HcmV?d00001 diff --git a/test/api_test.dart b/test/api_test.dart index 4f4f8d6..b05b70a 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -42,7 +42,7 @@ // expect(dan63047.tlSeason1.glicko != null, true); // //expect(dan63047.tlSeason1.rank != "z", true); lol // expect(dan63047.tlSeason1.percentileRank != "z", true); -// expect(dan63047.tlSeason1.rating > -1, true); +// expect(dan63047.tlSeason1.tr > -1, true); // expect(dan63047.tlSeason1.gamesPlayed > 9, true); // expect(dan63047.tlSeason1.gamesWon > 0, true); // //expect(dan63047.tlSeason1.standing, -1); @@ -70,7 +70,7 @@ // expect(osk.tlSeason1.glicko != null, true); // expect(osk.tlSeason1.rank == "z", true); // expect(osk.tlSeason1.percentileRank != "z", true); -// expect(osk.tlSeason1.rating > -1, true); +// expect(osk.tlSeason1.tr > -1, true); // expect(osk.tlSeason1.gamesPlayed > 9, true); // expect(osk.tlSeason1.gamesWon > 0, true); // expect(osk.tlSeason1.standing, -1); @@ -102,7 +102,7 @@ // expect(kagari.tlSeason1.glicko, null); // expect(kagari.tlSeason1.rank, "z"); // expect(kagari.tlSeason1.percentileRank, "z"); -// expect(kagari.tlSeason1.rating, -1); +// expect(kagari.tlSeason1.tr, -1); // expect(kagari.tlSeason1.decaying, false); // expect(kagari.tlSeason1.gamesPlayed, 0); // expect(kagari.tlSeason1.gamesWon, 0); @@ -133,7 +133,7 @@ // expect(furry.tlSeason1.glicko, null); // expect(furry.tlSeason1.rank, "z"); // expect(furry.tlSeason1.percentileRank, "z"); -// expect(furry.tlSeason1.rating, -1); +// expect(furry.tlSeason1.tr, -1); // expect(furry.tlSeason1.decaying, false); // expect(furry.tlSeason1.gamesPlayed, 0); // expect(furry.tlSeason1.gamesWon, 0); @@ -163,7 +163,7 @@ // expect(oskwarefan.tlSeason1.glicko, null); // expect(oskwarefan.tlSeason1.rank, "z"); // expect(oskwarefan.tlSeason1.percentileRank, "z"); -// expect(oskwarefan.tlSeason1.rating, -1); +// expect(oskwarefan.tlSeason1.tr, -1); // expect(oskwarefan.tlSeason1.decaying, true); // ??? why true? // expect(oskwarefan.tlSeason1.gamesPlayed, 0); // expect(oskwarefan.tlSeason1.gamesWon, 0); From 67da831cd2277eeec1158a312f53c5b9d8ee6338 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 18 Aug 2024 02:39:20 +0300 Subject: [PATCH 22/33] 1.6.4, many fixes --- lib/data_objects/tetra_stats.dart | 4 +- lib/data_objects/tetrio.dart | 56 +++++++----- lib/services/custom_http_client.dart | 3 + lib/services/tetrio_crud.dart | 44 ++++++---- lib/views/main_view.dart | 63 ++++++------- lib/views/main_view_tiles.dart | 10 +-- lib/views/ranks_averages_view.dart | 127 +++++++++++++++++++++------ lib/widgets/tl_progress_bar.dart | 2 +- lib/widgets/zenith_thingy.dart | 3 +- pubspec.yaml | 2 +- 10 files changed, 208 insertions(+), 106 deletions(-) diff --git a/lib/data_objects/tetra_stats.dart b/lib/data_objects/tetra_stats.dart index e5af9cf..e90c8dd 100644 --- a/lib/data_objects/tetra_stats.dart +++ b/lib/data_objects/tetra_stats.dart @@ -1,10 +1,12 @@ // p1nkl0bst3r data objects class Cutoffs{ + DateTime ts; Map tr; Map glicko; + Map gxe; - Cutoffs(this.tr, this.glicko); + Cutoffs(this.ts, this.tr, this.glicko, this.gxe); } class TopTr{ diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 1683e1b..79def98 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -42,26 +42,26 @@ const Map rankCutoffs = { "z": -1, "": 0.5 }; -const Map rankTargets = { - "x": 24503.75, // where that comes from? - "u": 23038, - "ss": 21583, - "s+": 20128, - "s": 18673, - "s-": 16975, - "a+": 15035, - "a": 13095, - "a-": 11155, - "b+": 9215, - "b": 7275, - "b-": 5335, - "c+": 3880, - "c": 2425, - "c-": 1213, - "d+": 606, - "d": 0, -}; -DateTime seasonStart = DateTime.utc(2024, 08, 16, 18); +// const Map rankTargets = { +// "x": 24503.75, // where that comes from? +// "u": 23038, +// "ss": 21583, +// "s+": 20128, +// "s": 18673, +// "s-": 16975, +// "a+": 15035, +// "a": 13095, +// "a-": 11155, +// "b+": 9215, +// "b": 7275, +// "b-": 5335, +// "c+": 3880, +// "c": 2425, +// "c-": 1213, +// "d+": 606, +// "d": 0, +// }; +// DateTime seasonStart = DateTime.utc(2024, 08, 16, 18); //DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15); enum Stats { tr, @@ -123,7 +123,8 @@ const Map chartsShortTitles = { Stats.openerMinusInfDS: "Opener - Inf. DS" }; -const Map rankColors = { // thanks osk for const rankColors at https://ch.tetr.io/res/js/base.js:418 +const Map rankColors = { // thanks osk for const rankColors at https://ch.tetr.io/res/js/base.js:458 + 'x+': Color(0xFF643C8D), 'x': Color(0xFFFF45FF), 'u': Color(0xFFFF3813), 'ss': Color(0xFFDB8B1F), @@ -1422,10 +1423,10 @@ class TetraLeague { timestamp = ts; gamesPlayed = json['gamesplayed'] ?? 0; gamesWon = json['gameswon'] ?? 0; - tr = json['tr'] != null ? json['tr'].toDouble() : -1; + tr = json['tr'] != null ? json['tr'].toDouble() : json['rating'] != null ? json['rating'].toDouble() : -1; glicko = json['glicko']?.toDouble(); rd = json['rd'] != null ? json['rd']!.toDouble() : noTrRd; - gxe = json['gxe'].toDouble(); + gxe = json['gxe'] != null ? json['gxe'].toDouble() : -1; rank = json['rank'] != null ? json['rank']!.toString() : 'z'; bestRank = json['bestrank'] != null ? json['bestrank']!.toString() : 'z'; apm = json['apm']?.toDouble(); @@ -1721,6 +1722,11 @@ class TetrioPlayersLeaderboard { TetrioPlayersLeaderboard(this.type, this.leaderboard); + @override + String toString(){ + return "$type leaderboard: ${leaderboard.length} players"; + } + List getStatRanking(List leaderboard, Stats stat, {bool reversed = false, String country = ""}){ List lb = List.from(leaderboard); if (country.isNotEmpty){ @@ -2425,6 +2431,10 @@ class TetrioPlayersLeaderboard { leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts)); } } + + addPlayers(List list){ + leaderboard.addAll(list); + } } class TetrioPlayerFromLeaderboard { diff --git a/lib/services/custom_http_client.dart b/lib/services/custom_http_client.dart index 004f2aa..e19b56e 100644 --- a/lib/services/custom_http_client.dart +++ b/lib/services/custom_http_client.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:http/http.dart' as http; class UserAgentClient extends http.BaseClient { @@ -9,6 +11,7 @@ class UserAgentClient extends http.BaseClient { @override Future send(http.BaseRequest request) { request.headers['user-agent'] = userAgent; + request.headers['X-Session-ID'] = "${Random().nextInt(1<<32)}"; return _inner.send(request); } } \ No newline at end of file diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 3a39315..05352ae 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -411,12 +411,7 @@ class TetrioService extends DB { Cutoffs? cached = _cache.get("", Cutoffs); if (cached != null) return cached; - Uri url; - if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLCutoffs"}); - } else { - url = Uri.https('api.p1nkl0bst3r.xyz', 'rankcutoff', {"users": null}); - } + Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/cutoffs.json', {"users": null}); try{ final response = await client.get(url); @@ -424,13 +419,14 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: Map rawData = jsonDecode(response.body); - Map data = rawData["cutoffs"] as Map; - Cutoffs result = Cutoffs({}, {}); + Map data = rawData["data"] as Map; + Cutoffs result = Cutoffs(DateTime.fromMillisecondsSinceEpoch(rawData["created"]), {}, {}, {}); for (String rank in data.keys){ - result.tr[rank] = data[rank]["rating"]; + result.tr[rank] = data[rank]["tr"]; result.glicko[rank] = data[rank]["glicko"]; + result.gxe[rank] = data[rank]["gxe"]; } - _cache.store(result, rawData["ts"] + 300000); + _cache.store(result, rawData["cache_until"]); return result; case 404: developer.log("fetchCutoffs: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); @@ -466,7 +462,7 @@ class TetrioService extends DB { if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLTopOne"}); } else { - url = Uri.https('ch.tetr.io', 'api/users/lists/league', {"after": "25000", "limit": "1"}); + url = Uri.https('ch.tetr.io', 'api/users/by/league', {"after": "25000:0:0", "limit": "1"}); } try{ @@ -475,7 +471,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: var rawJson = jsonDecode(response.body); - TetrioPlayerFromLeaderboard result = TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["users"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"])); + TetrioPlayerFromLeaderboard result = TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["entries"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"])); _cache.store(result, rawJson["cache"]["cached_until"]); return result; case 404: @@ -640,15 +636,17 @@ class TetrioService extends DB { } /// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve. - Future fetchTLLeaderboard() async { - TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); + Future fetchTLLeaderboard({double? after}) async { + TetrioPlayersLeaderboard? cached = _cache.get("league${after != null ? after.toString() : ""}", TetrioPlayersLeaderboard); if (cached != null) return cached; - Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); } else { - url = Uri.https('ch.tetr.io', 'api/users/by/league'); + url = Uri.https('ch.tetr.io', 'api/users/by/league', { + "limit": "100", + if (after != null) "after": "$after:0:0" + }); } try{ final response = await client.get(url); @@ -688,6 +686,20 @@ class TetrioService extends DB { } } + Stream fetchFullLeaderboard() async* { + late double after; + int lbLength = 100; + TetrioPlayersLeaderboard leaderboard = await fetchTLLeaderboard(); + after = leaderboard.leaderboard.last.tr; + while (lbLength == 100){ + TetrioPlayersLeaderboard pseudoLb = await fetchTLLeaderboard(after: after); + leaderboard.addPlayers(pseudoLb.leaderboard); + lbLength = pseudoLb.leaderboard.length; + after = pseudoLb.leaderboard.last.tr; + yield leaderboard; + } + } + // i want to know progress, so i trying to figure out this thing: // Stream fetchTLLeaderboardAsStream() async { // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 3fa3e66..1fbdc79 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -11,6 +11,7 @@ import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show prefs, teto; @@ -156,8 +157,8 @@ class _MainState extends State with TickerProviderStateMixin { if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) await windowManager.setTitle(title); // Requesting Tetra League (alpha), records, news and top TR of player - late List requests; - late Summaries summaries; + List requests; + Summaries summaries = await teto.fetchSummaries(_searchFor); late TetraLeagueBetaStream tlStream; late News news; // late SingleplayerStream recentSprint; @@ -166,20 +167,20 @@ class _MainState extends State with TickerProviderStateMixin { // late SingleplayerStream blitz; late SingleplayerStream recentZenith; late SingleplayerStream recentZenithEX; - // late TetrioPlayerFromLeaderboard? topOne; + late TetrioPlayerFromLeaderboard? topOne; // late TopTr? topTR; - requests = await Future.wait([ // all at once (8 requests to oskware in total) + requests = await Future.wait([ teto.fetchSummaries(_searchFor), teto.fetchTLStream(_searchFor), teto.fetchNews(_searchFor), teto.fetchStream(_searchFor, "zenith/recent"), teto.fetchStream(_searchFor, "zenithex/recent"), - //teto.fetchStream(_searchFor, "40l/top"), - //teto.fetchStream(_searchFor, "blitz/top"), + teto.fetchCutoffs(), + (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), ]); //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 + + //(summaries.league.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR summaries = requests[0] as Summaries; tlStream = requests[1] as TetraLeagueBetaStream; // records = requests[1] as UserRecords; @@ -189,7 +190,7 @@ class _MainState extends State with TickerProviderStateMixin { // recent = requests[3] as SingleplayerStream; // sprint = requests[4] as SingleplayerStream; // blitz = requests[5] as SingleplayerStream; - // topOne = requests[7] as TetrioPlayerFromLeaderboard?; + topOne = requests[6] as TetrioPlayerFromLeaderboard?; // topTR = requests[8] as TopTr?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); @@ -202,17 +203,17 @@ 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 = (requests[5] as Cutoffs?)?.tr; + Map? cutoffsGlicko = (requests[5] 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?.tr??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 (summaries.league.gamesPlayed > 9) { + thatRankCutoff = cutoffs?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank]; + thatRankGlickoCutoff = cutoffsGlicko?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank]; + nextRankCutoff = (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? topOne?.tr??25000 : cutoffs?[ranks.elementAtOrNull(ranks.indexOf(summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank)+1)]; + nextRankGlickoCutoff = (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank)+1)]; + } - // if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; + // if (everyone != null && summaries.league.gamesPlayed > 9) rankAverages = everyone?.averages[summaries.league.percentileRank]?[0]; // Making list of Tetra League matches bool isTracking = await teto.isPlayerTracking(me.userId); @@ -270,7 +271,7 @@ class _MainState extends State with TickerProviderStateMixin { } } - //states.addAll(await teto.getPlayer(me.userId)); + states.addAll(await teto.getPlayer(me.userId)); for (var element in states) { // For graphs I need only unique entries if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); @@ -475,12 +476,12 @@ class _MainState extends State with TickerProviderStateMixin { //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, + thatRankCutoff: thatRankCutoff, + thatRankCutoffGlicko: thatRankGlickoCutoff, + //thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, + nextRankCutoff: nextRankCutoff, + nextRankCutoffGlicko: nextRankGlickoCutoff, + //nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, //averages: rankAverages, //lbPositions: meAmongEveryone ), @@ -516,12 +517,12 @@ class _MainState extends State with TickerProviderStateMixin { //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, + thatRankCutoff: thatRankCutoff, + thatRankCutoffGlicko: thatRankGlickoCutoff, + //thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, + nextRankCutoff: nextRankCutoff, + nextRankCutoffGlicko: nextRankGlickoCutoff, + //nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, //averages: rankAverages, //lbPositions: meAmongEveryone ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 4e04035..da979c4 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -170,7 +170,7 @@ class DestinationLeaderboards extends StatefulWidget{ class _DestinationLeaderboardsState extends State { Cards rightCard = Cards.tetraLeague; - Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); final List leaderboards = ["Tetra League", "Quick Play", "Quick Play Expert"]; @override @@ -245,7 +245,7 @@ class _DestinationGraphsState extends State { final List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; int _chartsIndex = 0; late List>> chartsData; - Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override void initState(){ @@ -611,7 +611,7 @@ class RecordSummary extends StatelessWidget{ class _DestinationHomeState extends State { Cards rightCard = Cards.overview; CardMod cardMod = CardMod.info; - Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); late Map>> modeButtons; late MapEntry? closestAverageBlitz; late bool blitzBetterThanClosestAverage; @@ -849,7 +849,7 @@ class _DestinationHomeState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) ], ), ), @@ -1021,7 +1021,7 @@ class _DestinationHomeState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), + //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), ], ), ), diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index dbfa0e9..78c5bbd 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -1,9 +1,14 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetra_stats.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/compare_view.dart'; import 'package:tetra_stats/views/rank_averages_view.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:window_manager/window_manager.dart'; import 'package:tetra_stats/main.dart' show teto; @@ -17,14 +22,9 @@ class RankAveragesView extends StatefulWidget { late String oldWindowTitle; class RanksAverages extends State { - Map> averages = {}; @override void initState() { - teto.fetchTLLeaderboard().then((value){ - averages = value.averages; - setState(() {}); - }); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); windowManager.setTitle("Tetra Stats: ${t.rankAveragesViewTitle}"); @@ -40,35 +40,110 @@ class RanksAverages extends State { @override Widget build(BuildContext context) { + bool bigScreen = MediaQuery.of(context).size.width >= 700; return Scaffold( appBar: AppBar( title: Text(t.rankAveragesViewTitle), ), backgroundColor: Colors.black, body: SafeArea( - child: averages.isEmpty ? const Center(child: Text('Fetching...')) : ListView.builder( - itemCount: averages.length, - itemBuilder: (context, index){ - List keys = averages.keys.toList(); - return ListTile( - leading: Image.asset("res/tetrio_tl_alpha_ranks/${keys[index]}.png", height: 48), - title: Text(t.players(n: averages[keys[index]]?[1]["players"]), style: const TextStyle(fontFamily: "Eurostile Round Extended")), - subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM", - style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey, fontSize: 13)), - trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: const TextStyle(fontSize: 28, fontFamily: "Eurostile Round")), - onTap: (){ - if (averages[keys[index]]?[1]["players"] > 0) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RankView(rank: averages[keys[index]]!), + child: FutureBuilder(future: teto.fetchCutoffs(), builder: (context, snapshot){ + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator(color: Colors.white)); + case ConnectionState.done: + if (snapshot.hasData){ + return Container( + alignment: Alignment.center, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Container( + alignment: Alignment.center, + width: MediaQuery.of(context).size.width, + constraints: const BoxConstraints(maxWidth: 900, minWidth: 610), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all(color: Colors.grey.shade900), + columnWidths: const {0: FixedColumnWidth(48)}, + children: [ + TableRow( + children: [ + Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("Glicko", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("Glixare", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("S1 TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + ] + ), + for (String rank in snapshot.data!.tr.keys) TableRow( + decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(100), rankColors[rank]!.withAlpha(200)])), + children: [ + Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.tr[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.glicko[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.gxe[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(f2.format(snapshot.data!.gxe[rank]!*250), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + ] + ) + ], + ), + Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.ts))) + ], ), - ); - } - }, + ), + ), + ), ); - }) - ), + } + if (snapshot.hasError){ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + if (snapshot.stackTrace != null) Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), + ), + ], + ) + ); + } + return const Text("end of FutureBuilder"); + } + }) + ), ); } } diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index b59b181..8a1be3c 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -61,7 +61,7 @@ class TLProgress extends StatelessWidget{ if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.tr)}) TR"), if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), - if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) + if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x+" && tlData.rank != "z") || tlData.percentileRank != "x+"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x+")) ? Colors.greenAccent : null)) ] ) ) diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index 0c0164e..0a9394c 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -148,7 +148,7 @@ class _ZenithThingyState extends State { const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), Padding( padding: const EdgeInsets.only(left: 10.0), - child: Text("${getMoreNormalTime(record!.stats.finalTime)}%", style: TextStyle( + child: Text("${getMoreNormalTime(record!.stats.finalTime)}", style: TextStyle( shadows: textShadow, fontFamily: "Eurostile Round Extended", fontSize: 36, @@ -158,7 +158,6 @@ class _ZenithThingyState extends State { ) ], ), - Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), Table( columnWidths: const { 0: FixedColumnWidth(36) diff --git a/pubspec.yaml b/pubspec.yaml index f8a3f3e..bf95d9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.3+29 +version: 1.6.4+30 environment: sdk: '>=3.0.0' From e21ec84fc12b0a167aa7dee0cbbce55c3604e7d5 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 19 Aug 2024 01:02:04 +0300 Subject: [PATCH 23/33] i bringed another changes to tetris io stats --- lib/data_objects/tetrio.dart | 18 +++++++++--------- lib/services/custom_http_client.dart | 3 ++- lib/services/tetrio_crud.dart | 2 +- lib/views/main_view.dart | 6 +++--- lib/views/ranks_averages_view.dart | 2 +- pubspec.yaml | 2 +- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 79def98..ba79b51 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -340,10 +340,6 @@ class TetrioPlayer { return tlSeason1!.lessStrictCheck(other.tlSeason1!); } - TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard() => TetrioPlayerFromLeaderboard( - userId, username, role, xp, country, state, gamesPlayed, gamesWon, - tlSeason1!.tr, tlSeason1!.glicko??0, tlSeason1!.rd??noTrRd, tlSeason1!.rank, tlSeason1!.bestRank, tlSeason1!.apm??0, tlSeason1!.pps??0, tlSeason1!.vs??0, tlSeason1!.decaying); - @override String toString() { return "$username ($state)"; @@ -1453,6 +1449,10 @@ class TetraLeague { double? get esttracc => (estTr != null) ? estTr!.esttr - tr : null; + TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) => TetrioPlayerFromLeaderboard( + id, "", "user", -1, null, timestamp, gamesPlayed, gamesWon, + tr, glicko??0, rd??noTrRd, rank, bestRank, apm??0, pps??0, vs??0, decaying); + Map toJson() { final Map data = {}; if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed; @@ -2337,21 +2337,21 @@ class TetrioPlayersLeaderboard { } } - PlayerLeaderboardPosition? getLeaderboardPosition(TetrioPlayer user) { - if (user.tlSeason1?.gamesPlayed == 0) return null; + PlayerLeaderboardPosition? getLeaderboardPosition(Mapleague) { + if (league.values.first.gamesPlayed == 0) return null; bool fakePositions = false; late List copyOfLeaderboard; - if (leaderboard.indexWhere((element) => element.userId == user.userId) == -1){ + if (leaderboard.indexWhere((element) => element.userId == league.keys.first) == -1){ fakePositions =true; copyOfLeaderboard = List.of(leaderboard); - copyOfLeaderboard.add(user.convertToPlayerFromLeaderboard()); + copyOfLeaderboard.add(league.values.first.convertToPlayerFromLeaderboard(league.keys.first)); } List stats = [Stats.apm, Stats.pps, Stats.vs, Stats.gp, Stats.gw, Stats.wr, Stats.app, Stats.vsapm, Stats.dss, Stats.dsp, Stats.appdsp, Stats.cheese, Stats.gbe, Stats.nyaapp, Stats.area, Stats.eTR, Stats.acceTR]; List results = []; for (Stats stat in stats) { List sortedLeaderboard = getStatRanking(fakePositions ? copyOfLeaderboard : leaderboard, stat, reversed: stat == Stats.cheese ? true : false); - int position = sortedLeaderboard.indexWhere((element) => element.userId == user.userId) + 1; + int position = sortedLeaderboard.indexWhere((element) => element.userId == league.keys.first) + 1; if (position == 0) { results.add(null); } else { diff --git a/lib/services/custom_http_client.dart b/lib/services/custom_http_client.dart index e19b56e..c244a4f 100644 --- a/lib/services/custom_http_client.dart +++ b/lib/services/custom_http_client.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; class UserAgentClient extends http.BaseClient { @@ -11,7 +12,7 @@ class UserAgentClient extends http.BaseClient { @override Future send(http.BaseRequest request) { request.headers['user-agent'] = userAgent; - request.headers['X-Session-ID'] = "${Random().nextInt(1<<32)}"; + if (!kIsWeb) request.headers['X-Session-ID'] = "${Random().nextInt(1<<32)}"; return _inner.send(request); } } \ No newline at end of file diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 05352ae..d27a2e6 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -411,7 +411,7 @@ class TetrioService extends DB { Cutoffs? cached = _cache.get("", Cutoffs); if (cached != null) return cached; - Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/cutoffs.json', {"users": null}); + Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/cutoffs.json'); try{ final response = await client.get(url); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 1fbdc79..b160e99 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -199,7 +199,7 @@ class _MainState extends State with TickerProviderStateMixin { everyone = teto.getCachedLeaderboard(); everyone ??= await teto.fetchTLLeaderboard(); if (meAmongEveryone == null && everyone!.leaderboard.isNotEmpty){ - meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); + meAmongEveryone = await compute(everyone!.getLeaderboardPosition, {me.userId: summaries.league}); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } } @@ -216,7 +216,7 @@ class _MainState extends State with TickerProviderStateMixin { // if (everyone != null && summaries.league.gamesPlayed > 9) rankAverages = everyone?.averages[summaries.league.percentileRank]?[0]; // Making list of Tetra League matches - bool isTracking = await teto.isPlayerTracking(me.userId); + //bool isTracking = await teto.isPlayerTracking(me.userId); List states = []; TetraLeague? compareWith; Set uniqueTL = {}; @@ -274,7 +274,7 @@ 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 (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); - if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); + if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); } // Also i need previous Tetra League State for comparison if avaliable if (uniqueTL.length >= 2){ diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index 78c5bbd..1749c6a 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -108,7 +108,7 @@ class RanksAverages extends State { ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.gxe[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + child: Text(f3.format(snapshot.data!.gxe[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), diff --git a/pubspec.yaml b/pubspec.yaml index bf95d9e..ea53554 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.4+30 +version: 1.6.5+31 environment: sdk: '>=3.0.0' From c0d395235b318c701edc70b245addd5e069e19c0 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 19 Aug 2024 20:59:25 +0300 Subject: [PATCH 24/33] Full leaderboard - full capabilities --- lib/data_objects/tetrio.dart | 54 +++++- lib/services/tetrio_crud.dart | 56 +++--- lib/views/main_view.dart | 10 +- lib/views/rank_averages_view.dart | 8 +- lib/views/tl_leaderboard_view.dart | 291 +++++++++++++++-------------- pubspec.yaml | 2 +- 6 files changed, 234 insertions(+), 187 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index ba79b51..df4dfbe 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -66,6 +66,8 @@ const Map rankCutoffs = { enum Stats { tr, glicko, + gxe, + s1tr, rd, gp, gw, @@ -95,6 +97,8 @@ enum Stats { const Map chartsShortTitles = { Stats.tr: "TR", + Stats.gxe: "Glixare", + Stats.s1tr: "S1 TR", Stats.glicko: "Glicko", Stats.rd: "RD", Stats.gp: "GP", @@ -351,6 +355,10 @@ class TetrioPlayer { return tlSeason1?.tr; case Stats.glicko: return tlSeason1?.glicko; + case Stats.gxe: + return tlSeason1?.gxe; + case Stats.s1tr: + return tlSeason1?.s1tr; case Stats.rd: return tlSeason1?.rd; case Stats.gp: @@ -1414,6 +1422,7 @@ class TetraLeague { } double get winrate => gamesWon / gamesPlayed; + double get s1tr => gxe * 250; TetraLeague.fromJson(Map json, ts) { timestamp = ts; @@ -1451,7 +1460,7 @@ class TetraLeague { TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) => TetrioPlayerFromLeaderboard( id, "", "user", -1, null, timestamp, gamesPlayed, gamesWon, - tr, glicko??0, rd??noTrRd, rank, bestRank, apm??0, pps??0, vs??0, decaying); + tr, gxe, glicko??0, rd??noTrRd, rank, bestRank, apm??0, pps??0, vs??0, decaying); Map toJson() { final Map data = {}; @@ -1757,6 +1766,7 @@ class TetrioPlayersLeaderboard { avgPPS = 0, avgVS = 0, avgTR = 0, + avgGlixare = 0, avgGlicko = 0, avgRD = 0, avgAPP = 0, @@ -1775,6 +1785,7 @@ class TetrioPlayersLeaderboard { avgStride = 0, avgInfDS = 0, lowestTR = 25000, + lowestGlixare = double.infinity, lowestGlicko = double.infinity, lowestRD = double.infinity, lowestWinrate = double.infinity, @@ -1797,6 +1808,7 @@ class TetrioPlayersLeaderboard { lowestStride = double.infinity, lowestInfDS = double.infinity, highestTR = double.negativeInfinity, + highestGlixare = double.negativeInfinity, highestGlicko = double.negativeInfinity, highestRD = double.negativeInfinity, highestWinrate = double.negativeInfinity, @@ -1827,6 +1839,7 @@ class TetrioPlayersLeaderboard { highestGamesPlayed = 0, highestGamesWon = 0; String lowestTRid = "", lowestTRnick = "", + lowestGlixareID = "", lowestGlixareNick = "", lowestGlickoID = "", lowestGlickoNick = "", lowestRdID = "", lowestRdNick = "", lowestGamesPlayedID = "", lowestGamesPlayedNick = "", @@ -1851,6 +1864,7 @@ class TetrioPlayersLeaderboard { lowestStrideID = "", lowestStrideNick = "", lowestInfDSid = "", lowestInfDSnick = "", highestTRid = "", highestTRnick = "", + highestGlixareID = "", highestGlixareNick = "", highestGlickoID = "", highestGlickoNick = "", highestRdID = "", highestRdNick = "", highestGamesPlayedID = "", highestGamesPlayedNick = "", @@ -1879,6 +1893,7 @@ class TetrioPlayersLeaderboard { avgPPS += entry.pps; avgVS += entry.vs; avgTR += entry.tr; + avgGlixare += entry.gxe; if (entry.glicko != null) avgGlicko += entry.glicko!; if (entry.rd != null) avgRD += entry.rd!; avgAPP += entry.nerdStats.app; @@ -1903,6 +1918,11 @@ class TetrioPlayersLeaderboard { lowestTRid = entry.userId; lowestTRnick = entry.username; } + if (entry.gxe < lowestGlixare){ + lowestGlixare = entry.gxe; + lowestGlixareID = entry.userId; + lowestGlixareNick = entry.username; + } if (entry.glicko != null && entry.glicko! < lowestGlicko){ lowestGlicko = entry.glicko!; lowestGlickoID = entry.userId; @@ -2023,6 +2043,11 @@ class TetrioPlayersLeaderboard { highestTRid = entry.userId; highestTRnick = entry.username; } + if (entry.gxe > highestGlixare){ + highestGlixare = entry.gxe; + highestGlixareID = entry.userId; + highestGlixareNick = entry.username; + } if (entry.glicko != null && entry.glicko! > highestGlicko){ highestGlicko = entry.glicko!; highestGlickoID = entry.userId; @@ -2143,6 +2168,7 @@ class TetrioPlayersLeaderboard { avgPPS /= filtredLeaderboard.length; avgVS /= filtredLeaderboard.length; avgTR /= filtredLeaderboard.length; + avgGlixare /= filtredLeaderboard.length; avgGlicko /= filtredLeaderboard.length; avgRD /= filtredLeaderboard.length; avgAPP /= filtredLeaderboard.length; @@ -2162,7 +2188,7 @@ class TetrioPlayersLeaderboard { avgInfDS /= filtredLeaderboard.length; avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor(); avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor(); - return [TetraLeague(timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, gxe: -1, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), + return [TetraLeague(timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, gxe: avgGlixare, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), { "everyone": rank == "", "totalGamesPlayed": totalGamesPlayed, @@ -2171,6 +2197,12 @@ class TetrioPlayersLeaderboard { "lowestTR": lowestTR, "lowestTRid": lowestTRid, "lowestTRnick": lowestTRnick, + "lowestGlixare": lowestGlixare, + "lowestGlixareID": lowestGlixareID, + "lowestGlixareNick": lowestGlixareNick, + "lowestS1tr": lowestGlixare * 250, + "lowestS1trID": lowestGlixareID, + "lowestS1trNick": lowestGlixareNick, "lowestGlicko": lowestGlicko, "lowestGlickoID": lowestGlickoID, "lowestGlickoNick": lowestGlickoNick, @@ -2243,6 +2275,12 @@ class TetrioPlayersLeaderboard { "highestTR": highestTR, "highestTRid": highestTRid, "highestTRnick": highestTRnick, + "highestGlixare": highestGlixare, + "highestGlixareID": highestGlixareID, + "highestGlixareNick": highestGlixareNick, + "highestS1tr": highestGlixare * 250, + "highestS1trID": highestGlixareID, + "highestS1trNick": highestGlixareNick, "highestGlicko": highestGlicko, "highestGlickoID": highestGlickoID, "highestGlickoNick": highestGlickoNick, @@ -2327,8 +2365,8 @@ class TetrioPlayersLeaderboard { "avgPlonk": avgPlonk, "avgStride": avgStride, "avgInfDS": avgInfDS, - "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()].tr : lowestTR, - "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()].glicko : 0, + "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].tr : lowestTR, + "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0, "entries": filtredLeaderboard }]; }else{ @@ -2447,6 +2485,7 @@ class TetrioPlayerFromLeaderboard { late int gamesPlayed; late int gamesWon; late double tr; + late double gxe; late double? glicko; late double? rd; late String rank; @@ -2469,6 +2508,7 @@ class TetrioPlayerFromLeaderboard { this.gamesPlayed, this.gamesWon, this.tr, + this.gxe, this.glicko, this.rd, this.rank, @@ -2484,6 +2524,7 @@ class TetrioPlayerFromLeaderboard { double get winrate => gamesWon / gamesPlayed; double get esttracc => estTr.esttr - tr; + double get s1tr => gxe * 250; TetrioPlayerFromLeaderboard.fromJson(Map json, DateTime ts) { userId = json['_id']; @@ -2495,6 +2536,7 @@ class TetrioPlayerFromLeaderboard { gamesPlayed = json['league']['gamesplayed'] as int; gamesWon = json['league']['gameswon'] as int; tr = json['league']['tr'] != null ? json['league']['tr'].toDouble() : 0; + gxe = json['league']['gxe']??-1; glicko = json['league']['glicko']?.toDouble(); rd = json['league']['rd']?.toDouble(); rank = json['league']['rank']; @@ -2514,6 +2556,10 @@ class TetrioPlayerFromLeaderboard { return tr; case Stats.glicko: return glicko??-1; + case Stats.gxe: + return gxe; + case Stats.s1tr: + return s1tr; case Stats.rd: return rd??-1; case Stats.gp: diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index d27a2e6..4e8da1c 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -636,18 +636,12 @@ class TetrioService extends DB { } /// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve. - Future fetchTLLeaderboard({double? after}) async { - TetrioPlayersLeaderboard? cached = _cache.get("league${after != null ? after.toString() : ""}", TetrioPlayersLeaderboard); + Future fetchTLLeaderboard() async { + TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); if (cached != null) return cached; - Uri url; - if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); - } else { - url = Uri.https('ch.tetr.io', 'api/users/by/league', { - "limit": "100", - if (after != null) "after": "$after:0:0" - }); - } + + Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/leaderboard.json'); + try{ final response = await client.get(url); @@ -655,16 +649,10 @@ class TetrioService extends DB { case 200: _lbPositions.clear(); var rawJson = jsonDecode(response.body); - if (rawJson['success']) { // if api confirmed that everything ok - 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']); - return leaderboard; - } else { // idk how to hit that one - developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); - throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side? - } + TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['created'])); + developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + _cache.store(leaderboard, rawJson['cache_until']); + return leaderboard; case 403: throw TetrioForbidden(); case 429: @@ -686,19 +674,19 @@ class TetrioService extends DB { } } - Stream fetchFullLeaderboard() async* { - late double after; - int lbLength = 100; - TetrioPlayersLeaderboard leaderboard = await fetchTLLeaderboard(); - after = leaderboard.leaderboard.last.tr; - while (lbLength == 100){ - TetrioPlayersLeaderboard pseudoLb = await fetchTLLeaderboard(after: after); - leaderboard.addPlayers(pseudoLb.leaderboard); - lbLength = pseudoLb.leaderboard.length; - after = pseudoLb.leaderboard.last.tr; - yield leaderboard; - } - } + // Stream fetchFullLeaderboard() async* { + // late double after; + // int lbLength = 100; + // TetrioPlayersLeaderboard leaderboard = await fetchTLLeaderboard(); + // after = leaderboard.leaderboard.last.tr; + // while (lbLength == 100){ + // TetrioPlayersLeaderboard pseudoLb = await fetchTLLeaderboard(after: after); + // leaderboard.addPlayers(pseudoLb.leaderboard); + // lbLength = pseudoLb.leaderboard.length; + // after = pseudoLb.leaderboard.last.tr; + // yield leaderboard; + // } + // } // i want to know progress, so i trying to figure out this thing: // Stream fetchTLLeaderboardAsStream() async { diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index b160e99..df6fe00 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -213,7 +213,7 @@ class _MainState extends State with TickerProviderStateMixin { nextRankGlickoCutoff = (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank)+1)]; } - // if (everyone != null && summaries.league.gamesPlayed > 9) rankAverages = everyone?.averages[summaries.league.percentileRank]?[0]; + if (everyone != null && summaries.league.gamesPlayed > 9) rankAverages = everyone?.averages[summaries.league.percentileRank]?[0]; // Making list of Tetra League matches //bool isTracking = await teto.isPlayerTracking(me.userId); @@ -482,8 +482,8 @@ class _MainState extends State with TickerProviderStateMixin { nextRankCutoff: nextRankCutoff, nextRankCutoffGlicko: nextRankGlickoCutoff, //nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, - //averages: rankAverages, - //lbPositions: meAmongEveryone + averages: rankAverages, + lbPositions: meAmongEveryone ), ), SizedBox( @@ -523,8 +523,8 @@ class _MainState extends State with TickerProviderStateMixin { nextRankCutoff: nextRankCutoff, nextRankCutoffGlicko: nextRankGlickoCutoff, //nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, - //averages: rankAverages, - //lbPositions: meAmongEveryone + averages: rankAverages, + lbPositions: meAmongEveryone ), _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![1].league.gamesPlayed > 0), diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 4ff0446..557bda3 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -379,6 +379,8 @@ class RankState extends State with SingleTickerProviderStateMixin { child: ListView( children: [ _ListEntry(value: widget.rank[1]["lowestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestTRid"], username: widget.rank[1]["lowestTRnick"], approximate: false, fractionDigits: 2), + _ListEntry(value: widget.rank[1]["lowestGlixare"], label: "Glixare", id: widget.rank[1]["lowestGlixareID"], username: widget.rank[1]["lowestGlixareNick"], approximate: false, fractionDigits: 3), + _ListEntry(value: widget.rank[1]["lowestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["lowestS1trID"], username: widget.rank[1]["lowestS1trNick"], approximate: false, fractionDigits: 2), _ListEntry(value: widget.rank[1]["lowestGlicko"], label: "Glicko", id: widget.rank[1]["lowestGlickoID"], username: widget.rank[1]["lowestGlickoNick"], approximate: false, fractionDigits: 2), _ListEntry(value: widget.rank[1]["lowestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestRdID"], username: widget.rank[1]["lowestRdNick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["lowestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGamesPlayedID"], username: widget.rank[1]["lowestGamesPlayedNick"], approximate: false), @@ -413,6 +415,8 @@ class RankState extends State with SingleTickerProviderStateMixin { Expanded( child: ListView(children: [ _ListEntry(value: widget.rank[0].tr, label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), + _ListEntry(value: widget.rank[0].gxe, label: "Glixare", id: "", username: "", approximate: false, fractionDigits: 3), + _ListEntry(value: widget.rank[0].s1tr, label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: "", username: "", approximate: false, fractionDigits: 2), _ListEntry(value: widget.rank[0].glicko, label: "Glicko", id: "", username: "", approximate: true, fractionDigits: 2), _ListEntry(value: widget.rank[0].rd, label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[0].gamesPlayed, label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 0), @@ -446,6 +450,8 @@ class RankState extends State with SingleTickerProviderStateMixin { child: ListView( children: [ _ListEntry(value: widget.rank[1]["highestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestTRid"], username: widget.rank[1]["highestTRnick"], approximate: false, fractionDigits: 2), + _ListEntry(value: widget.rank[1]["highestGlixare"], label: "Glixare", id: widget.rank[1]["highestGlixareID"], username: widget.rank[1]["highestGlixareNick"], approximate: false, fractionDigits: 3), + _ListEntry(value: widget.rank[1]["highestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["highestS1trID"], username: widget.rank[1]["highestS1trNick"], approximate: false, fractionDigits: 2), _ListEntry(value: widget.rank[1]["highestGlicko"], label: "Glicko", id: widget.rank[1]["highestGlickoID"], username: widget.rank[1]["highestGlickoNick"], approximate: false, fractionDigits: 2), _ListEntry(value: widget.rank[1]["highestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestRdID"], username: widget.rank[1]["highestRdNick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["highestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGamesPlayedID"], username: widget.rank[1]["highestGamesPlayedNick"], approximate: false), @@ -517,7 +523,7 @@ class _ListEntry extends StatelessWidget { children: [ Text(f.format(value), style: const TextStyle(fontSize: 22, height: 0.9)), - if (id.isNotEmpty) Text(t.forPlayer(username: username)) + if (id.isNotEmpty) Text(t.forPlayer(username: username), style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w100),) ], ), onTap: id.isNotEmpty diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index b3c181d..0a3d87e 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/services/tetrio_crud.dart'; +import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/rank_averages_view.dart'; import 'package:tetra_stats/views/ranks_averages_view.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; -final TetrioService _teto = TetrioService(); List _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; Stats _sortBy = Stats.tr; bool reversed = false; @@ -64,148 +64,155 @@ class TLLeaderboardState extends State { ), backgroundColor: Colors.black, body: SafeArea( - child: FutureBuilder( - future: _teto.fetchTLLeaderboard(), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers != null ? allPlayers.length : 0)}"); - bool bigScreen = MediaQuery.of(context).size.width > 768; - return NestedScrollView( - headerSliverBuilder: (context, value) { - String howManyPlayers(int numberOfPlayers) => Intl.plural( - numberOfPlayers, - zero: t.lbViewZeroEntrys, - one: t.lbViewOneEntry, - other: t.lbViewManyEntrys(numberOfPlayers: t.players(n: numberOfPlayers)), - name: 'howManyPeople', - args: [numberOfPlayers], - desc: 'Description of how many people are seen in a place.', - examples: const {'numberOfPeople': 3}, - ); - return [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceBetween, - children: [ - Text( - howManyPlayers(allPlayers.length), - style: const TextStyle(color: Colors.white, fontSize: 25), - ), - TextButton(onPressed: (){ - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RankView(rank: snapshot.data!.getAverageOfRank("")), - ), - ); - }, child: Text(t.everyoneAverages, - style: const TextStyle(fontSize: 25))) - ],) - )), - SliverToBoxAdapter(child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 16, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.sortBy}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton(items: _itemStats, value: _sortBy, onChanged: ((value) { - _sortBy = value; - setState(() {}); - }),), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.reversed}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 5.5, 0, 7.5), - child: Checkbox(value: reversed, - checkColor: Colors.black, - onChanged: ((value) { - reversed = value!; - setState(() {}); - }),), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.country}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton(items: _itemCountries, value: _country, onChanged: ((value) { - _country = value; - setState(() {}); - }),), - ], - ), - ], - ), - ),), - const SliverToBoxAdapter(child: Divider()) - ]; - }, - body: ListView.builder( - itemCount: allPlayers!.length, - prototypeItem: ListTile( - leading: Text("0", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)), - title: Text("ehhh...", style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), - trailing: SizedBox(height: bigScreen ? 48 : 36, width: 1,), - subtitle: const Text("eh..."), + child: FutureBuilder( + future: teto.fetchTLLeaderboard(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers != null ? allPlayers.length : 0)}"); + bool bigScreen = MediaQuery.of(context).size.width > 768; + return NestedScrollView( + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceBetween, + children: [ + Text( + "${t.players(n: allPlayers.length)} • ${t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))}", + style: const TextStyle(color: Colors.white, fontSize: 25), + ), + TextButton(onPressed: (){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RankView(rank: snapshot.data!.getAverageOfRank("")), ), - itemBuilder: (context, index) { - return ListTile( - leading: Text( - (index+1).toString(), - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9) + ); + }, child: Text(t.everyoneAverages, + style: const TextStyle(fontSize: 25))) + ],) + )), + SliverToBoxAdapter(child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("${t.sortBy}: ", + style: const TextStyle(color: Colors.white, fontSize: 25)), + DropdownButton(items: _itemStats, value: _sortBy, onChanged: ((value) { + _sortBy = value; + setState(() {}); + }),), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("${t.reversed}: ", + style: const TextStyle(color: Colors.white, fontSize: 25)), + Padding( + padding: const EdgeInsets.fromLTRB(0, 5.5, 0, 7.5), + child: Checkbox(value: reversed, + checkColor: Colors.black, + onChanged: ((value) { + reversed = value!; + setState(() {}); + }),), ), - title: Text(allPlayers[index].username, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), - subtitle: (bigScreen || _sortBy != Stats.tr) ? Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", - style: TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: bigScreen ? null : 13, color: _sortBy == Stats.tr ? Colors.grey : null)) : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(allPlayers[index].tr)} TR", style: const TextStyle(fontSize: 28)), - Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36), - ], - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MainView(player: allPlayers[index].userId), - maintainState: false, - ), - ); - }, - ); - })); - } - })), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("${t.country}: ", + style: const TextStyle(color: Colors.white, fontSize: 25)), + DropdownButton(items: _itemCountries, value: _country, onChanged: ((value) { + _country = value; + setState(() {}); + }),), + ], + ), + ], + ), + ),), + const SliverToBoxAdapter(child: Divider()) + ]; + }, + body: ListView.builder( + itemCount: allPlayers!.length, + prototypeItem: ListTile( + leading: Text("0", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)), + title: Text("ehhh...", style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), + trailing: SizedBox(height: bigScreen ? 48 : 36, width: 1,), + subtitle: const Text("eh..."), + ), + itemBuilder: (context, index) { + return ListTile( + leading: Text( + (index+1).toString(), + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9) + ), + title: Text(allPlayers[index].username, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), + subtitle: (bigScreen || _sortBy != Stats.tr) ? Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", + style: TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: bigScreen ? null : 13, color: _sortBy == Stats.tr ? Colors.grey : null)) : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(allPlayers[index].tr)} TR", style: const TextStyle(fontSize: 28)), + Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36), + ], + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MainView(player: allPlayers[index].userId), + maintainState: false, + ), + ); + }, + ); + })); + } + if (snapshot.hasError){ + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + if (snapshot.stackTrace != null) Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), + ), + ], + ) + ); + } + return Text("end of FutureBuilder"); + } + })), ); } } diff --git a/pubspec.yaml b/pubspec.yaml index ea53554..bf491d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.5+31 +version: 1.6.6+32 environment: sdk: '>=3.0.0' From e5ffa9711e80809f169f35854e84e42436abb2db Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 20 Aug 2024 20:17:59 +0300 Subject: [PATCH 25/33] compare view fix (fix #129) + records for X+ ranks fix --- lib/data_objects/tetrio.dart | 3 +- lib/views/compare_view.dart | 968 ++++++++++++++++++++++----- lib/views/main_view.dart | 8 +- lib/widgets/singleplayer_record.dart | 12 +- pubspec.yaml | 2 +- 5 files changed, 817 insertions(+), 176 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index df4dfbe..277b5fa 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -18,7 +18,7 @@ const double vsapmWeight = 60; const double cheeseWeight = 1.25; const double gbeWeight = 315; const List ranks = [ - "d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x" + "d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x", "x+" ]; const Map rankCutoffs = { "x+": 0.002, @@ -1502,7 +1502,6 @@ class RecordSingle { RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); RecordSingle.fromJson(Map json, int ran, int cran) { - //developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio"); ownId = json['_id']; gamemode = json['gamemode']; stats = ResultsStats.fromJson(json['results']['stats']); diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index abc0a81..8c0df87 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -8,6 +8,7 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show teto; +import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/widgets/vs_graphs.dart'; import 'package:window_manager/window_manager.dart'; @@ -17,7 +18,7 @@ enum Mode{ averages } Mode greenSideMode = Mode.player; -List theGreenSide = [null, null, null]; // TetrioPlayer?, List>?, TetraLeague? +List theGreenSide = [null, null, null]; // TetrioPlayer?, List>?, Summary Mode redSideMode = Mode.player; List theRedSide = [null, null, null]; final DateFormat dateFormat = DateFormat.yMd(LocaleSettings.currentLocale.languageCode).add_Hm(); @@ -65,8 +66,9 @@ class CompareState extends State { if (user.startsWith("\$avg")){ try{ var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; + Summaries summary = Summaries("avg${user.substring(4).toLowerCase()}", average, TetrioZen(level: 0, score: 0)); redSideMode = Mode.averages; - theRedSide = [null, null, average]; + theRedSide = [null, null, summary]; return setState(() {}); }on Exception { if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); @@ -82,7 +84,7 @@ class CompareState extends State { double vs = double.parse(threeNumbers[2][0]!); theRedSide = [null, null, - TetraLeague( + Summaries(user, TetraLeague( timestamp: DateTime.now(), apm: apm, pps: pps, @@ -100,30 +102,30 @@ class CompareState extends State { standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1) - ]; + prevAt: -1), TetrioZen(level: 0, score: 0))]; return setState(() {}); } var player = await teto.fetchPlayer(user); + Summaries summary = await teto.fetchSummaries(player.userId); redSideMode = Mode.player; - late List states; - List>? dStates = >[]; - try{ - states = await teto.getPlayer(player.userId); - for (final TetrioPlayer state in states) { - dStates.add(DropdownMenuItem( - value: state, child: Text(dateFormat.format(state.state)))); - } - dStates.firstWhere((element) => element.value == player, orElse: () { - dStates?.add(DropdownMenuItem( - value: player, child: Text(t.mostRecentOne))); - return DropdownMenuItem( - value: player, child: Text(t.mostRecentOne)); - },); - }on Exception { - dStates = null; - } - theRedSide = [player, dStates, player.tlSeason1]; + //late List states; + // List>? dStates = >[]; + // try{ + // states = await teto.getPlayer(player.userId); + // for (final TetrioPlayer state in states) { + // dStates.add(DropdownMenuItem( + // value: state, child: Text(dateFormat.format(state.state)))); + // } + // dStates.firstWhere((element) => element.value == player, orElse: () { + // dStates?.add(DropdownMenuItem( + // value: player, child: Text(t.mostRecentOne))); + // return DropdownMenuItem( + // value: player, child: Text(t.mostRecentOne)); + // },); + // }on Exception { + // dStates = null; + // } + theRedSide = [player, null, summary]; } on Exception { if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); } @@ -132,7 +134,7 @@ class CompareState extends State { void changeRedSide(TetrioPlayer user) { setState(() {theRedSide[0] = user; - theRedSide[2] = user.tlSeason1;}); + theRedSide[2].league = user.tlSeason1;}); } void fetchGreenSide(String user) async { @@ -140,8 +142,9 @@ class CompareState extends State { if (user.startsWith("\$avg")){ try{ var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; + Summaries summary = Summaries("avg${user.substring(4).toLowerCase()}", average, TetrioZen(level: 0, score: 0)); greenSideMode = Mode.averages; - theGreenSide = [null, null, average]; + theGreenSide = [null, null, summary]; return setState(() {}); }on Exception { if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); @@ -157,7 +160,7 @@ class CompareState extends State { double vs = double.parse(threeNumbers[2][0]!); theGreenSide = [null, null, - TetraLeague( + Summaries(user, TetraLeague( timestamp: DateTime.now(), apm: apm, pps: pps, @@ -175,30 +178,30 @@ class CompareState extends State { standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1) - ]; + prevAt: -1), TetrioZen(level: 0, score: 0))]; return setState(() {}); } var player = await teto.fetchPlayer(user); + Summaries summary = await teto.fetchSummaries(player.userId); greenSideMode = Mode.player; - late List states; - List>? dStates = >[]; - try{ - states = await teto.getPlayer(player.userId); - for (final TetrioPlayer state in states) { - dStates.add(DropdownMenuItem( - value: state, child: Text(dateFormat.format(state.state)))); - } - dStates.firstWhere((element) => element.value == player, orElse: () { - dStates?.add(DropdownMenuItem( - value: player, child: Text(t.mostRecentOne))); - return DropdownMenuItem( - value: player, child: Text(t.mostRecentOne)); - },); - }on Exception { - dStates = null; - } - theGreenSide = [player, dStates, player.tlSeason1]; + // late List states; + // List>? dStates = >[]; + // try{ + // states = await teto.getPlayer(player.userId); + // for (final TetrioPlayer state in states) { + // dStates.add(DropdownMenuItem( + // value: state, child: Text(dateFormat.format(state.state)))); + // } + // dStates.firstWhere((element) => element.value == player, orElse: () { + // dStates?.add(DropdownMenuItem( + // value: player, child: Text(t.mostRecentOne))); + // return DropdownMenuItem( + // value: player, child: Text(t.mostRecentOne)); + // },); + // }on Exception { + // dStates = null; + // } + theGreenSide = [player, null, summary]; } on Exception { if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); } @@ -207,7 +210,7 @@ class CompareState extends State { void changeGreenSide(TetrioPlayer user) { setState(() {theGreenSide[0] = user; - theGreenSide[2] = user.tlSeason1;}); + theGreenSide[2].league = user.tlSeason1;}); } double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { @@ -237,10 +240,10 @@ class CompareState extends State { titleGreenSide = theGreenSide[0] != null ? theGreenSide[0].username.toUpperCase() : "???"; break; case Mode.stats: - titleGreenSide = "${theGreenSide[2].apm} APM, ${theGreenSide[2].pps} PPS, ${theGreenSide[2].vs} VS"; + titleGreenSide = "${theGreenSide[2].league.apm} APM, ${theGreenSide[2].league.pps} PPS, ${theGreenSide[2].league.vs} VS"; break; case Mode.averages: - titleGreenSide = t.averageXrank(rankLetter: theGreenSide[2].rank.toUpperCase()); + titleGreenSide = t.averageXrank(rankLetter: theGreenSide[2].league.rank.toUpperCase()); break; } switch (redSideMode){ @@ -248,10 +251,10 @@ class CompareState extends State { titleRedSide = theRedSide[0] != null ? theRedSide[0].username.toUpperCase() : "???"; break; case Mode.stats: - titleRedSide = "${theRedSide[2].apm} APM, ${theRedSide[2].pps} PPS, ${theRedSide[2].vs} VS"; + titleRedSide = "${theRedSide[2].league.apm} APM, ${theRedSide[2].league.pps} PPS, ${theRedSide[2].league.vs} VS"; break; case Mode.averages: - titleRedSide = t.averageXrank(rankLetter: theRedSide[2].rank.toUpperCase()); + titleRedSide = t.averageXrank(rankLetter: theRedSide[2].league.rank.toUpperCase()); break; } windowManager.setTitle("Tetra Stats: $titleGreenSide ${t.vs} $titleRedSide"); @@ -381,7 +384,7 @@ class CompareState extends State { label: t.normalBanned, trueIsBetter: false ), - (theGreenSide[2].gamesPlayed > 0 || greenSideMode == Mode.stats) && (theRedSide[2].gamesPlayed > 0 || redSideMode == Mode.stats) + (theGreenSide[2].league.gamesPlayed > 0 || greenSideMode == Mode.stats) && (theRedSide[2].league.gamesPlayed > 0 || redSideMode == Mode.stats) ? Column( children: [ Padding( @@ -391,14 +394,14 @@ class CompareState extends State { fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && + if (theGreenSide[2].league.gamesPlayed > 9 && + theRedSide[2].league.gamesPlayed > 9 && greenSideMode != Mode.stats && redSideMode != Mode.stats) CompareThingy( label: "TR", - greenSide: theGreenSide[2].tr, - redSide: theRedSide[2].tr, + greenSide: theGreenSide[2].league.tr, + redSide: theRedSide[2].league.tr, fractionDigits: 2, higherIsBetter: true, ), @@ -406,16 +409,16 @@ class CompareState extends State { redSideMode != Mode.stats) CompareThingy( label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].gamesPlayed, - redSide: theRedSide[2].gamesPlayed, + greenSide: theGreenSide[2].league.gamesPlayed, + redSide: theRedSide[2].league.gamesPlayed, higherIsBetter: true, ), if (greenSideMode != Mode.stats && redSideMode != Mode.stats) CompareThingy( label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].gamesWon, - redSide: theRedSide[2].gamesWon, + greenSide: theGreenSide[2].league.gamesWon, + redSide: theRedSide[2].league.gamesWon, higherIsBetter: true, ), if (greenSideMode != Mode.stats && @@ -423,93 +426,93 @@ class CompareState extends State { CompareThingy( label: "WR %", greenSide: - theGreenSide[2].winrate * 100, - redSide: theRedSide[2].winrate * 100, + theGreenSide[2].league.winrate * 100, + redSide: theRedSide[2].league.winrate * 100, fractionDigits: 2, higherIsBetter: true, ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && + if (theGreenSide[2].league.gamesPlayed > 9 && + theRedSide[2].league.gamesPlayed > 9 && greenSideMode != Mode.stats && redSideMode != Mode.stats) CompareThingy( label: "Glicko", - greenSide: theGreenSide[2].glicko!, - redSide: theRedSide[2].glicko!, + greenSide: theGreenSide[2].league.glicko!, + redSide: theRedSide[2].league.glicko!, fractionDigits: 2, higherIsBetter: true, ), - if (theGreenSide[2].gamesPlayed > 9 && - theRedSide[2].gamesPlayed > 9 && + if (theGreenSide[2].league.gamesPlayed > 9 && + theRedSide[2].league.gamesPlayed > 9 && greenSideMode != Mode.stats && redSideMode != Mode.stats) CompareThingy( label: "RD", - greenSide: theGreenSide[2].rd!, - redSide: theRedSide[2].rd!, + greenSide: theGreenSide[2].league.rd!, + redSide: theRedSide[2].league.rd!, fractionDigits: 3, higherIsBetter: false, ), - if (theGreenSide[2].standing > 0 && - theRedSide[2].standing > 0 && + if (theGreenSide[2].league.standing > 0 && + theRedSide[2].league.standing > 0 && greenSideMode == Mode.player && redSideMode == Mode.player) CompareThingy( label: t.statCellNum.lbpShort, - greenSide: theGreenSide[2].standing, - redSide: theRedSide[2].standing, + greenSide: theGreenSide[2].league.standing, + redSide: theRedSide[2].league.standing, higherIsBetter: false, ), - if (theGreenSide[2].standingLocal > 0 && - theRedSide[2].standingLocal > 0 && + if (theGreenSide[2].league.standingLocal > 0 && + theRedSide[2].league.standingLocal > 0 && greenSideMode == Mode.player && redSideMode == Mode.player) CompareThingy( label: t.statCellNum.lbpcShort, greenSide: - theGreenSide[2].standingLocal, - redSide: theRedSide[2].standingLocal, + theGreenSide[2].league.standingLocal, + redSide: theRedSide[2].league.standingLocal, higherIsBetter: false, ), - if (theGreenSide[2].apm != null && - theRedSide[2].apm != null) + if (theGreenSide[2].league.apm != null && + theRedSide[2].league.apm != null) CompareThingy( label: "APM", - greenSide: theGreenSide[2].apm!, - redSide: theRedSide[2].apm!, + greenSide: theGreenSide[2].league.apm!, + redSide: theRedSide[2].league.apm!, fractionDigits: 2, higherIsBetter: true, ), - if (theGreenSide[2].pps != null && - theRedSide[2].pps != null) + if (theGreenSide[2].league.pps != null && + theRedSide[2].league.pps != null) CompareThingy( label: "PPS", - greenSide: theGreenSide[2].pps!, - redSide: theRedSide[2].pps!, + greenSide: theGreenSide[2].league.pps!, + redSide: theRedSide[2].league.pps!, fractionDigits: 2, higherIsBetter: true, ), - if (theGreenSide[2].vs != null && - theRedSide[2].vs != null) + if (theGreenSide[2].league.vs != null && + theRedSide[2].league.vs != null) CompareThingy( label: "VS", - greenSide: theGreenSide[2].vs!, - redSide: theRedSide[2].vs!, + greenSide: theGreenSide[2].league.vs!, + redSide: theRedSide[2].league.vs!, fractionDigits: 2, higherIsBetter: true, ), ], ) : CompareBoolThingy( - greenSide: theGreenSide[2].gamesPlayed > 0, - redSide: theRedSide[2].gamesPlayed > 0, + greenSide: theGreenSide[2].league.gamesPlayed > 0, + redSide: theRedSide[2].league.gamesPlayed > 0, label: t.playedTL, trueIsBetter: false), - const Divider(), - if (theGreenSide[2].nerdStats != null && - theRedSide[2].nerdStats != null) + if (theGreenSide[2].league.nerdStats != null && + theRedSide[2].league.nerdStats != null) Column( children: [ + const Divider(), Padding( padding: const EdgeInsets.only(bottom: 16), child: Text(t.nerdStats, @@ -519,117 +522,117 @@ class CompareState extends State { ), CompareThingy( label: "APP", - greenSide: theGreenSide[2].nerdStats!.app, - redSide: theRedSide[2].nerdStats!.app, + greenSide: theGreenSide[2].league.nerdStats!.app, + redSide: theRedSide[2].league.nerdStats!.app, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "VS/APM", - greenSide: theGreenSide[2].nerdStats!.vsapm, - redSide: theRedSide[2].nerdStats!.vsapm, + greenSide: theGreenSide[2].league.nerdStats!.vsapm, + redSide: theRedSide[2].league.nerdStats!.vsapm, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "DS/S", - greenSide: theGreenSide[2].nerdStats!.dss, - redSide: theRedSide[2].nerdStats!.dss, + greenSide: theGreenSide[2].league.nerdStats!.dss, + redSide: theRedSide[2].league.nerdStats!.dss, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "DS/P", - greenSide: theGreenSide[2].nerdStats!.dsp, - redSide: theRedSide[2].nerdStats!.dsp, + greenSide: theGreenSide[2].league.nerdStats!.dsp, + redSide: theRedSide[2].league.nerdStats!.dsp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "APP + DS/P", greenSide: - theGreenSide[2].nerdStats!.appdsp, - redSide: theRedSide[2].nerdStats!.appdsp, + theGreenSide[2].league.nerdStats!.appdsp, + redSide: theRedSide[2].league.nerdStats!.appdsp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), greenSide: - theGreenSide[2].nerdStats!.cheese, - redSide: theRedSide[2].nerdStats!.cheese, + theGreenSide[2].league.nerdStats!.cheese, + redSide: theRedSide[2].league.nerdStats!.cheese, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( label: "Gb Eff.", - greenSide: theGreenSide[2].nerdStats!.gbe, - redSide: theRedSide[2].nerdStats!.gbe, + greenSide: theGreenSide[2].league.nerdStats!.gbe, + redSide: theRedSide[2].league.nerdStats!.gbe, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "wAPP", greenSide: - theGreenSide[2].nerdStats!.nyaapp, - redSide: theRedSide[2].nerdStats!.nyaapp, + theGreenSide[2].league.nerdStats!.nyaapp, + redSide: theRedSide[2].league.nerdStats!.nyaapp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Area", - greenSide: theGreenSide[2].nerdStats!.area, - redSide: theRedSide[2].nerdStats!.area, + greenSide: theGreenSide[2].league.nerdStats!.area, + redSide: theRedSide[2].league.nerdStats!.area, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( label: t.statCellNum.estOfTRShort, - greenSide: theGreenSide[2].estTr!.esttr, - redSide: theRedSide[2].estTr!.esttr, + greenSide: theGreenSide[2].league.estTr!.esttr, + redSide: theRedSide[2].league.estTr!.esttr, fractionDigits: 2, higherIsBetter: true, ), - if (theGreenSide[2].gamesPlayed > 9 && - theGreenSide[2].gamesPlayed > 9 && + if (theGreenSide[2].league.gamesPlayed > 9 && + theGreenSide[2].league.gamesPlayed > 9 && greenSideMode != Mode.stats && redSideMode != Mode.stats) CompareThingy( label: t.statCellNum.accOfEstShort, - greenSide: theGreenSide[2].esttracc!, - redSide: theRedSide[2].esttracc!, + greenSide: theGreenSide[2].league.esttracc!, + redSide: theRedSide[2].league.esttracc!, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( label: "Opener", - greenSide: theGreenSide[2].playstyle!.opener, - redSide: theRedSide[2].playstyle!.opener, + greenSide: theGreenSide[2].league.playstyle!.opener, + redSide: theRedSide[2].league.playstyle!.opener, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Plonk", - greenSide: theGreenSide[2].playstyle!.plonk, - redSide: theRedSide[2].playstyle!.plonk, + greenSide: theGreenSide[2].league.playstyle!.plonk, + redSide: theRedSide[2].league.playstyle!.plonk, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Stride", - greenSide: theGreenSide[2].playstyle!.stride, - redSide: theRedSide[2].playstyle!.stride, + greenSide: theGreenSide[2].league.playstyle!.stride, + redSide: theRedSide[2].league.playstyle!.stride, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( label: "Inf. DS", - greenSide: theGreenSide[2].playstyle!.infds, - redSide: theRedSide[2].playstyle!.infds, + greenSide: theGreenSide[2].league.playstyle!.infds, + redSide: theRedSide[2].league.playstyle!.infds, fractionDigits: 3, higherIsBetter: true, ), - VsGraphs(theGreenSide[2].apm!, theGreenSide[2].pps!, theGreenSide[2].vs!, theGreenSide[2].nerdStats!, theGreenSide[2].playstyle!, theRedSide[2].apm!, theRedSide[2].pps!, theRedSide[2].vs!, theRedSide[2].nerdStats!, theRedSide[2].playstyle!), + VsGraphs(theGreenSide[2].league.apm!, theGreenSide[2].league.pps!, theGreenSide[2].league.vs!, theGreenSide[2].league.nerdStats!, theGreenSide[2].league.playstyle!, theRedSide[2].league.apm!, theRedSide[2].league.pps!, theRedSide[2].league.vs!, theRedSide[2].league.nerdStats!, theRedSide[2].league.playstyle!), const Divider(), Padding( padding: const EdgeInsets.only(bottom: 16), @@ -639,20 +642,20 @@ class CompareState extends State { fontSize: bigScreen ? 42 : 28)), ), if (greenSideMode != Mode.stats && redSideMode != Mode.stats && - theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9) + theGreenSide[2].league.gamesPlayed > 9 && theRedSide[2].league.gamesPlayed > 9) CompareThingy( label: t.byGlicko, greenSide: getWinrateByTR( - theGreenSide[2].glicko!, - theGreenSide[2].rd!, - theRedSide[2].glicko!, - theRedSide[2].rd!) * + theGreenSide[2].league.glicko!, + theGreenSide[2].league.rd!, + theRedSide[2].league.glicko!, + theRedSide[2].league.rd!) * 100, redSide: getWinrateByTR( - theRedSide[2].glicko!, - theRedSide[2].rd!, - theGreenSide[2].glicko!, - theGreenSide[2].rd!) * + theRedSide[2].league.glicko!, + theRedSide[2].league.rd!, + theGreenSide[2].league.glicko!, + theGreenSide[2].league.rd!) * 100, fractionDigits: 2, higherIsBetter: true, @@ -661,22 +664,604 @@ class CompareState extends State { CompareThingy( label: t.byEstTR, greenSide: getWinrateByTR( - theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd ?? noTrRd, - theRedSide[2].estTr!.estglicko, - theRedSide[2].rd ?? noTrRd) * + theGreenSide[2].league.estTr!.estglicko, + theGreenSide[2].league.rd ?? noTrRd, + theRedSide[2].league.estTr!.estglicko, + theRedSide[2].league.rd ?? noTrRd) * 100, redSide: getWinrateByTR( - theRedSide[2].estTr!.estglicko, - theRedSide[2].rd ?? noTrRd, - theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd ?? noTrRd) * + theRedSide[2].league.estTr!.estglicko, + theRedSide[2].league.rd ?? noTrRd, + theGreenSide[2].league.estTr!.estglicko, + theGreenSide[2].league.rd ?? noTrRd) * 100, fractionDigits: 2, higherIsBetter: true, postfix: "%", ), ], + ), + if (theGreenSide[2].zenith != null && theRedSide[2].zenith != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.quickPlay, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "Height", + greenSide: theGreenSide[2].zenith.stats.zenith!.altitude, + redSide: theRedSide[2].zenith.stats.zenith!.altitude, + fractionDigits: 2, + higherIsBetter: true, + postfix: "m", + ), + CompareThingy( + label: "Position", + greenSide: theGreenSide[2].zenith.rank, + redSide: theRedSide[2].zenith.rank, + higherIsBetter: false, + prefix: "№ ", + ), + CompareThingy( + label: "Position (Country)", + greenSide: theGreenSide[2].zenith.countryRank, + redSide: theRedSide[2].zenith.countryRank, + higherIsBetter: false, + prefix: "№ ", + ), + CompareThingy( + label: "APM", + greenSide: theGreenSide[2].zenith.aggregateStats.apm, + redSide: theRedSide[2].zenith.aggregateStats.apm, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "PPS", + greenSide: theGreenSide[2].zenith.aggregateStats.pps, + redSide: theRedSide[2].zenith.aggregateStats.pps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "VS", + greenSide: theGreenSide[2].zenith.aggregateStats.vs, + redSide: theRedSide[2].zenith.aggregateStats.vs, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "KO's", + greenSide: theGreenSide[2].zenith.stats.kills, + redSide: theRedSide[2].zenith.stats.kills, + higherIsBetter: true, + ), + CompareThingy( + label: "CPS", + greenSide: theGreenSide[2].zenith.stats.cps, + redSide: theRedSide[2].zenith.stats.cps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Peak CPS", + greenSide: theGreenSide[2].zenith.stats.zenith!.peakrank, + redSide: theRedSide[2].zenith.stats.zenith!.peakrank, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareDurationThingy( + label: "Time", + greenSide: theGreenSide[2].zenith.stats.finalTime, + redSide: theRedSide[2].zenith.stats.finalTime, + higherIsBetter: false, + ), + CompareThingy( + label: "Finesse", + greenSide: theGreenSide[2].zenith.stats.finessePercentage * 100, + redSide: theRedSide[2].zenith.stats.finessePercentage * 100, + fractionDigits: 2, + postfix: "%", + higherIsBetter: true, + ), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("${t.quickPlay} ${t.nerdStats}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + ), + CompareThingy( + label: "APP", + greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.app, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.vsapm, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.dss, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.dsp, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: + theGreenSide[2].zenith.aggregateStats.nerdStats.appdsp, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), + greenSide: + theGreenSide[2].zenith.aggregateStats.nerdStats.cheese, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.cheese, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Gb Eff.", + greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.gbe, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "wAPP", + greenSide: + theGreenSide[2].zenith.aggregateStats.nerdStats.nyaapp, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.area, + redSide: theRedSide[2].zenith.aggregateStats.nerdStats.area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Opener", + greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.opener, + redSide: theRedSide[2].zenith.aggregateStats.playstyle.opener, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Plonk", + greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.plonk, + redSide: theRedSide[2].zenith.aggregateStats.playstyle.plonk, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Stride", + greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.stride, + redSide: theRedSide[2].zenith.aggregateStats.playstyle.stride, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Inf. DS", + greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.infds, + redSide: theRedSide[2].zenith.aggregateStats.playstyle.infds, + fractionDigits: 3, + higherIsBetter: true, + ), + VsGraphs(theGreenSide[2].zenith.aggregateStats.apm, theGreenSide[2].zenith.aggregateStats.pps, theGreenSide[2].zenith.aggregateStats.vs, theGreenSide[2].zenith.aggregateStats.nerdStats, theGreenSide[2].zenith.aggregateStats.playstyle, theRedSide[2].zenith.aggregateStats.apm, theRedSide[2].zenith.aggregateStats.pps, theRedSide[2].zenith.aggregateStats.vs, theRedSide[2].zenith.aggregateStats.nerdStats, theRedSide[2].zenith.aggregateStats.playstyle), + ], + ) + else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].zenith != null, redSide: theRedSide[2].zenith != null, label: "Played QP", trueIsBetter: true), + if (theGreenSide[2].zenithEx != null && theRedSide[2].zenithEx != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("${t.quickPlay} ${t.expert}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "Height", + greenSide: theGreenSide[2].zenithEx.stats.zenith!.altitude, + redSide: theRedSide[2].zenithEx.stats.zenith!.altitude, + fractionDigits: 2, + higherIsBetter: true, + postfix: "m", + ), + CompareThingy( + label: "Position", + greenSide: theGreenSide[2].zenithEx.rank, + redSide: theRedSide[2].zenithEx.rank, + higherIsBetter: false, + prefix: "№ ", + ), + CompareThingy( + label: "Position (Country)", + greenSide: theGreenSide[2].zenithEx.countryRank, + redSide: theRedSide[2].zenithEx.countryRank, + higherIsBetter: false, + prefix: "№ ", + ), + CompareThingy( + label: "APM", + greenSide: theGreenSide[2].zenithEx.aggregateStats.apm, + redSide: theRedSide[2].zenithEx.aggregateStats.apm, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "PPS", + greenSide: theGreenSide[2].zenithEx.aggregateStats.pps, + redSide: theRedSide[2].zenithEx.aggregateStats.pps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "VS", + greenSide: theGreenSide[2].zenithEx.aggregateStats.vs, + redSide: theRedSide[2].zenithEx.aggregateStats.vs, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "KO's", + greenSide: theGreenSide[2].zenithEx.stats.kills, + redSide: theRedSide[2].zenithEx.stats.kills, + higherIsBetter: true, + ), + CompareThingy( + label: "CPS", + greenSide: theGreenSide[2].zenithEx.stats.cps, + redSide: theRedSide[2].zenithEx.stats.cps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Peak CPS", + greenSide: theGreenSide[2].zenithEx.stats.zenith!.peakrank, + redSide: theRedSide[2].zenithEx.stats.zenith!.peakrank, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareDurationThingy( + label: "Time", + greenSide: theGreenSide[2].zenithEx.stats.finalTime, + redSide: theRedSide[2].zenithEx.stats.finalTime, + higherIsBetter: false, + ), + CompareThingy( + label: "Finesse", + greenSide: theGreenSide[2].zenithEx.stats.finessePercentage * 100, + redSide: theRedSide[2].zenithEx.stats.finessePercentage * 100, + fractionDigits: 2, + postfix: "%", + higherIsBetter: true, + ), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text("${t.quickPlay} ${t.expert} ${t.nerdStats}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + ), + CompareThingy( + label: "APP", + greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.app, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.app, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "VS/APM", + greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.vsapm, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.vsapm, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/S", + greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.dss, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.dss, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "DS/P", + greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.dsp, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.dsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "APP + DS/P", + greenSide: + theGreenSide[2].zenithEx.aggregateStats.nerdStats.appdsp, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.appdsp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), + greenSide: + theGreenSide[2].zenithEx.aggregateStats.nerdStats.cheese, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.cheese, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Gb Eff.", + greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.gbe, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.gbe, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "wAPP", + greenSide: + theGreenSide[2].zenithEx.aggregateStats.nerdStats.nyaapp, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.nyaapp, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Area", + greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.area, + redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.area, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Opener", + greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.opener, + redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.opener, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Plonk", + greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.plonk, + redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.plonk, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Stride", + greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.stride, + redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.stride, + fractionDigits: 3, + higherIsBetter: true, + ), + CompareThingy( + label: "Inf. DS", + greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.infds, + redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.infds, + fractionDigits: 3, + higherIsBetter: true, + ), + VsGraphs(theGreenSide[2].zenithEx.aggregateStats.apm, theGreenSide[2].zenithEx.aggregateStats.pps, theGreenSide[2].zenithEx.aggregateStats.vs, theGreenSide[2].zenithEx.aggregateStats.nerdStats, theGreenSide[2].zenithEx.aggregateStats.playstyle, theRedSide[2].zenithEx.aggregateStats.apm, theRedSide[2].zenithEx.aggregateStats.pps, theRedSide[2].zenithEx.aggregateStats.vs, theRedSide[2].zenithEx.aggregateStats.nerdStats, theRedSide[2].zenithEx.aggregateStats.playstyle), + ], + ) + else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].zenithEx != null, redSide: theRedSide[2].zenithEx != null, label: "Played QP Expert", trueIsBetter: true), + if (theGreenSide[2].sprint != null && theRedSide[2].sprint != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.sprint, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareDurationThingy( + label: "Time", + greenSide: theGreenSide[2].sprint.stats.finalTime, + redSide: theRedSide[2].sprint.stats.finalTime, + higherIsBetter: false, + ), + CompareThingy( + label: "Lines", + greenSide: theGreenSide[2].sprint.stats.lines, + redSide: theRedSide[2].sprint.stats.lines, + higherIsBetter: false, + ), + CompareThingy( + label: t.statCellNum.pieces.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].sprint.stats.piecesPlaced, + redSide: theRedSide[2].sprint.stats.piecesPlaced, + higherIsBetter: false, + ), + CompareThingy( + label: t.statCellNum.keys.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].sprint.stats.inputs, + redSide: theRedSide[2].sprint.stats.inputs, + higherIsBetter: false, + ), + CompareThingy( + label: "PPS", + greenSide: theGreenSide[2].sprint.stats.pps, + redSide: theRedSide[2].sprint.stats.pps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "KPP", + greenSide: theGreenSide[2].sprint.stats.kpp, + redSide: theRedSide[2].sprint.stats.kpp, + fractionDigits: 2, + higherIsBetter: false, + ), + CompareThingy( + label: "KPS", + greenSide: theGreenSide[2].sprint.stats.kps, + redSide: theRedSide[2].sprint.stats.kps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Finesse", + greenSide: theGreenSide[2].sprint.stats.finessePercentage * 100, + redSide: theRedSide[2].sprint.stats.finessePercentage * 100, + fractionDigits: 2, + postfix: "%", + higherIsBetter: true, + ), + CompareThingy( + label: "Holds", + greenSide: theGreenSide[2].sprint.stats.holds, + redSide: theRedSide[2].sprint.stats.holds, + higherIsBetter: false, + ), + CompareThingy( + label: "T-spins", + greenSide: theGreenSide[2].sprint.stats.tSpins, + redSide: theRedSide[2].sprint.stats.tSpins, + higherIsBetter: false, + ), + CompareThingy( + label: "Quads", + greenSide: theGreenSide[2].sprint.stats.clears.quads, + redSide: theRedSide[2].sprint.stats.clears.quads, + higherIsBetter: true, + ), + CompareThingy( + label: "PC's", + greenSide: theGreenSide[2].sprint.stats.clears.allClears, + redSide: theRedSide[2].sprint.stats.clears.allClears, + higherIsBetter: true, + ), + ], + ) + else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].sprint != null, redSide: theRedSide[2].sprint != null, label: "Played 40 Lines", trueIsBetter: true), + if (theGreenSide[2].blitz != null && theRedSide[2].blitz != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.blitz, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "Score", + greenSide: theGreenSide[2].blitz.stats.score, + redSide: theRedSide[2].blitz.stats.score, + higherIsBetter: true, + ), + CompareThingy( + label: "SPP", + greenSide: theGreenSide[2].blitz.stats.spp, + redSide: theRedSide[2].blitz.stats.spp, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Level", + greenSide: theGreenSide[2].blitz.stats.level, + redSide: theRedSide[2].blitz.stats.level, + higherIsBetter: true, + ), + CompareThingy( + label: "Lines", + greenSide: theGreenSide[2].blitz.stats.lines, + redSide: theRedSide[2].blitz.stats.lines, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.pieces.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].blitz.stats.piecesPlaced, + redSide: theRedSide[2].blitz.stats.piecesPlaced, + higherIsBetter: true, + ), + CompareThingy( + label: t.statCellNum.keys.replaceAll(RegExp(r'\n'), " "), + greenSide: theGreenSide[2].blitz.stats.inputs, + redSide: theRedSide[2].blitz.stats.inputs, + higherIsBetter: true, + ), + CompareThingy( + label: "PPS", + greenSide: theGreenSide[2].blitz.stats.pps, + redSide: theRedSide[2].blitz.stats.pps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "KPP", + greenSide: theGreenSide[2].blitz.stats.kpp, + redSide: theRedSide[2].blitz.stats.kpp, + fractionDigits: 2, + higherIsBetter: false, + ), + CompareThingy( + label: "KPS", + greenSide: theGreenSide[2].blitz.stats.kps, + redSide: theRedSide[2].blitz.stats.kps, + fractionDigits: 2, + higherIsBetter: true, + ), + CompareThingy( + label: "Finesse", + greenSide: theGreenSide[2].blitz.stats.finessePercentage * 100, + redSide: theRedSide[2].blitz.stats.finessePercentage * 100, + fractionDigits: 2, + postfix: "%", + higherIsBetter: true, + ), + CompareThingy( + label: "Holds", + greenSide: theGreenSide[2].blitz.stats.holds, + redSide: theRedSide[2].blitz.stats.holds, + higherIsBetter: false, + ), + CompareThingy( + label: "T-spins", + greenSide: theGreenSide[2].blitz.stats.tSpins, + redSide: theRedSide[2].blitz.stats.tSpins, + higherIsBetter: false, + ), + CompareThingy( + label: "Quads", + greenSide: theGreenSide[2].blitz.stats.clears.quads, + redSide: theRedSide[2].blitz.stats.clears.quads, + higherIsBetter: true, + ), + CompareThingy( + label: "PC's", + greenSide: theGreenSide[2].blitz.stats.clears.allClears, + redSide: theRedSide[2].blitz.stats.clears.allClears, + higherIsBetter: true, + ), + ], + ) + else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].blitz != null, redSide: theRedSide[2].blitz != null, label: "Played Blitz", trueIsBetter: true), + if (greenSideMode == Mode.player && redSideMode == Mode.player) Column( + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ), + CompareThingy( + label: "Level", + greenSide: theGreenSide[2].zen.level, + redSide: theRedSide[2].zen.level, + higherIsBetter: true, + ), + CompareThingy( + label: "Score", + greenSide: theGreenSide[2].zen.score, + redSide: theRedSide[2].zen.score, + higherIsBetter: true, + ), + ], ) ], ) @@ -684,7 +1269,7 @@ class CompareState extends State { padding: const EdgeInsets.all(8.0), child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), ) - ], + ], ), ), ), @@ -717,10 +1302,10 @@ class PlayerSelector extends StatelessWidget { playerController.text = data[0] != null ? data[0].username : ""; break; case Mode.stats: - playerController.text = "${data[2].apm} ${data[2].pps} ${data[2].vs}"; + playerController.text = "${data[2].league.apm} ${data[2].league.pps} ${data[2].league.vs}"; break; case Mode.averages: - playerController.text = "\$avg${data[2].rank.toUpperCase()}"; + playerController.text = "\$avg${data[2].league.rank.toUpperCase()}"; break; } } @@ -730,10 +1315,10 @@ class PlayerSelector extends StatelessWidget { underFieldString = data[0] != null ? data[0].toString() : "???"; break; case Mode.stats: - underFieldString = "${data[2].apm} APM, ${data[2].pps} PPS, ${data[2].vs} VS"; + underFieldString = "${data[2].league.apm} APM, ${data[2].league.pps} PPS, ${data[2].league.vs} VS"; break; case Mode.averages: - underFieldString = t.averageXrank(rankLetter: data[2].rank.toUpperCase()); + underFieldString = t.averageXrank(rankLetter: data[2].league.rank.toUpperCase()); break; } } @@ -1055,14 +1640,42 @@ class CompareDurationThingy extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: Text( - greenSide.toString(), - style: const TextStyle( - fontSize: 22, + Expanded(child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + transform: const GradientRotation(0.6), + stops: [ + 0.0, + higherIsBetter + ? greenSide > redSide + ? 0.6 + : 0 + : greenSide < redSide + ? 0.6 + : 0 + ], + ) ), - textAlign: TextAlign.start, - )), + child: Text(get40lTime(greenSide.inMicroseconds), style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), textAlign: TextAlign.start) + )), Column( children: [ Text( @@ -1088,12 +1701,41 @@ class CompareDurationThingy extends StatelessWidget { verdict(greenSide, redSide).toString(), style: verdictStyle, textAlign: TextAlign.center) ], ), - Expanded( - child: Text( - redSide.toString(), - style: const TextStyle(fontSize: 22), - textAlign: TextAlign.end, - )), + Expanded(child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.centerRight, + end: Alignment.centerLeft, + transform: const GradientRotation(-0.6), + stops: [ + 0.0, + higherIsBetter + ? redSide > greenSide + ? 0.6 + : 0 + : redSide < greenSide + ? 0.6 + : 0 + ], + )), + child: Text(get40lTime(redSide.inMicroseconds), style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), textAlign: TextAlign.end) + )), ], ), ); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index df6fe00..324bed9 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1103,10 +1103,10 @@ class _TwoRecordsThingy extends StatelessWidget { Widget build(BuildContext context) { late MapEntry closestAverageBlitz; late bool blitzBetterThanClosestAverage; - bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null; + bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null; late MapEntry closestAverageSprint; late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.stats.finalTime < sprintAverages[rank]! : null; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && 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!.stats.finalTime).abs() < (b -sprint!.stats.finalTime).abs() ? a : b)); sprintBetterThanClosestAverage = sprint!.stats.finalTime < closestAverageSprint.value; @@ -1144,7 +1144,7 @@ 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: readableTimeDifference(sprint!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + if (rank != null && rank != "z" && rank != "x+") 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!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( @@ -1227,7 +1227,7 @@ 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!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + if (rank != null && rank != "z" && rank != "x+") 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!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index 890f59e..f717ca2 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -28,10 +28,10 @@ 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!.stats.score > blitzAverages[rank]! : null; + bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+") ? record!.stats.score > blitzAverages[rank]! : null; late MapEntry closestAverageSprint; late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.finalTime < sprintAverages[rank]! : null; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+") ? 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; @@ -74,16 +74,16 @@ class SingleplayerRecord extends StatelessWidget { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - 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( + if (record!.gamemode == "40l" && (rank != null && rank != "z" && rank != "x+")) 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!.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( + else if (record!.gamemode == "40l" && (rank == null || rank == "z" || rank != "x+")) 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!.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( + else if (record!.gamemode == "blitz" && (rank != null && rank != "z" && rank != "x+")) 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!.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( + else if (record!.gamemode == "blitz" && (rank == null || rank == "z" || rank != "x+")) 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 != -1) TextSpan(text: "№${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank))), diff --git a/pubspec.yaml b/pubspec.yaml index bf491d7..f4cc18a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.6+32 +version: 1.6.7+33 environment: sdk: '>=3.0.0' From 7cb1fc05439df522ac5e4b937f6985df997636f3 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 24 Aug 2024 17:41:07 +0300 Subject: [PATCH 26/33] ok :droidsmile: --- lib/data_objects/tetrio.dart | 83 +++++++-- lib/services/tetrio_crud.dart | 54 +++++- lib/views/main_view.dart | 12 +- lib/views/main_view_tiles.dart | 270 +++++++++++++++++------------ lib/views/ranks_averages_view.dart | 80 ++++++--- pubspec.yaml | 2 +- 6 files changed, 336 insertions(+), 165 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 277b5fa..1906636 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:vector_math/vector_math.dart'; @@ -42,25 +43,26 @@ const Map rankCutoffs = { "z": -1, "": 0.5 }; -// const Map rankTargets = { -// "x": 24503.75, // where that comes from? -// "u": 23038, -// "ss": 21583, -// "s+": 20128, -// "s": 18673, -// "s-": 16975, -// "a+": 15035, -// "a": 13095, -// "a-": 11155, -// "b+": 9215, -// "b": 7275, -// "b-": 5335, -// "c+": 3880, -// "c": 2425, -// "c-": 1213, -// "d+": 606, -// "d": 0, -// }; +const Map rankTargets = { + "x+": 24000.00, + "x": 22500.00, + "u": 20000.00, + "ss": 18000.00, + "s+": 16500.00, + "s": 15200.00, + "s-": 13800.00, + "a+": 12000.00, + "a": 10500.00, + "a-": 9000.00, + "b+": 7400.00, + "b": 5700.00, + "b-": 4200.00, + "c+": 3000.00, + "c": 2000.00, + "c-": 1300.00, + "d+": 800.00, + "d": 0.00, +}; // DateTime seasonStart = DateTime.utc(2024, 08, 16, 18); //DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15); enum Stats { @@ -424,7 +426,9 @@ class Summaries{ RecordSingle? sprint; RecordSingle? blitz; RecordSingle? zenith; + RecordSingle? zenithCareerBest; // leaderboard best, not overall RecordSingle? zenithEx; + RecordSingle? zenithExCareerBest; // leaderboard best, not overall late List achievements; late TetraLeague league; late TetrioZen zen; @@ -436,7 +440,9 @@ class Summaries{ if (json['40l']['record'] != null) sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank'], json['40l']['rank_local']); if (json['blitz']['record'] != null) blitz = RecordSingle.fromJson(json['blitz']['record'], json['blitz']['rank'], json['40l']['rank_local']); if (json['zenith']['record'] != null) zenith = RecordSingle.fromJson(json['zenith']['record'], json['zenith']['rank'], json['zenith']['rank_local']); + if (json['zenith']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenith']['best']['record'], json['zenith']['best']['rank'], -1); if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); + if (json['zenithex']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenithex']['best']['record'], json['zenith']['best']['rank'], -1); achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; league = TetraLeague.fromJson(json['league'], DateTime.now()); zen = TetrioZen.fromJson(json['zen']); @@ -2612,3 +2618,42 @@ class TetrioPlayerFromLeaderboard { } } } + +class CutoffTetrio { + late int pos; + late double percentile; + late double tr; + late double targetTr; + late double apm; + late double pps; + late double vs; + late int count; + late double countPercentile; + + CutoffTetrio.fromJson(Map json, int total){ + pos = json['pos']; + percentile = json['percentile'].toDouble(); + tr = json['tr'].toDouble(); + targetTr = json['targettr'].toDouble(); + apm = json['apm'].toDouble(); + pps = json['pps'].toDouble(); + vs = json['vs'].toDouble(); + count = json['count']; + countPercentile = count / total; + } +} + +class CutoffsTetrio { + late DateTime timestamp; + late int total; + Map data = {}; + + CutoffsTetrio.fromJson(Map json){ + timestamp = DateTime.parse(json['t']); + total = json['data']['total']; + json['data'].remove("total"); + for (String rank in json['data'].keys){ + data[rank] = CutoffTetrio.fromJson(json['data'][rank], total); + } + } +} \ No newline at end of file diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 4e8da1c..1559fa4 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -407,7 +407,53 @@ class TetrioService extends DB { // Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above, // so i'm going to document only unique differences between them - Future fetchCutoffs() async { + Future fetchCutoffsTetrio() async { + CutoffsTetrio? cached = _cache.get("", CutoffsTetrio); + if (cached != null) return cached; + + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "cutoffs"}); + } else { + url = Uri.https('ch.tetr.io', 'api/labs/league_ranks'); + } + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + Map rawData = jsonDecode(response.body); + CutoffsTetrio result = CutoffsTetrio.fromJson(rawData['data']); + _cache.store(result, rawData["cache"]["cached_until"]); + return result; + case 404: + developer.log("fetchCutoffsTetrio: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); + return null; + // if not 200 or 404 - throw a unique for each code exception + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + developer.log("fetchCutoffsTetrio: Cutoffs are unavalable (${response.statusCode})", name: "services/tetrio_crud", error: response.statusCode); + return null; + default: + developer.log("fetchCutoffsTetrio: Failed to fetch top Cutoffs", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { // If local http client fails + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet + } + } + + Future fetchCutoffsBeanserver() async { Cutoffs? cached = _cache.get("", Cutoffs); if (cached != null) return cached; @@ -429,7 +475,7 @@ class TetrioService extends DB { _cache.store(result, rawData["cache_until"]); return result; case 404: - developer.log("fetchCutoffs: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); + developer.log("fetchCutoffsBeanserver: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); return null; // if not 200 or 404 - throw a unique for each code exception case 403: @@ -442,10 +488,10 @@ class TetrioService extends DB { case 502: case 503: case 504: - developer.log("fetchCutoffs: Cutoffs are unavalable (${response.statusCode})", name: "services/tetrio_crud", error: response.statusCode); + developer.log("fetchCutoffsBeanserver: Cutoffs are unavalable (${response.statusCode})", name: "services/tetrio_crud", error: response.statusCode); return null; default: - developer.log("fetchCutoffs: Failed to fetch top Cutoffs", name: "services/tetrio_crud", error: response.statusCode); + developer.log("fetchCutoffsBeanserver: Failed to fetch top Cutoffs", name: "services/tetrio_crud", error: response.statusCode); throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } } on http.ClientException catch (e, s) { // If local http client fails diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 324bed9..9b06306 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -175,10 +175,10 @@ class _MainState extends State with TickerProviderStateMixin { teto.fetchNews(_searchFor), teto.fetchStream(_searchFor, "zenith/recent"), teto.fetchStream(_searchFor, "zenithex/recent"), - teto.fetchCutoffs(), + teto.fetchCutoffsBeanserver(), (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), ]); - //prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>>[]), + //prefs.getBool("showPositions") != true ? teto.fetchCutoffsBeanserver() : Future.delayed(Duration.zero, ()=>>[]), //(summaries.league.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR summaries = requests[0] as Summaries; @@ -478,10 +478,10 @@ class _MainState extends State with TickerProviderStateMixin { guest: snapshot.data![0].role == "anon", thatRankCutoff: thatRankCutoff, thatRankCutoffGlicko: thatRankGlickoCutoff, - //thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, + thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, nextRankCutoff: nextRankCutoff, nextRankCutoffGlicko: nextRankGlickoCutoff, - //nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, + nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, averages: rankAverages, lbPositions: meAmongEveryone ), @@ -519,10 +519,10 @@ class _MainState extends State with TickerProviderStateMixin { guest: snapshot.data![0].role == "anon", thatRankCutoff: thatRankCutoff, thatRankCutoffGlicko: thatRankGlickoCutoff, - //thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, + thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, nextRankCutoff: nextRankCutoff, nextRankCutoffGlicko: nextRankGlickoCutoff, - //nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, + nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, averages: rankAverages, lbPositions: meAmongEveryone ), diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index da979c4..6c13992 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -40,7 +40,7 @@ class MainView extends StatefulWidget { enum Page {home, leaderboards, leagueAverages, calculator, settings} enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} -enum CardMod {info, recent, top, ex, exRecent, exTop} +enum CardMod {info, records, ex, exRecords} Map cardsTitles = { Cards.overview: "Overview", Cards.tetraLeague: t.tetraLeague, @@ -574,7 +574,7 @@ class RecordSummary 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: switch(record!.gamemode){ + if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), _ => record!.stats.score.toString() @@ -872,7 +872,7 @@ class _DestinationHomeState extends State { ); } - Widget getListOfRecords(String stream, bool isTop, BoxConstraints constraints){ + Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ return Column( children: [ Card( @@ -883,7 +883,8 @@ class _DestinationHomeState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(isTop ? t.top : t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("Records", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) ], ), ), @@ -891,65 +892,137 @@ class _DestinationHomeState extends State { ), Card( clipBehavior: Clip.antiAlias, - child: FutureBuilder( - future: teto.fetchStream(widget.searchFor, stream), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return Column( + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TabBar( + tabs: [ + Tab(text: "Recent"), + Tab(text: "Top"), + ], + ), + SizedBox( + height: 400, + child: TabBarView( children: [ - for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), - leading: Text( - isTop ? "#${i+1}" : switch (snapshot.data!.records[i].gamemode){ - "40l" => "40L", - "blitz" => "BLZ", - "5mblast" => "5MB", - "zenith" => "QP", - "zenithex" => "QPE", - String() => "huh", + FutureBuilder( + future: teto.fetchStream(widget.searchFor, recentStream), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => "40L", + "blitz" => "BLZ", + "5mblast" => "5MB", + "zenith" => "QP", + "zenithex" => "QPE", + String() => "huh", + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ); + } + if (snapshot.hasError){ + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + return Text("what?"); }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), - title: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), - "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - String() => "huh", + FutureBuilder( + future: teto.fetchStream(widget.searchFor, topStream), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + "#${i+1}", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ); + } + if (snapshot.hasError){ + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), + ), + ], + ) + ); + } + } + return Text("what?"); }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) - ) - ], - ); - } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } - } - return Text("what?"); - }, - ), + ), + ] + ), + ) + ], + ), + ) ), ], ); @@ -1296,14 +1369,14 @@ class _DestinationHomeState extends State { ), ], Cards.tetraLeague: [ - const ButtonSegment( - value: CardMod.info, - label: Text('Standing'), - ), - const ButtonSegment( - value: CardMod.recent, - label: Text('Recent Matches'), - ), + const ButtonSegment( + value: CardMod.info, + label: Text('Standing'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Recent Matches'), + ), ], Cards.quickPlay: [ const ButtonSegment( @@ -1311,25 +1384,17 @@ class _DestinationHomeState extends State { label: Text('Normal'), ), const ButtonSegment( - value: CardMod.recent, - label: Text('Recent Normal'), - ), - const ButtonSegment( - value: CardMod.top, - label: Text('Top Normal'), + value: CardMod.records, + label: Text('Records'), ), const ButtonSegment( value: CardMod.ex, label: Text('Expert'), ), const ButtonSegment( - value: CardMod.exRecent, - label: Text('Recent Expert'), - ), - const ButtonSegment( - value: CardMod.exTop, - label: Text('Top Expert'), - ), + value: CardMod.exRecords, + label: Text('Expert Records'), + ) ], Cards.blitz: [ const ButtonSegment( @@ -1337,13 +1402,9 @@ class _DestinationHomeState extends State { label: Text('PB'), ), const ButtonSegment( - value: CardMod.recent, - label: Text('Recent'), - ), - const ButtonSegment( - value: CardMod.top, - label: Text('Top'), - ), + value: CardMod.records, + label: Text('Records'), + ) ], Cards.sprint: [ const ButtonSegment( @@ -1351,13 +1412,9 @@ class _DestinationHomeState extends State { label: Text('PB'), ), const ButtonSegment( - value: CardMod.recent, - label: Text('Recent'), - ), - const ButtonSegment( - value: CardMod.top, - label: Text('Top'), - ), + value: CardMod.records, + label: Text('Records'), + ) ] }; super.initState(); @@ -1389,8 +1446,8 @@ class _DestinationHomeState extends State { ); } if (snapshot.hasData){ - blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null) ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; - sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null) ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; + blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; + sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; if (snapshot.data!.summaries!.sprint != null) { closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; @@ -1467,28 +1524,24 @@ class _DestinationHomeState extends State { Cards.overview => getOverviewCard(snapshot.data!.summaries!), Cards.tetraLeague => switch (cardMod){ CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league), - CardMod.recent => getRecentTLrecords(widget.constraints), + CardMod.records => getRecentTLrecords(widget.constraints), _ => Center(child: Text("huh?")) }, Cards.quickPlay => switch (cardMod){ CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), - CardMod.recent => getListOfRecords("zenith/recent", false, widget.constraints), - CardMod.top => getListOfRecords("zenith/top", true, widget.constraints), + CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), - CardMod.exRecent => getListOfRecords("zenithex/recent", false, widget.constraints), - CardMod.exTop => getListOfRecords("zenithex/top", true, widget.constraints), + CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), _ => Center(child: Text("huh?")) }, Cards.sprint => switch (cardMod){ CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.recent => getListOfRecords("40l/recent", false, widget.constraints), - CardMod.top => getListOfRecords("40l/top", true, widget.constraints), + CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), _ => Center(child: Text("huh?")) }, Cards.blitz => switch (cardMod){ CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.recent => getListOfRecords("blitz/recent", false, widget.constraints), - CardMod.top => getListOfRecords("blitz/top", true, widget.constraints), + CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), _ => Center(child: Text("huh?")) }, }, @@ -2333,12 +2386,13 @@ class TetraLeagueThingy extends StatelessWidget{ backgroundColor: Colors.black, axes: [ RadialAxis( - minimum: 0.4, - maximum: 0.6, + minimum: 0.0, + maximum: 1.0, radiusFactor: 1.01, showTicks: true, showLabels: false, - interval: 0.1, + interval: 0.25, + minorTicksPerInterval: 0, ranges:[ GaugeRange(startValue: 0, endValue: league.winrate, color: theme.colorScheme.primary) ], diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index 1749c6a..c2f6387 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -1,13 +1,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/compare_view.dart'; -import 'package:tetra_stats/views/rank_averages_view.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:window_manager/window_manager.dart'; import 'package:tetra_stats/main.dart' show teto; @@ -40,14 +37,13 @@ class RanksAverages extends State { @override Widget build(BuildContext context) { - bool bigScreen = MediaQuery.of(context).size.width >= 700; return Scaffold( appBar: AppBar( title: Text(t.rankAveragesViewTitle), ), backgroundColor: Colors.black, body: SafeArea( - child: FutureBuilder(future: teto.fetchCutoffs(), builder: (context, snapshot){ + child: FutureBuilder(future: teto.fetchCutoffsTetrio(), builder: (context, snapshot){ switch (snapshot.connectionState) { case ConnectionState.none: case ConnectionState.waiting: @@ -61,8 +57,7 @@ class RanksAverages extends State { scrollDirection: Axis.horizontal, child: Container( alignment: Alignment.center, - width: MediaQuery.of(context).size.width, - constraints: const BoxConstraints(maxWidth: 900, minWidth: 610), + width: 900, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( @@ -71,54 +66,85 @@ class RanksAverages extends State { Table( defaultVerticalAlignment: TableCellVerticalAlignment.middle, border: TableBorder.all(color: Colors.grey.shade900), - columnWidths: const {0: FixedColumnWidth(48)}, + columnWidths: const { + 0: FixedColumnWidth(48), + 1: FixedColumnWidth(155), + 2: FixedColumnWidth(150), + 3: FixedColumnWidth(90), + 4: FixedColumnWidth(130), + }, children: [ TableRow( children: [ Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + ), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text("Glicko", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("Glixare", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("S1 TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), ), ] ), - for (String rank in snapshot.data!.tr.keys) TableRow( - decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(100), rankColors[rank]!.withAlpha(200)])), + for (String rank in snapshot.data!.data.keys) TableRow( + decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])), children: [ Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.tr[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.glicko[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + child: Text(f2.format(snapshot.data!.data[rank]!.apm), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f3.format(snapshot.data!.gxe[rank]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + child: Text(f2.format(snapshot.data!.data[rank]!.pps), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.gxe[rank]!*250), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + child: Text(f2.format(snapshot.data!.data[rank]!.vs), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("${f3.format(snapshot.data!.data[rank]!.apm / (snapshot.data!.data[rank]!.pps * 60))} APP\n${f3.format(snapshot.data!.data[rank]!.vs / snapshot.data!.data[rank]!.apm)} VS/APM", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), + children: [ + TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)), + TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)), + TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null)) + ] + )) ), ] ) ], ), - Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.ts))) + Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))) ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index f4cc18a..4ac97d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.7+33 +version: 1.6.8+34 environment: sdk: '>=3.0.0' From ce2fb89ccf06361d26faf4b071439b0046d74fd8 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 24 Aug 2024 17:48:09 +0300 Subject: [PATCH 27/33] cache fix --- lib/data_objects/tetrio.dart | 2 ++ lib/services/tetrio_crud.dart | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 1906636..084858e 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -2644,11 +2644,13 @@ class CutoffTetrio { } class CutoffsTetrio { + late String id; late DateTime timestamp; late int total; Map data = {}; CutoffsTetrio.fromJson(Map json){ + id = json['s']; timestamp = DateTime.parse(json['t']); total = json['data']['total']; json['data'].remove("total"); diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 1559fa4..d39419c 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -408,7 +408,7 @@ class TetrioService extends DB { // so i'm going to document only unique differences between them Future fetchCutoffsTetrio() async { - CutoffsTetrio? cached = _cache.get("", CutoffsTetrio); + CutoffsTetrio? cached = _cache.get("league_ranks", CutoffsTetrio); if (cached != null) return cached; Uri url; From d710674973fca9067b70bc812d01dd3372df6996 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sun, 1 Sep 2024 02:00:26 +0300 Subject: [PATCH 28/33] Parsing user registration date from user id + redesign progress --- lib/data_objects/tetrio.dart | 2 +- lib/views/main_view_tiles.dart | 355 +++++++++++++++++++++------------ lib/widgets/user_thingy.dart | 2 +- 3 files changed, 234 insertions(+), 125 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 084858e..70e8a0f 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -268,7 +268,7 @@ class TetrioPlayer { username = nick; state = stateTime; role = json['role']; - registrationTime = json['ts'] != null ? DateTime.parse(json['ts']) : null; + registrationTime = json['ts'] != null ? DateTime.parse(json['ts']) : DateTime.fromMillisecondsSinceEpoch(int.parse(id.substring(0, 8), radix: 16) * 1000); if (json['badges'] != null) { json['badges'].forEach((v) { badges.add(Badge.fromJson(v)); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 6c13992..83d57ab 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -24,9 +24,10 @@ 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_progress_bar.dart'; -import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; +var fDiff = NumberFormat("+#,###.####;-#,###.####"); + class MainView extends StatefulWidget { final String? player; /// The very first view, that user see when he launch this programm. @@ -78,6 +79,17 @@ class _MainState extends State with TickerProviderStateMixin { super.dispose(); } + NavigationRailDestination getDestinationButton(IconData icon, String title){ + return NavigationRailDestination( + icon: Tooltip( + message: title, + child: Icon(icon) + ), + selectedIcon: Icon(icon), + label: Text(title), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -101,42 +113,14 @@ class _MainState extends State with TickerProviderStateMixin { }, icon: const Icon(Icons.more_horiz_rounded), ), - destinations: const [ - NavigationRailDestination( - icon: Icon(Icons.home), - selectedIcon: Icon(Icons.home), - label: Text('Home'), - ), - NavigationRailDestination( - icon: Icon(Icons.data_thresholding_outlined), - selectedIcon: Icon(Icons.data_thresholding_outlined), - label: Text('Graphs'), - ), - NavigationRailDestination( - icon: Icon(Icons.leaderboard), - selectedIcon: Icon(Icons.leaderboard), - label: Text('Leaderboards'), - ), - NavigationRailDestination( - icon: Icon(Icons.compress), - selectedIcon: Icon(Icons.compress), - label: Text('Cutoffs'), - ), - NavigationRailDestination( - icon: Icon(Icons.calculate), - selectedIcon: Icon(Icons.calculate), - label: Text('Calc'), - ), - NavigationRailDestination( - icon: Icon(Icons.storage), - selectedIcon: Icon(Icons.storage), - label: Text('Saved Data'), - ), - NavigationRailDestination( - icon: Icon(Icons.settings), - selectedIcon: Icon(Icons.settings), - label: Text('Settings'), - ) + destinations: [ + getDestinationButton(Icons.home, "Home"), + getDestinationButton(Icons.data_thresholding_outlined, "Graphs"), + getDestinationButton(Icons.leaderboard, "Leaderboards"), + getDestinationButton(Icons.compress, "Cutoffs"), + getDestinationButton(Icons.calculate, "Calc"), + getDestinationButton(Icons.storage, "Saved Data"), + getDestinationButton(Icons.settings, "Settings"), ], selectedIndex: destination, onDestinationSelected: (value) { @@ -597,9 +581,9 @@ class RecordSummary extends StatelessWidget{ ), ), ], - ) else if (hideRank) RichText(text: TextSpan( + ) else if (hideRank) RichText(text: const TextSpan( text: "---", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), ), ) ], @@ -638,7 +622,7 @@ class _DestinationHomeState extends State { Widget getOverviewCard(Summaries summaries){ return Column( children: [ - Card( + const Card( child: Padding( padding: EdgeInsets.only(bottom: 4.0), child: Center( @@ -653,13 +637,19 @@ class _DestinationHomeState extends State { ), ), Card( - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - TLRatingThingy(userID: "", tlData: summaries.league) - ], + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + TLRatingThingy(userID: "", tlData: summaries.league), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Text("${summaries.league.apm != null ? f2.format(summaries.league.apm) : "-.--"} APM • ${summaries.league.pps != null ? f2.format(summaries.league.pps) : "-.--"} PPS • ${summaries.league.vs != null ? f2.format(summaries.league.vs) : "-.--"} VS • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.app) : "-.--"} APP • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) + ], + ), ), ), ), @@ -673,11 +663,11 @@ class _DestinationHomeState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), const Divider(color: Color.fromARGB(50, 158, 158, 158)), RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("Total runs submitted: ${summaries.achievements.firstWhere((e) => e.k == 5).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 5).v!) : "---"}", style: TextStyle(color: Colors.grey)) + Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) ], ), ), @@ -690,11 +680,11 @@ class _DestinationHomeState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), const Divider(color: Color.fromARGB(50, 158, 158, 158)), RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("Total score gained: ${summaries.achievements.firstWhere((e) => e.k == 6).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 6).v!) : "---"}", style: TextStyle(color: Colors.grey)) + Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) ], ), ), @@ -712,11 +702,11 @@ class _DestinationHomeState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), const Divider(color: Color.fromARGB(50, 158, 158, 158)), RecordSummary(record: summaries.zenith, hideRank: true), const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("Overall PB: ${summaries.achievements.firstWhere((e) => e.k == 18).v != null ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: TextStyle(color: Colors.grey)) + Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) ], ), ), @@ -729,11 +719,11 @@ class _DestinationHomeState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), const Divider(color: Color.fromARGB(50, 158, 158, 158)), RecordSummary(record: summaries.zenithEx, hideRank: true,), const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("Overall PB: ${summaries.achievements.firstWhere((e) => e.k == 19).v != null ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: TextStyle(color: Colors.grey)) + Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) ], ), ), @@ -751,10 +741,10 @@ class _DestinationHomeState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), Text("Score ${intf.format(summaries.zen.score)}"), - Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: TextStyle(color: Colors.grey)) + Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: const TextStyle(color: Colors.grey)) ], ), ), @@ -778,8 +768,8 @@ class _DestinationHomeState extends State { const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), Padding( padding: const EdgeInsets.only(left: 10.0), - child: Text("${(summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? - f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: TextStyle( + child: Text("${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? + f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: const TextStyle( //shadows: textShadow, fontFamily: "Eurostile Round Extended", fontSize: 36, @@ -791,16 +781,16 @@ class _DestinationHomeState extends State { ), Row( children: [ - Text("Total pieces placed:"), - Spacer(), - Text("${summaries.achievements.firstWhere((e) => e.k == 1).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"}"), + const Text("Total pieces placed:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"), ], ), Row( children: [ - Text(" - Placed with perfect finesse:"), - Spacer(), - Text("${summaries.achievements.firstWhere((e) => e.k == 4).v != null ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"}"), + const Text(" - Placed with perfect finesse:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"), ], ) ], @@ -810,23 +800,23 @@ class _DestinationHomeState extends State { ), ], ), - Card( + if (summaries.achievements.isNotEmpty) Card( child: Padding( - padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), child: Column( children: [ if (summaries.achievements.firstWhere((e) => e.k == 16).v != null) Row( children: [ - Text("Total height climbed in QP"), - Spacer(), + const Text("Total height climbed in QP"), + const Spacer(), Text("${f2.format(summaries.achievements.firstWhere((e) => e.k == 16).v!)} m"), ], ), if (summaries.achievements.firstWhere((e) => e.k == 17).v != null) Row( children: [ - Text("KO's in QP"), - Spacer(), - Text("${intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)}"), + const Text("KO's in QP"), + const Spacer(), + Text(intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)), ], ) ], @@ -875,15 +865,15 @@ class _DestinationHomeState extends State { Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ return Column( children: [ - Card( + const Card( child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), + padding: EdgeInsets.only(bottom: 4.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text("Records", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("Records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) ], ), @@ -896,7 +886,7 @@ class _DestinationHomeState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - TabBar( + const TabBar( tabs: [ Tab(text: "Recent"), Tab(text: "Top"), @@ -962,7 +952,7 @@ class _DestinationHomeState extends State { ); } } - return Text("what?"); + return const Text("what?"); }, ), FutureBuilder( @@ -1014,7 +1004,7 @@ class _DestinationHomeState extends State { ); } } - return Text("what?"); + return const Text("what?"); }, ), ] @@ -1074,7 +1064,7 @@ class _DestinationHomeState extends State { ); } } - return Text("what?"); + return const Text("what?"); }, ), ), @@ -1118,6 +1108,7 @@ class _DestinationHomeState extends State { child: Card( child: SizedBox( width: 300, + height: 318, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1132,7 +1123,7 @@ class _DestinationHomeState extends State { const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), Padding( padding: const EdgeInsets.only(left: 10.0), - child: Text(getMoreNormalTime(record.stats.finalTime), style: TextStyle( + child: Text(getMoreNormalTime(record.stats.finalTime), style: const TextStyle( shadows: textShadow, fontFamily: "Eurostile Round Extended", fontSize: 36, @@ -1156,11 +1147,11 @@ class _DestinationHomeState extends State { Text("Total", textAlign: TextAlign.right), ] ), - for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( + for (int i = 0; i < record.stats.zenith!.splits.length; i++) TableRow( children: [ Text((i+1).toString()), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), + Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]-(i-1 != -1 ? record.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), + Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), ] ) ], @@ -1191,8 +1182,8 @@ class _DestinationHomeState extends State { Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ if (record == null) { - return Card( - child: Center(child: Text("No record", style: const TextStyle(fontSize: 42))), + return const Card( + child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), ); } return Column( @@ -1243,14 +1234,14 @@ class _DestinationHomeState extends State { text: "", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), children: [ - if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), _ => record.stats.score.toString() }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent )) - else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + else if ((rank == null || rank == "z" || rank == "x+") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), "blitz" => readableIntDifference(record.stats.score, closestAverage.value), _ => record.stats.score.toString() @@ -1281,7 +1272,7 @@ class _DestinationHomeState extends State { "blitz" => record.stats.level.toString(), "5mblast" => NumberFormat.decimalPattern().format(record.stats.spp), _ => "What if " - }, textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), Text(switch(record.gamemode){ "40l" => " Pieces", "blitz" => " Level", @@ -1290,8 +1281,8 @@ class _DestinationHomeState extends State { }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), ]), TableRow(children: [ - Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" PPS", textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" PPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), ]), TableRow(children: [ Text(switch(record.gamemode){ @@ -1299,7 +1290,7 @@ class _DestinationHomeState extends State { "blitz" => f2.format(record.stats.spp), "5mblast" => record.stats.piecesPlaced.toString(), _ => "but god said" - }, textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), Text(switch(record.gamemode){ "40l" => " KPP", "blitz" => " SPP", @@ -1315,12 +1306,12 @@ class _DestinationHomeState extends State { defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ - Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" Key presses", textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Key presses", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), ]), TableRow(children: [ - Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), - Text(" KPS", textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" KPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), ]), TableRow(children: [ Text(switch(record.gamemode){ @@ -1328,7 +1319,7 @@ class _DestinationHomeState extends State { "blitz" => record.stats.piecesPlaced.toString(), "5mblast" => record.stats.piecesPlaced.toString(), _ => "but god said" - }, textAlign: TextAlign.right, style: TextStyle(fontSize: 21)), + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), Text(switch(record.gamemode){ "40l" => " ", "blitz" => " Pieces", @@ -1518,31 +1509,31 @@ class _DestinationHomeState extends State { child: Column( children: [ SizedBox( - height: widget.constraints.maxHeight - 64, + height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, child: SingleChildScrollView( child: switch (rightCard){ Cards.overview => getOverviewCard(snapshot.data!.summaries!), Cards.tetraLeague => switch (cardMod){ CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league), CardMod.records => getRecentTLrecords(widget.constraints), - _ => Center(child: Text("huh?")) + _ => const Center(child: Text("huh?")) }, Cards.quickPlay => switch (cardMod){ CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), - _ => Center(child: Text("huh?")) + _ => const Center(child: Text("huh?")) }, Cards.sprint => switch (cardMod){ CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), - _ => Center(child: Text("huh?")) + _ => const Center(child: Text("huh?")) }, Cards.blitz => switch (cardMod){ CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), - _ => Center(child: Text("huh?")) + _ => const Center(child: Text("huh?")) }, }, ), @@ -1594,7 +1585,7 @@ class _DestinationHomeState extends State { ); } } - return Text("End of FutureBuilder"); + return const Text("End of FutureBuilder"); }, ); } @@ -1890,7 +1881,7 @@ class FakeDistinguishmentThingy extends StatelessWidget{ Color getCardTint(){ if (banned) return Colors.red; if (badStanding) return Colors.redAccent; - if (bot) return Color.fromARGB(255, 60, 93, 55); + if (bot) return const Color.fromARGB(255, 60, 93, 55); return theme.colorScheme.surface; } @@ -1914,9 +1905,9 @@ class FakeDistinguishmentThingy extends StatelessWidget{ return Card( surfaceTintColor: getCardTint(), child: Container( - decoration: banned ? BoxDecoration( + decoration: banned ? const BoxDecoration( gradient: LinearGradient( - colors: [Colors.transparent, const Color.fromARGB(171, 244, 67, 54), Color.fromARGB(171, 244, 67, 54)], + colors: [Colors.transparent, Color.fromARGB(171, 244, 67, 54), Color.fromARGB(171, 244, 67, 54)], stops: [0.1, 0.9, 0.01], tileMode: TileMode.mirror, begin: Alignment.topLeft, @@ -2084,6 +2075,7 @@ class NewUserThingy extends StatelessWidget { child: Stack( //clipBehavior: Clip.none, children: [ + // TODO: osk banner can cause memory leak 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, @@ -2100,7 +2092,6 @@ class NewUserThingy extends StatelessWidget { ? 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); }) @@ -2110,13 +2101,18 @@ class NewUserThingy extends StatelessWidget { Positioned( top: player.bannerRevision != null ? 120.0 : 40.0, left: 160.0, - child: Text(player.username, - //softWrap: true, - overflow: TextOverflow.fade, - style: TextStyle( - fontFamily: fontStyle(player.username.length), - fontSize: 28, - ) + child: Tooltip( + message: "${player.userId}\n(Click to copy user ID)", + child: RichText(text: TextSpan(text: player.username, style: TextStyle( + fontFamily: fontStyle(player.username.length), + fontSize: 28, + ), + recognizer: TapGestureRecognizer()..onTap = (){ + copyToClipboard(player.userId); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); + } + ) + ) ), ), Positioned( @@ -2146,14 +2142,17 @@ class NewUserThingy extends StatelessWidget { Positioned( top: player.bannerRevision != null ? 193.0 : 113.0, left: 160.0, - child: RichText( - text: TextSpan( - style: const 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: const TextStyle(color: Colors.grey)) - ] - ) + child: SizedBox( + width: 270, + child: RichText( + text: TextSpan( + style: const 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: const TextStyle(color: Colors.grey)) + ] + ) + ), ) ), Positioned( @@ -2164,7 +2163,7 @@ class NewUserThingy extends StatelessWidget { text: TextSpan( style: const TextStyle(fontFamily: "Eurostile Round"), children: [ - TextSpan(text: "Level ${intf.format(player.level.floor())}", style: const TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: TapGestureRecognizer()..onTap = (){ + TextSpan(text: "Level ${(player.level.isNegative || player.level.isNaN) ? "---" : intf.format(player.level.floor())}", style: TextStyle(decoration: (player.level.isNegative || player.level.isNaN) ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: (player.level.isNegative || player.level.isNaN) ? Colors.grey : Colors.white), recognizer: (player.level.isNegative || player.level.isNaN) ? null : TapGestureRecognizer()?..onTap = (){ showDialog( context: context, builder: (BuildContext context) => AlertDialog( @@ -2710,7 +2709,7 @@ class ZenithThingy extends StatelessWidget{ defaultColumnWidth:const IntrinsicColumnWidth(), children: [ TableRow(children: [ - Text("${intf.format(zenith!.stats.kills)}", textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + Text(intf.format(zenith!.stats.kills), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), const Text(" KO's", style: TextStyle(fontSize: 21)) ]), TableRow(children: [ @@ -2803,4 +2802,114 @@ class _TLRecords extends StatelessWidget { ); }); } +} + +class TLRatingThingy extends StatelessWidget{ + final String userID; + final TetraLeague tlData; + final TetraLeague? oldTl; + final double? topTR; + final DateTime? lastMatchPlayed; + + const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed}); + + @override + Widget build(BuildContext context) { + bool oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; + bool bigScreen = MediaQuery.of(context).size.width >= 768; + String decimalSeparator = f4.symbols.DECIMAL_SEP; + List formatedTR = f4.format(tlData.tr).split(decimalSeparator); + List formatedGlicko = tlData.glicko != null ? 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()); + return Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + clipBehavior: Clip.hardEdge, + children: [ + (userID == "5e32fc85ab319c2ab1beb07c" && oskKagariGimmick) // he love her so much, you can't even imagine + ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? + : Image.asset("res/tetrio_tl_alpha_ranks/${tlData.rank}.png", height: 128), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white), + children: (tlData.gamesPlayed > 9) ? switch(prefs.getInt("ratingMode")){ + 1 => [ + TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (formatedGlicko.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedGlicko[1]), + TextSpan(text: " Glicko", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + ], + 2 => [ + TextSpan(text: "${t.top} ${formatedPercentile[0]}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (formatedPercentile.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedPercentile[1]), + TextSpan(text: " %", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + ], + _ => [ + TextSpan(text: formatedTR[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (formatedTR.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedTR[1]), + TextSpan(text: " TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + ], + } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),] + ) + ), + if (oldTl != null) Text( + switch(prefs.getInt("ratingMode")){ + 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", + 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", + _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" + }, + textAlign: TextAlign.center, + style: TextStyle( + color: tlData.tr - oldTl!.tr < 0 ? + Colors.red : + Colors.green + ), + ), + if (tlData.gamesPlayed > 9) Column( + children: [ + RichText( + textAlign: TextAlign.center, + softWrap: true, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan(text: prefs.getInt("ratingMode") == 2 ? "${f2.format(tlData.tr)} TR • % ${t.rank}: ${tlData.percentileRank.toUpperCase()}" : "${t.top} ${f2.format(tlData.percentile * 100)}% (${tlData.percentileRank.toUpperCase()})"), + if (tlData.bestRank != "z") const TextSpan(text: " • "), + if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"), + if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"), + TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.tr)} TR • RD: " : "Glicko: ${tlData.glicko != null ? 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)) + ], + ), + ), + ], + ), + RichText( + textAlign: TextAlign.start, + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (tlData.standing != -1) TextSpan(text: "№ ${intf.format(tlData.standing)}", style: TextStyle(color: getColorOfRank(tlData.standing))), + if (tlData.standing != -1 || tlData.standingLocal != -1) const TextSpan(text: " • "), + if (tlData.standingLocal != -1) TextSpan(text: "№ ${intf.format(tlData.standingLocal)} local", style: TextStyle(color: getColorOfRank(tlData.standingLocal))), + if (tlData.standing != -1 && tlData.standingLocal != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(tlData.timestamp)), + ] + ), + ), + ], + ), + ], + ); + } } \ No newline at end of file diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index f373f3a..62d12c4 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -239,7 +239,7 @@ class UserThingy extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.start, clipBehavior: Clip.hardEdge, // hard WHAT??? children: [ - StatCellNum( + if (!player.level.isNegative && !player.level.isNaN) StatCellNum( playerStat: player.level, playerStatLabel: t.statCellNum.xpLevel, isScreenBig: bigScreen, From 38ec643a011422ebe6db5ba7ede491eb86331a28 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 2 Sep 2024 00:44:19 +0300 Subject: [PATCH 29/33] Redoing local DB - Retrieving history is slow rn because i save one entry at the time - No check if entry is already here - S1 history is not avaliable for now - Maybe i should store it like i did that during S1 but idk --- lib/data_objects/tetrio.dart | 102 +++----------- lib/main.dart | 8 +- lib/services/sqlite_db_controller.dart | 1 + lib/services/tetrio_crud.dart | 185 +++++++++++++------------ lib/views/compare_view.dart | 24 ++-- lib/views/main_view.dart | 20 +-- lib/views/main_view_tiles.dart | 53 ++++--- lib/views/rank_averages_view.dart | 2 +- lib/views/state_view.dart | 4 +- lib/views/states_view.dart | 8 +- lib/views/tl_leaderboard_view.dart | 2 +- lib/widgets/tl_progress_bar.dart | 2 +- lib/widgets/tl_rating_thingy.dart | 2 +- lib/widgets/tl_thingy.dart | 10 +- lib/widgets/user_thingy.dart | 3 +- lib/widgets/zenith_thingy.dart | 2 +- 16 files changed, 196 insertions(+), 232 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 70e8a0f..25092c2 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -3,10 +3,10 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:vector_math/vector_math.dart'; +const int currentSeason = 2; const double noTrRd = 60.9; const double apmWeight = 1; const double ppsWeight = 45; @@ -228,7 +228,6 @@ class TetrioPlayer { bool? badstanding; String? botmaster; Connections? connections; - TetraLeague? tlSeason1; TetrioZen? zen; Distinguishment? distinguishment; DateTime? cachedUntil; @@ -254,7 +253,6 @@ class TetrioPlayer { this.badstanding, this.botmaster, required this.connections, - required this.tlSeason1, this.zen, this.distinguishment, this.cachedUntil @@ -281,7 +279,6 @@ class TetrioPlayer { country = json['country']; supporterTier = json['supporter_tier'] ?? 0; verified = json['verified'] ?? false; - tlSeason1 = json['league'] != null ? TetraLeague.fromJson(json['league'], stateTime) : null; avatarRevision = json['avatar_revision']; bannerRevision = json['banner_revision']; bio = json['bio']; @@ -307,7 +304,6 @@ class TetrioPlayer { if (country != null) data['country'] = country; if (supporterTier > 0) data['supporter_tier'] = supporterTier; if (verified) data['verified'] = verified; - 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; @@ -337,83 +333,15 @@ class TetrioPlayer { if (badstanding != other.badstanding) return false; if (botmaster != other.botmaster) return false; if (connections != other.connections) return false; - if (tlSeason1 != other.tlSeason1) return false; if (distinguishment != other.distinguishment) return false; return true; } - bool checkForRetrivedHistory(covariant TetrioPlayer other) { - return tlSeason1!.lessStrictCheck(other.tlSeason1!); - } - @override String toString() { return "$username ($state)"; } - num? getStatByEnum(Stats stat){ - switch (stat) { - case Stats.tr: - return tlSeason1?.tr; - case Stats.glicko: - return tlSeason1?.glicko; - case Stats.gxe: - return tlSeason1?.gxe; - case Stats.s1tr: - return tlSeason1?.s1tr; - case Stats.rd: - return tlSeason1?.rd; - case Stats.gp: - return tlSeason1?.gamesPlayed; - case Stats.gw: - return tlSeason1?.gamesWon; - case Stats.wr: - return tlSeason1?.winrate; - case Stats.apm: - return tlSeason1?.apm; - case Stats.pps: - return tlSeason1?.pps; - case Stats.vs: - return tlSeason1?.vs; - case Stats.app: - return tlSeason1?.nerdStats?.app; - case Stats.dss: - return tlSeason1?.nerdStats?.dss; - case Stats.dsp: - return tlSeason1?.nerdStats?.dsp; - case Stats.appdsp: - return tlSeason1?.nerdStats?.appdsp; - case Stats.vsapm: - return tlSeason1?.nerdStats?.vsapm; - case Stats.cheese: - return tlSeason1?.nerdStats?.cheese; - case Stats.gbe: - return tlSeason1?.nerdStats?.gbe; - case Stats.nyaapp: - return tlSeason1?.nerdStats?.nyaapp; - case Stats.area: - return tlSeason1?.nerdStats?.area; - case Stats.eTR: - return tlSeason1?.estTr?.esttr; - case Stats.acceTR: - return tlSeason1?.esttracc; - case Stats.acceTRabs: - return tlSeason1?.esttracc?.abs(); - case Stats.opener: - return tlSeason1?.playstyle?.opener; - case Stats.plonk: - return tlSeason1?.playstyle?.plonk; - case Stats.infDS: - return tlSeason1?.playstyle?.infds; - case Stats.stride: - return tlSeason1?.playstyle?.stride; - case Stats.stridemMinusPlonk: - 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; - } - } - @override int get hashCode => state.hashCode; @@ -444,7 +372,7 @@ class Summaries{ if (json['zenithex']['record'] != null) zenithEx = RecordSingle.fromJson(json['zenithex']['record'], json['zenithex']['rank'], json['zenithex']['rank_local']); if (json['zenithex']['best']['record'] != null) zenithCareerBest = RecordSingle.fromJson(json['zenithex']['best']['record'], json['zenith']['best']['rank'], -1); achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; - league = TetraLeague.fromJson(json['league'], DateTime.now()); + league = TetraLeague.fromJson(json['league'], DateTime.now(), currentSeason, i); zen = TetrioZen.fromJson(json['zen']); } } @@ -1373,6 +1301,7 @@ class EndContextMulti { } class TetraLeague { + late String id; late DateTime timestamp; late int gamesPlayed; late int gamesWon; @@ -1397,10 +1326,11 @@ class TetraLeague { NerdStats? nerdStats; EstTr? estTr; Playstyle? playstyle; - List? records; + late int season; TetraLeague( - {required this.timestamp, + {required this.id, + required this.timestamp, required this.gamesPlayed, required this.gamesWon, required this.bestRank, @@ -1421,7 +1351,7 @@ class TetraLeague { this.apm, this.pps, this.vs, - this.records}){ + required this.season}){ nerdStats = (apm != null && pps != null && vs != null) ? NerdStats(apm!, pps!, vs!) : null; estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; playstyle =(nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; @@ -1430,8 +1360,10 @@ class TetraLeague { double get winrate => gamesWon / gamesPlayed; double get s1tr => gxe * 250; - TetraLeague.fromJson(Map json, ts) { + TetraLeague.fromJson(Map json, ts, int s, String i) { timestamp = ts; + season = s; + id = i; gamesPlayed = json['gamesplayed'] ?? 0; gamesWon = json['gameswon'] ?? 0; tr = json['tr'] != null ? json['tr'].toDouble() : json['rating'] != null ? json['rating'].toDouble() : -1; @@ -1470,25 +1402,29 @@ class TetraLeague { Map toJson() { final Map data = {}; + data['id'] = id; + data['timestamp'] = timestamp.millisecondsSinceEpoch; if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed; if (gamesWon > 0) data['gameswon'] = gamesWon; if (tr >= 0) data['tr'] = tr; if (glicko != null) data['glicko'] = glicko; + if (gxe != -1) data['gxe'] = gxe; if (rd != null && rd != noTrRd) data['rd'] = rd; if (rank != 'z') data['rank'] = rank; if (bestRank != 'z') data['bestrank'] = bestRank; if (apm != null) data['apm'] = apm; if (pps != null) data['pps'] = pps; if (vs != null) data['vs'] = vs; - if (decaying) data['decaying'] = decaying; + if (decaying) data['decaying'] = decaying ? 1 : 0; if (standing >= 0) data['standing'] = standing; - if (!rankCutoffs.containsValue(percentile)) data['percentile'] = percentile; + data['percentile'] = percentile; if (standingLocal >= 0) data['standing_local'] = standingLocal; if (prevRank != null) data['prev_rank'] = prevRank; if (prevAt >= 0) data['prev_at'] = prevAt; if (nextRank != null) data['next_rank'] = nextRank; if (nextAt >= 0) data['next_at'] = nextAt; - if (percentileRank != rank) data['percentile_rank'] = percentileRank; + data['percentile_rank'] = percentileRank; + data['season'] = season; return data; } } @@ -2193,7 +2129,7 @@ class TetrioPlayersLeaderboard { avgInfDS /= filtredLeaderboard.length; avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor(); avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor(); - return [TetraLeague(timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, gxe: avgGlixare, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), + return [TetraLeague(id: "", timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, gxe: avgGlixare, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason), { "everyone": rank == "", "totalGamesPlayed": totalGamesPlayed, @@ -2375,7 +2311,7 @@ class TetrioPlayersLeaderboard { "entries": filtredLeaderboard }]; }else{ - return [TetraLeague(timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, tr: 0, rank: rank, percentileRank: rank, gxe: -1, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), + return [TetraLeague(id: "", timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, tr: 0, rank: rank, percentileRank: rank, gxe: -1, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason), {"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0, "toEnterGlicko": 0}]; } } diff --git a/lib/main.dart b/lib/main.dart index 3f81e0f..bbba219 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,14 +39,14 @@ ThemeData theme = ThemeData( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(12.0), right: Radius.circular(12.0)))), elevation: WidgetStatePropertyAll(8.0) ), - chipTheme: ChipThemeData( + chipTheme: const ChipThemeData( side: BorderSide(color: Colors.transparent), ), segmentedButtonTheme: SegmentedButtonThemeData( style: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide(color: Colors.transparent)), - surfaceTintColor: WidgetStatePropertyAll(Colors.cyanAccent), - iconColor: WidgetStatePropertyAll(Colors.cyanAccent), + side: const WidgetStatePropertyAll(BorderSide(color: Colors.transparent)), + surfaceTintColor: const WidgetStatePropertyAll(Colors.cyanAccent), + iconColor: const WidgetStatePropertyAll(Colors.cyanAccent), shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200), ) ), diff --git a/lib/services/sqlite_db_controller.dart b/lib/services/sqlite_db_controller.dart index 1af160c..b1bb348 100644 --- a/lib/services/sqlite_db_controller.dart +++ b/lib/services/sqlite_db_controller.dart @@ -33,6 +33,7 @@ class DB { await db.execute(createTetrioUsersToTrack); await db.execute(createTetrioTLRecordsTable); await db.execute(createTetrioTLReplayStats); + await db.execute(createTetrioLeagueTable); } on MissingPlatformDirectoryException { throw UnableToGetDocuments(); } diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index d39419c..e97d16c 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sql.dart'; import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/main.dart' show packageInfo; @@ -21,6 +22,7 @@ const String tetrioUsersTable = "tetrioUsers"; const String tetrioUsersToTrackTable = "tetrioUsersToTrack"; const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces"; const String tetrioTLReplayStatsTable = "tetrioTLReplayStats"; +const String tetrioLeagueTable = "tetrioLeague"; const String idCol = "id"; const String replayID = "replayId"; const String nickCol = "nickname"; @@ -67,6 +69,33 @@ const String createTetrioTLReplayStats = ''' PRIMARY KEY("id") ) '''; +const String createTetrioLeagueTable = ''' + CREATE TABLE IF NOT EXISTS "tetrioLeague" ( + "id" TEXT NOT NULL, + "timestamp" INTEGER NOT NULL, + "gamesplayed" INTEGER NOT NULL DEFAULT 0, + "gameswon" INTEGER NOT NULL DEFAULT 0, + "tr" REAL, + "glicko" REAL, + "rd" REAL, + "gxe" REAL, + "rank" TEXT NOT NULL DEFAULT 'z', + "bestrank" TEXT NOT NULL DEFAULT 'z', + "apm" REAL, + "pps" REAL, + "vs" REAL, + "decaying" INTEGER NOT NULL DEFAULT 0, + "standing" INTEGER NOT NULL DEFAULT -1, + "standing_local" INTEGER NOT NULL DEFAULT -1, + "percentile" REAL NOT NULL, + "prev_rank" TEXT, + "prev_at" INTEGER NOT NULL DEFAULT -1, + "next_rank" TEXT, + "next_at" INTEGER NOT NULL DEFAULT -1, + "percentile_rank" TEXT NOT NULL DEFAULT 'z', + "season" INTEGER NOT NULL DEFAULT 1 + ) +'''; class CacheController { late Map _cache; @@ -546,7 +575,7 @@ class TetrioService extends DB { /// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data. - Future> fetchAndsaveTLHistory(String id) async { + Future> fetchAndsaveTLHistory(String id) async { Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); @@ -558,27 +587,14 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); // that one api returns csv instead of json List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); - List history = []; - // doesn't return nickname, need to retrieve it separately - String nick = await getNicknameByID(id); + List history = []; for (List entry in csv){ // each entry is one state - TetrioPlayer state = TetrioPlayer( - userId: id, - username: nick, - role: "p1nkl0bst3r", - state: DateTime.parse(entry[9]), - badges: [], - friendCount: -1, - gamesPlayed: -1, - gamesWon: -1, - gameTime: const Duration(seconds: -1), - xp: -1, - supporterTier: 0, - verified: false, - connections: null, - tlSeason1: TetraLeague( + TetraLeague state = TetraLeague( + id: id, timestamp: DateTime.parse(entry[9]), apm: entry[6] != '' ? entry[6] : null, pps: entry[7] != '' ? entry[7] : null, @@ -597,24 +613,12 @@ class TetrioService extends DB { standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1 - ), + prevAt: -1, + season: 1 ); history.add(state); + await db.insert(tetrioLeagueTable, state.toJson(), conflictAlgorithm: ConflictAlgorithm.replace); } - - // trying to dump it to local DB - await ensureDbIsOpen(); - final db = getDatabaseOrThrow(); - List states = await getPlayer(id); - if (states.isEmpty) await createPlayer(history.first); - states.insertAll(0, history.reversed); - final Map statesJson = {}; - for (var e in states) { // making one big json out of this list - statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); - } - // and putting it to local DB - await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]); return history; case 404: developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); @@ -1057,7 +1061,10 @@ class TetrioService extends DB { if (results.isNotEmpty) { throw TetrioPlayerAlreadyExist(); } + await db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username}, conflictAlgorithm: ConflictAlgorithm.replace); db.insert(tetrioUsersToTrackTable, {idCol: tetrioPlayer.userId}); + _players[tetrioPlayer.userId] = tetrioPlayer.username; + _tetrioStreamController.add(_players); } /// Returns bool, which tells whether is given [id] is in [tetrioUsersToTrackTable]. @@ -1081,6 +1088,7 @@ class TetrioService extends DB { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final deletedPlayer = await db.delete(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + await db.delete(tetrioUsersTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); if (deletedPlayer != 1) { throw CouldNotDeletePlayer(); } else { @@ -1089,72 +1097,67 @@ class TetrioService extends DB { } } - /// Saves state (which is [tetrioPlayer]) to the local database. - Future storeState(TetrioPlayer tetrioPlayer) async { - // if tetrio player doesn't have entry in database - just calling different function - List states = await getPlayer(tetrioPlayer.userId); - if (states.isEmpty) { - await createPlayer(tetrioPlayer); - return; - } - - // we not going to add state, that is same, as the previous - if (!states.last.isSameState(tetrioPlayer)) states.add(tetrioPlayer); - - // Making map of the states - final Map statesJson = {}; - for (var e in states) { - // Saving in format: {"unix_seconds": json_of_state} - statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); - } - - // Rewrite our database + Future> getStates(String userID, int season) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); - await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, - where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); + List query = await db.query(tetrioLeagueTable, where: '"id" = ? AND "season" = ?', whereArgs: [userID, season]); + List result = []; + for (var entry in query){ + result.add(TetraLeague.fromJson(entry as Map, entry["timestamp"], entry["season"], entry["id"])); + } + return result; + } + + /// Saves state (which is [TetraLeague]) to the local database. + Future storeState(TetraLeague league) async { + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + List test = await db.query(tetrioLeagueTable, where: '"id" = ? AND "gamesplayed" = ? AND "rd" = ?', whereArgs: [league.id, league.gamesPlayed, league.rd]); + if (test.isEmpty) { + await db.insert(tetrioLeagueTable, league.toJson()); + } } /// Remove state (which is [tetrioPlayer]) from the local database - Future deleteState(TetrioPlayer tetrioPlayer) async { - await ensureDbIsOpen(); - final db = getDatabaseOrThrow(); - List states = await getPlayer(tetrioPlayer.userId); - // removing state from map that contain every state of each user - states.removeWhere((element) => element.state == tetrioPlayer.state); + // Future deleteState(TetrioPlayer tetrioPlayer) async { + // await ensureDbIsOpen(); + // final db = getDatabaseOrThrow(); + // //List states = await getPlayer(tetrioPlayer.userId); + // // removing state from map that contain every state of each user + // states.removeWhere((element) => element.state == tetrioPlayer.state); - // Making map of the states (without deleted one) - final Map statesJson = {}; - for (var e in states) { - statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); - } - // Rewriting database entry with new json - await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, - where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); - _tetrioStreamController.add(_players); - } + // // Making map of the states (without deleted one) + // final Map statesJson = {}; + // // for (var e in states) { + // // statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); + // // } + // // Rewriting database entry with new json + // await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, + // where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); + // _tetrioStreamController.add(_players); + // } /// Returns list of all states of player with given [id] from database. Can return empty list if player /// was not found. - Future> getPlayer(String id) async { - await ensureDbIsOpen(); - final db = getDatabaseOrThrow(); - List states = []; - final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); - if (results.isEmpty) { - return states; // it empty - } else { - dynamic rawStates = results.first['jsonStates'] as String; - rawStates = json.decode(rawStates); - // recreating objects of states - rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String))); - // updating the stream - _players.removeWhere((key, value) => key == id); - _players.addEntries({states.last.userId: states.last.username}.entries); - _tetrioStreamController.add(_players); - return states; - } - } + // Future> getPlayer(String id) async { + // await ensureDbIsOpen(); + // final db = getDatabaseOrThrow(); + // List states = []; + // final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + // if (results.isEmpty) { + // return states; // it empty + // } else { + // dynamic rawStates = results.first['jsonStates'] as String; + // rawStates = json.decode(rawStates); + // // recreating objects of states + // rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String))); + // // updating the stream + // _players.removeWhere((key, value) => key == id); + // _players.addEntries({states.last.userId: states.last.username}.entries); + // _tetrioStreamController.add(_players); + // return states; + // } + // } /// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user. /// If [isItDiscordID] is true, function expects [user] to be a discord user id. Throws an exception if fails to retrieve. diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 8c0df87..333df80 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -85,6 +85,7 @@ class CompareState extends State { theRedSide = [null, null, Summaries(user, TetraLeague( + id: "", timestamp: DateTime.now(), apm: apm, pps: pps, @@ -102,7 +103,7 @@ class CompareState extends State { standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1), TetrioZen(level: 0, score: 0))]; + prevAt: -1, season: currentSeason), TetrioZen(level: 0, score: 0))]; return setState(() {}); } var player = await teto.fetchPlayer(user); @@ -132,9 +133,11 @@ class CompareState extends State { _justUpdate(); } - void changeRedSide(TetrioPlayer user) { - setState(() {theRedSide[0] = user; - theRedSide[2].league = user.tlSeason1;}); + void changeRedSide(TetraLeague user) { + setState(() { + //theRedSide[0] = user; + theRedSide[2].league = user; + }); } void fetchGreenSide(String user) async { @@ -161,6 +164,7 @@ class CompareState extends State { theGreenSide = [null, null, Summaries(user, TetraLeague( + id: "", timestamp: DateTime.now(), apm: apm, pps: pps, @@ -178,7 +182,7 @@ class CompareState extends State { standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1), TetrioZen(level: 0, score: 0))]; + prevAt: -1, season: currentSeason), TetrioZen(level: 0, score: 0))]; return setState(() {}); } var player = await teto.fetchPlayer(user); @@ -208,9 +212,11 @@ class CompareState extends State { _justUpdate(); } - void changeGreenSide(TetrioPlayer user) { - setState(() {theGreenSide[0] = user; - theGreenSide[2].league = user.tlSeason1;}); + void changeGreenSide(TetraLeague user) { + setState(() { + //theGreenSide[0] = user; + theGreenSide[2].league = user; + }); } double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { @@ -955,7 +961,7 @@ class CompareState extends State { const Divider(), Padding( padding: const EdgeInsets.only(bottom: 16), - child: Text("${t.quickPlay} ${t.expert} ${t.nerdStats}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + child: Text("${t.quickPlay} ${t.expert} ${t.nerdStats}", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), ), CompareThingy( label: "APP", diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 9b06306..2d4fc1d 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -216,15 +216,15 @@ class _MainState extends State with TickerProviderStateMixin { if (everyone != null && summaries.league.gamesPlayed > 9) rankAverages = everyone?.averages[summaries.league.percentileRank]?[0]; // Making list of Tetra League matches - //bool isTracking = await teto.isPlayerTracking(me.userId); + bool isTracking = await teto.isPlayerTracking(me.userId); List states = []; TetraLeague? compareWith; Set uniqueTL = {}; 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); - // } + if (isTracking){ // if tracked - save data to local DB + await teto.storeState(summaries.league); + //await teto.saveTLMatchesFromStream(tlStream); + } TetraLeagueAlphaStream? oldMatches; // building list of TL matches if(fetchTLmatches) { @@ -271,11 +271,11 @@ 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 (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); - if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); - } + //states.addAll(await teto.getPlayer(me.userId)); + // for (var element in states) { // For graphs I need only unique entries + // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); + // if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); + // } // Also i need previous Tetra League State for comparison if avaliable if (uniqueTL.length >= 2){ compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 83d57ab..c749d5b 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; @@ -286,11 +287,11 @@ class _DestinationGraphsState extends State { } } - states.addAll(await teto.getPlayer(widget.searchFor)); - for (var element in states) { - if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); - if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); - } + //states.addAll(await teto.getPlayer(widget.searchFor)); + // for (var element in states) { + // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); + // if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); + // } if (uniqueTL.length >= 2){ chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid @@ -511,9 +512,10 @@ class FetchResults{ bool success; TetrioPlayer? player; Summaries? summaries; + Cutoffs? cutoffs; Exception? exception; - FetchResults(this.success, this.player, this.summaries, this.exception); + FetchResults(this.success, this.player, this.summaries, this.cutoffs, this.exception); } class RecordSummary extends StatelessWidget{ @@ -613,10 +615,17 @@ class _DestinationHomeState extends State { player = await teto.fetchPlayer(widget.searchFor); // Otherwise it's probably a user id or username } }on TetrioPlayerNotExist{ - return FetchResults(false, null, null, TetrioPlayerNotExist()); + return FetchResults(false, null, null, null, TetrioPlayerNotExist()); } - Summaries summaries = await teto.fetchSummaries(player.userId); - return FetchResults(true, player, summaries, null); + late Summaries summaries; + late Cutoffs cutoffs; + List requests = await Future.wait([ + teto.fetchSummaries(player.userId), + teto.fetchCutoffsBeanserver(), + ]); + summaries = requests[0]; + cutoffs = requests[1]; + return FetchResults(true, player, summaries, cutoffs, null); } Widget getOverviewCard(Summaries summaries){ @@ -645,7 +654,7 @@ class _DestinationHomeState extends State { children: [ const Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), const Divider(color: Color.fromARGB(50, 158, 158, 158)), - TLRatingThingy(userID: "", tlData: summaries.league), + TLRatingThingy(userID: "", tlData: summaries.league, showPositions: true), const Divider(color: Color.fromARGB(50, 158, 158, 158)), Text("${summaries.league.apm != null ? f2.format(summaries.league.apm) : "-.--"} APM • ${summaries.league.pps != null ? f2.format(summaries.league.pps) : "-.--"} PPS • ${summaries.league.vs != null ? f2.format(summaries.league.vs) : "-.--"} VS • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.app) : "-.--"} APP • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) ], @@ -827,7 +836,7 @@ class _DestinationHomeState extends State { ); } - Widget getTetraLeagueCard(TetraLeague data){ + Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs){ return Column( children: [ Card( @@ -845,7 +854,7 @@ class _DestinationHomeState extends State { ), ), ), - TetraLeagueThingy(league: data), + TetraLeagueThingy(league: data, cutoffs: cutoffs), if (data.nerdStats != null) Card( child: Row( mainAxisSize: MainAxisSize.min, @@ -1514,7 +1523,7 @@ class _DestinationHomeState extends State { child: switch (rightCard){ Cards.overview => getOverviewCard(snapshot.data!.summaries!), Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league), + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs), CardMod.records => getRecentTLrecords(widget.constraints), _ => const Center(child: Text("huh?")) }, @@ -2340,8 +2349,9 @@ class _SearchDrawerState extends State { class TetraLeagueThingy extends StatelessWidget{ final TetraLeague league; + final Cutoffs? cutoffs; - const TetraLeagueThingy({super.key, required this.league}); + const TetraLeagueThingy({super.key, required this.league, this.cutoffs}); @override Widget build(BuildContext context) { @@ -2349,7 +2359,15 @@ class TetraLeagueThingy extends StatelessWidget{ child: Column( children: [ TLRatingThingy(userID: "w", tlData: league), - TLProgress(tlData: league,), + TLProgress( + tlData: league, + previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, + nextRankTRcutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.tr[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, + nextRankTRcutoffTarget: league.rank != "z" ? rankTargets[league.rank] : null, + previousRankTRcutoffTarget: (league.rank != "z" && league.rank != "x+") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(league.rank)+1)] : null, + previousGlickoCutoff: cutoffs != null ? cutoffs!.glicko[league.rank != "z" ? league.rank : league.percentileRank] : null, + nextRankGlickoCutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.glicko[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, + ), Row( // spacing: 25.0, // alignment: WrapAlignment.spaceAround, @@ -2809,9 +2827,10 @@ class TLRatingThingy extends StatelessWidget{ final TetraLeague tlData; final TetraLeague? oldTl; final double? topTR; + final bool? showPositions; final DateTime? lastMatchPlayed; - const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed}); + const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed, this.showPositions}); @override Widget build(BuildContext context) { @@ -2893,7 +2912,7 @@ class TLRatingThingy extends StatelessWidget{ ), ], ), - RichText( + if (showPositions == true) RichText( textAlign: TextAlign.start, text: TextSpan( text: "", diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 557bda3..14c6ad0 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -523,7 +523,7 @@ class _ListEntry extends StatelessWidget { children: [ Text(f.format(value), style: const TextStyle(fontSize: 22, height: 0.9)), - if (id.isNotEmpty) Text(t.forPlayer(username: username), style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w100),) + if (id.isNotEmpty) Text(t.forPlayer(username: username), style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.w100),) ], ), onTap: id.isNotEmpty diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart index 19b190e..9be1ccf 100644 --- a/lib/views/state_view.dart +++ b/lib/views/state_view.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/widgets/tl_thingy.dart'; +//import 'package:tetra_stats/widgets/tl_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:window_manager/window_manager.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: Container()))); } } diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart index a7cab43..f5a0f2c 100644 --- a/lib/views/states_view.dart +++ b/lib/views/states_view.dart @@ -61,14 +61,14 @@ 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??0))), + subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: 0)), trailing: IconButton( icon: const Icon(Icons.delete_forever), onPressed: () { DateTime nn = widget.states[index].state; - teto.deleteState(widget.states[index]).then((value) => setState(() { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn))))); - })); + // teto.deleteState(widget.states[index]).then((value) => setState(() { + // ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn))))); + // })); }, ), onTap: () { diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index 0a3d87e..d7699ad 100644 --- a/lib/views/tl_leaderboard_view.dart +++ b/lib/views/tl_leaderboard_view.dart @@ -210,7 +210,7 @@ class TLLeaderboardState extends State { ) ); } - return Text("end of FutureBuilder"); + return const Text("end of FutureBuilder"); } })), ); diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index 8a1be3c..bc8f94f 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -51,7 +51,7 @@ class TLProgress extends StatelessWidget{ ] ) ), - Spacer(), + const Spacer(), RichText( textAlign: TextAlign.right, text: TextSpan( diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index 3fc69be..ac7d4c3 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -59,7 +59,7 @@ class TLRatingThingy extends StatelessWidget{ if (formatedTR.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedTR[1]), TextSpan(text: " TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) ], - } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: TextStyle(color: Colors.grey, fontSize: 14)),] + } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),] ) ), if (oldTl != null) Text( diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 0731e10..853adde 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -22,7 +22,7 @@ var intFDiff = NumberFormat("+#,###.000;-#,###.000"); class TLThingy extends StatefulWidget { final TetraLeague tl; final String userID; - final List states; + final List states; final bool showTitle; final bool bot; final bool guest; @@ -47,13 +47,13 @@ class _TLThingyState extends State with TickerProviderStateMixin { late TetraLeague? oldTl; late TetraLeague currentTl; late RangeValues _currentRangeValues; - late List sortedStates; + late List sortedStates; @override void initState() { _currentRangeValues = const RangeValues(0, 1); sortedStates = widget.states.reversed.toList(); - oldTl = sortedStates.elementAtOrNull(1)?.tlSeason1; + oldTl = sortedStates.elementAtOrNull(1); currentTl = widget.tl; super.initState(); } @@ -95,12 +95,12 @@ 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]!; } if (values.end.round() == 0){ oldTl = widget.tl; }else{ - oldTl = sortedStates[values.end.round()-1].tlSeason1; + oldTl = sortedStates[values.end.round()-1]; } }); }, diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 62d12c4..f27e7a3 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -182,7 +182,6 @@ class UserThingy extends StatelessWidget { ],), onPressed: () { teto.addPlayerToTrack(player).then((value) => setState()); - teto.storeState(player); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked))); }, ), @@ -213,7 +212,7 @@ class UserThingy extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => CompareView(greenSide: [player, null, player.tlSeason1], redSide: const [null, null, null], greenMode: Mode.player, redMode: Mode.player), + builder: (context) => CompareView(greenSide: [player, null, null], redSide: const [null, null, null], greenMode: Mode.player, redMode: Mode.player), ), ); }, diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index 0a9394c..b8c8535 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -148,7 +148,7 @@ class _ZenithThingyState extends State { const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), Padding( padding: const EdgeInsets.only(left: 10.0), - child: Text("${getMoreNormalTime(record!.stats.finalTime)}", style: TextStyle( + child: Text(getMoreNormalTime(record!.stats.finalTime), style: const TextStyle( shadows: textShadow, fontFamily: "Eurostile Round Extended", fontSize: 36, From 9ed6ddb33d7b5acb5a378dba91061461723925cc Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 3 Sep 2024 00:17:09 +0300 Subject: [PATCH 30/33] ok that works faster but i still need some tests --- lib/data_objects/tetrio.dart | 9 ++++++--- lib/services/tetrio_crud.dart | 19 +++++++++++++++---- lib/views/main_view.dart | 10 +++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 25092c2..c2b02c2 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -1375,7 +1375,11 @@ class TetraLeague { apm = json['apm']?.toDouble(); pps = json['pps']?.toDouble(); vs = json['vs']?.toDouble(); - decaying = json['decaying'] ?? false; + decaying = switch(json['decaying'].runtimeType){ + int => json['decaying'] == 1, + bool => json['decaying'], + _ => false + }; standing = json['standing'] ?? -1; percentile = json['percentile'] != null ? json['percentile'].toDouble() : rankCutoffs[rank]; standingLocal = json['standing_local'] ?? -1; @@ -1402,8 +1406,7 @@ class TetraLeague { Map toJson() { final Map data = {}; - data['id'] = id; - data['timestamp'] = timestamp.millisecondsSinceEpoch; + data['id'] = id+timestamp.millisecondsSinceEpoch.toRadixString(16); if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed; if (gamesWon > 0) data['gameswon'] = gamesWon; if (tr >= 0) data['tr'] = tr; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index e97d16c..1d93cab 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -6,6 +6,7 @@ import 'dart:developer' as developer; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sql.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/main.dart' show packageInfo; @@ -72,7 +73,6 @@ const String createTetrioTLReplayStats = ''' const String createTetrioLeagueTable = ''' CREATE TABLE IF NOT EXISTS "tetrioLeague" ( "id" TEXT NOT NULL, - "timestamp" INTEGER NOT NULL, "gamesplayed" INTEGER NOT NULL DEFAULT 0, "gameswon" INTEGER NOT NULL DEFAULT 0, "tr" REAL, @@ -93,7 +93,8 @@ const String createTetrioLeagueTable = ''' "next_rank" TEXT, "next_at" INTEGER NOT NULL DEFAULT -1, "percentile_rank" TEXT NOT NULL DEFAULT 'z', - "season" INTEGER NOT NULL DEFAULT 1 + "season" INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY("id") ) '''; @@ -592,6 +593,7 @@ class TetrioService extends DB { // that one api returns csv instead of json List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); List history = []; + Batch batch = db.batch(); for (List entry in csv){ // each entry is one state TetraLeague state = TetraLeague( id: id, @@ -617,8 +619,9 @@ class TetrioService extends DB { season: 1 ); history.add(state); - await db.insert(tetrioLeagueTable, state.toJson(), conflictAlgorithm: ConflictAlgorithm.replace); + batch.insert(tetrioLeagueTable, state.toJson(), conflictAlgorithm: ConflictAlgorithm.replace); } + batch.commit(); return history; case 404: developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); @@ -1112,12 +1115,20 @@ class TetrioService extends DB { Future storeState(TetraLeague league) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); - List test = await db.query(tetrioLeagueTable, where: '"id" = ? AND "gamesplayed" = ? AND "rd" = ?', whereArgs: [league.id, league.gamesPlayed, league.rd]); + List test = await db.query(tetrioLeagueTable, where: '"id" LIKE ? AND "gamesplayed" = ? AND "rd" = ?', whereArgs: ["${league.id}%", league.gamesPlayed, league.rd]); if (test.isEmpty) { await db.insert(tetrioLeagueTable, league.toJson()); } } + Future> getHistory(String id, {int season = currentSeason}) async { + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + List raw = await db.query(tetrioLeagueTable, where: '"id" = ? AND "season" = ?', whereArgs: [id, season]); + List result = [for (var entry in raw) TetraLeague.fromJson(entry as Map, DateTime.fromMillisecondsSinceEpoch(int.parse(entry["id"].substring(24), radix: 16)), entry["season"], entry["id"].substring(0, 24))]; + return result; + } + /// Remove state (which is [tetrioPlayer]) from the local database // Future deleteState(TetrioPlayer tetrioPlayer) async { // await ensureDbIsOpen(); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 2d4fc1d..078a2fb 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -217,7 +217,7 @@ class _MainState extends State with TickerProviderStateMixin { // Making list of Tetra League matches bool isTracking = await teto.isPlayerTracking(me.userId); - List states = []; + List states = await teto.getHistory(me.userId); TetraLeague? compareWith; Set uniqueTL = {}; List storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches @@ -277,8 +277,8 @@ class _MainState extends State with TickerProviderStateMixin { // if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); // } // Also i need previous Tetra League State for comparison if avaliable - if (uniqueTL.length >= 2){ - compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); + if (states.length >= 2){ + compareWith = states.elementAtOrNull(states.length - 2); chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), @@ -312,7 +312,7 @@ class _MainState extends State with TickerProviderStateMixin { changePlayer(me.userId); }); } - return [me, summaries, news, tlStream, recentZenith, recentZenithEX]; + return [me, summaries, news, tlStream, recentZenith, recentZenithEX, states]; //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; } @@ -471,7 +471,7 @@ class _MainState extends State with TickerProviderStateMixin { child: TLThingy( tl: snapshot.data![1].league, userID: snapshot.data![0].userId, - states: const [], //snapshot.data![2], + states: snapshot.data![6], //topTR: snapshot.data![7]?.tr, //lastMatchPlayed: snapshot.data![11], bot: snapshot.data![0].role == "bot", From 3d28e5a21429840916131e015fd0d01be34a4b4e Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 4 Sep 2024 00:07:27 +0300 Subject: [PATCH 31/33] history is fixed, but maaan... --- lib/services/tetrio_crud.dart | 20 ++---- lib/views/main_view.dart | 95 +++++++++++++++++------------ lib/views/states_view.dart | 26 ++++---- lib/views/tracked_players_view.dart | 12 ++-- 4 files changed, 79 insertions(+), 74 deletions(-) diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 1d93cab..8308dcb 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -576,7 +576,7 @@ class TetrioService extends DB { /// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data. - Future> fetchAndsaveTLHistory(String id) async { + Future> fetchAndsaveTLHistory(String id) async { Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); @@ -1100,15 +1100,11 @@ class TetrioService extends DB { } } - Future> getStates(String userID, int season) async { + Future> getStates(String userID, {int? season}) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); - List query = await db.query(tetrioLeagueTable, where: '"id" = ? AND "season" = ?', whereArgs: [userID, season]); - List result = []; - for (var entry in query){ - result.add(TetraLeague.fromJson(entry as Map, entry["timestamp"], entry["season"], entry["id"])); - } - return result; + List query = await db.query(tetrioLeagueTable, where: season != null ? '"id" LIKE ? AND "season" = ?' : '"id" LIKE ?', whereArgs: season != null ? ["${userID}%", season] : ["${userID}%"], orderBy: '"id" ASC'); + return [for (var entry in query) TetraLeague.fromJson(entry as Map, DateTime.fromMillisecondsSinceEpoch(int.parse(entry["id"].substring(24), radix: 16)), entry["season"], entry["id"].substring(0, 24))]; } /// Saves state (which is [TetraLeague]) to the local database. @@ -1121,14 +1117,6 @@ class TetrioService extends DB { } } - Future> getHistory(String id, {int season = currentSeason}) async { - await ensureDbIsOpen(); - final db = getDatabaseOrThrow(); - List raw = await db.query(tetrioLeagueTable, where: '"id" = ? AND "season" = ?', whereArgs: [id, season]); - List result = [for (var entry in raw) TetraLeague.fromJson(entry as Map, DateTime.fromMillisecondsSinceEpoch(int.parse(entry["id"].substring(24), radix: 16)), entry["season"], entry["id"].substring(0, 24))]; - return result; - } - /// Remove state (which is [tetrioPlayer]) from the local database // Future deleteState(TetrioPlayer tetrioPlayer) async { // await ensureDbIsOpen(); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 078a2fb..9e52818 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -40,6 +40,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; int _chartsIndex = 0; +int _season = currentSeason-1; bool _gamesPlayedInsteadOfDateAndTime = false; late ZoomPanBehavior _zoomPanBehavior; bool _smooth = false; @@ -73,7 +74,8 @@ class _MainState extends State with TickerProviderStateMixin { String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for String _titleNickname = ""; /// Each dropdown menu item contains list of dots for the graph - List>> chartsData = []; + /// chartsData[season-1][chart] + List>>> chartsData = []; //var tableData = []; final bodyGlobalKey = GlobalKey(); bool _showSearchBar = false; @@ -217,9 +219,9 @@ class _MainState extends State with TickerProviderStateMixin { // Making list of Tetra League matches bool isTracking = await teto.isPlayerTracking(me.userId); - List states = await teto.getHistory(me.userId); - TetraLeague? compareWith; - Set uniqueTL = {}; + List> states = await Future.wait>([ + teto.getStates(me.userId, season: 1), teto.getStates(me.userId, season: 2), + ]); List storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches if (isTracking){ // if tracked - save data to local DB await teto.storeState(summaries.league); @@ -277,31 +279,32 @@ class _MainState extends State with TickerProviderStateMixin { // if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); // } // Also i need previous Tetra League State for comparison if avaliable - if (states.length >= 2){ - compareWith = states.elementAtOrNull(states.length - 2); - chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), - ]; + TetraLeague? compareWith; + if (states[1].length >= 2 || states[0].length >= 2){ + compareWith = states[1].length >= 2 ? states[1].elementAtOrNull(states.length - 2) : null; + chartsData = [for (List s in states) >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid + DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), + DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), + DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), + DropdownMenuItem(value: [for (var tl in s) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), + DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), + ]]; }else{ compareWith = null; chartsData = []; @@ -312,7 +315,7 @@ class _MainState extends State with TickerProviderStateMixin { changePlayer(me.userId); }); } - return [me, summaries, news, tlStream, recentZenith, recentZenithEX, states]; + return [me, summaries, news, tlStream, recentZenith, recentZenithEX, states[currentSeason-1]]; //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; } @@ -850,7 +853,7 @@ class _ZenithRecords extends StatelessWidget { } class _History extends StatelessWidget{ - final List>> chartsData; + final List>>> chartsData; final String userID; final Function update; final Function changePlayer; @@ -873,7 +876,7 @@ class _History extends StatelessWidget{ )); } bool bigScreen = MediaQuery.of(context).size.width > 768; - List<_HistoryChartSpot> selectedGraph = chartsData[_chartsIndex].value!; + List<_HistoryChartSpot> selectedGraph = chartsData[_season][_chartsIndex].value!; return SingleChildScrollView( scrollDirection: Axis.vertical, child: Column( @@ -883,6 +886,20 @@ class _History extends StatelessWidget{ spacing: 20, crossAxisAlignment: WrapCrossAlignment.center, children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], + value: _season, + onChanged: (value) { + _season = value!; + update(); + } + ), + ], + ), Row( mainAxisSize: MainAxisSize.min, children: [ @@ -902,10 +919,10 @@ class _History extends StatelessWidget{ children: [ const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), DropdownButton( - items: chartsData, - value: chartsData[_chartsIndex].value, + items: chartsData[_season], + value: chartsData[_season][_chartsIndex].value, onChanged: (value) { - _chartsIndex = chartsData.indexWhere((element) => element.value == value); + _chartsIndex = chartsData[_season].indexWhere((element) => element.value == value); update(); } ), @@ -926,13 +943,13 @@ class _History extends StatelessWidget{ IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) ], ), - if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: selectedGraph, smooth: _smooth, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact()) - else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column( + if(chartsData[_season][_chartsIndex].value!.length > 1) _HistoryChartThigy(data: selectedGraph, smooth: _smooth, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact()) + else if (chartsData[_season][_chartsIndex].value!.length <= 1) Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) + if (wasActiveInTL && _season == 0) Text(t.errors.actionSuggestion), + if (wasActiveInTL && _season == 0) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) ], )) ], diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart index f5a0f2c..af7b32f 100644 --- a/lib/views/states_view.dart +++ b/lib/views/states_view.dart @@ -11,7 +11,7 @@ import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:window_manager/window_manager.dart'; class StatesView extends StatefulWidget { - final List states; + final List states; const StatesView({super.key, required this.states}); @override @@ -25,7 +25,7 @@ class StatesState extends State { void initState() { if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.username.toUpperCase())}"); + windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}"); } super.initState(); } @@ -41,14 +41,14 @@ class StatesState extends State { final t = Translations.of(context); return Scaffold( appBar: AppBar( - title: Text(t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.username.toUpperCase())), + title: Text(t.statesViewTitle(number: widget.states.length, nickname: widget.states.first.id)), actions: [ IconButton( onPressed: (){ Navigator.push( context, MaterialPageRoute( - builder: (context) => MatchesView(userID: widget.states.first.userId, username: widget.states.first.username), + builder: (context) => MatchesView(userID: widget.states.first.id, username: widget.states.first.id), ), ); }, icon: const Icon(Icons.list), tooltip: t.viewAllMatches) @@ -60,24 +60,24 @@ class StatesState extends State { itemCount: widget.states.length, 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: 0)), + title: Text(timestamp(widget.states[index].timestamp)), + //subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: 0)), trailing: IconButton( icon: const Icon(Icons.delete_forever), onPressed: () { - DateTime nn = widget.states[index].state; + //DateTime nn = widget.states[index].state; // teto.deleteState(widget.states[index]).then((value) => setState(() { // ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn))))); // })); }, ), onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StateView(state: widget.states[index]), - ), - ); + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => StateView(state: widget.states[index]), + // ), + // ); }, ); }))); diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart index 905e3f4..ef4cc6c 100644 --- a/lib/views/tracked_players_view.dart +++ b/lib/views/tracked_players_view.dart @@ -119,12 +119,12 @@ class TrackedPlayersState extends State { }, ), onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StatesView(states: allPlayers[keys[index]]!), - ), - ); + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => StatesView(states: allPlayers[keys[index]]!), + // ), + // ); }, ); })); From 1b5b9d7a3ff6c4db5efca79e4d85cd457787ab9a Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 5 Sep 2024 00:12:26 +0300 Subject: [PATCH 32/33] releasing this, then fully focusing on redesign --- lib/gen/strings.g.dart | 24 ++----- lib/services/tetrio_crud.dart | 60 ++++------------ lib/views/state_view.dart | 26 ++++--- lib/views/states_view.dart | 102 ++++++++++++++++++---------- lib/views/tracked_players_view.dart | 46 ++++++------- pubspec.yaml | 2 +- res/i18n/strings.i18n.json | 5 +- res/i18n/strings_ru.i18n.json | 5 +- 8 files changed, 125 insertions(+), 145 deletions(-) diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 4d8805c..a8b8209 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: 1216 (608 per locale) +/// Strings: 1210 (605 per locale) /// -/// Built on 2024-08-07 at 15:58 UTC +/// Built on 2024-09-04 at 20:41 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -224,9 +224,6 @@ class Translations implements BaseTranslations { 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 with hidden glicko (200 RD 🙂).\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'; @@ -293,7 +290,7 @@ class Translations implements BaseTranslations { String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}'; String statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; String matchesViewTitle({required Object nickname}) => '${nickname} TL matches'; - String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD'; + String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; String stateRemoved({required Object date}) => '${date} state was removed from database!'; String matchRemoved({required Object date}) => '${date} match was removed from database!'; String get viewAllMatches => 'View all matches'; @@ -938,9 +935,6 @@ class _StringsRu implements Translations { @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}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона'; @override String get nanow => 'Пока недоступно...'; @override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}'; @override String get seasonEnded => 'Сезон закончился'; @@ -1007,7 +1001,7 @@ class _StringsRu implements Translations { @override String stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; @override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; @override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${nickname}'; - @override String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD'; + @override String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; @override String stateRemoved({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!'; @override String matchRemoved({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; @override String get viewAllMatches => 'Все матчи'; @@ -1644,9 +1638,6 @@ extension on Translations { 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 with hidden glicko (200 RD 🙂).\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'; @@ -1713,7 +1704,7 @@ extension on Translations { case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} account on ${date}'; case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches'; - case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD'; + case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; case 'stateRemoved': return ({required Object date}) => '${date} state was removed from database!'; case 'matchRemoved': return ({required Object date}) => '${date} match was removed from database!'; case 'viewAllMatches': return 'View all matches'; @@ -2274,9 +2265,6 @@ extension on _StringsRu { 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}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона'; case 'nanow': return 'Пока недоступно...'; case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}'; case 'seasonEnded': return 'Сезон закончился'; @@ -2343,7 +2331,7 @@ extension on _StringsRu { case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${nickname}'; - case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD'; + case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; case 'stateRemoved': return ({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!'; case 'matchRemoved': return ({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; case 'viewAllMatches': return 'Все матчи'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 8308dcb..102dc89 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -213,6 +213,7 @@ class TetrioService extends DB { _players.removeWhere((key, value) => key == id); _tetrioStreamController.add(_players); } + await db.delete(tetrioLeagueTable, where: "id LIKE ?", whereArgs: ["$id%"]); } /// Gets nickname from database or requests it from API if missing. @@ -1117,46 +1118,14 @@ class TetrioService extends DB { } } - /// Remove state (which is [tetrioPlayer]) from the local database - // Future deleteState(TetrioPlayer tetrioPlayer) async { - // await ensureDbIsOpen(); - // final db = getDatabaseOrThrow(); - // //List states = await getPlayer(tetrioPlayer.userId); - // // removing state from map that contain every state of each user - // states.removeWhere((element) => element.state == tetrioPlayer.state); - - // // Making map of the states (without deleted one) - // final Map statesJson = {}; - // // for (var e in states) { - // // statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); - // // } - // // Rewriting database entry with new json - // await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, - // where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); - // _tetrioStreamController.add(_players); - // } - - /// Returns list of all states of player with given [id] from database. Can return empty list if player - /// was not found. - // Future> getPlayer(String id) async { - // await ensureDbIsOpen(); - // final db = getDatabaseOrThrow(); - // List states = []; - // final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); - // if (results.isEmpty) { - // return states; // it empty - // } else { - // dynamic rawStates = results.first['jsonStates'] as String; - // rawStates = json.decode(rawStates); - // // recreating objects of states - // rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String))); - // // updating the stream - // _players.removeWhere((key, value) => key == id); - // _players.addEntries({states.last.userId: states.last.username}.entries); - // _tetrioStreamController.add(_players); - // return states; - // } - // } + /// Remove state, which has [dbID] from the local database + /// ([dbid] is a concatenation of player id and UINX milliseconds in hex) + Future deleteState(String dbID) async { + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + int result = await db.delete(tetrioLeagueTable, where: "id = ?", whereArgs: [dbID]); + if (result == 0) throw Exception("Failed to remove a row $dbID - it's probably not exist"); + } /// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user. /// If [isItDiscordID] is true, function expects [user] to be a discord user id. Throws an exception if fails to retrieve. @@ -1256,17 +1225,14 @@ class TetrioService extends DB { } } - /// Retrieves whole [tetrioUsersTable] and returns Map with [TetrioPlayer] objects of everyone in database - Future>> getAllPlayers() async { + /// Retrieves whole [tetrioUsersTable] and returns Map {id: nickname} of everyone in database + Future> getAllPlayers() async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersTable); - Map> data = {}; + Map data = {}; for (var entry in players){ - var test = json.decode(entry['jsonStates'] as String); - List states = []; - test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), entry[idCol] as String, entry[nickCol] as String))); - data.addEntries({states.last.userId: states}.entries); + data[entry[idCol] as String] = entry[nickCol] as String; } return data; } diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart index 9be1ccf..b97f141 100644 --- a/lib/views/state_view.dart +++ b/lib/views/state_view.dart @@ -4,14 +4,15 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -//import 'package:tetra_stats/widgets/tl_thingy.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:tetra_stats/widgets/tl_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:window_manager/window_manager.dart'; final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); class StateView extends StatefulWidget { - final TetrioPlayer state; + final TetraLeague state; const StateView({super.key, required this.state}); @override @@ -28,7 +29,7 @@ class StateState extends State { _scrollController = ScrollController(); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.stateViewTitle(nickname: widget.state.username.toUpperCase(), date: dateFormat.format(widget.state.state))}"); + windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}"); } super.initState(); } @@ -48,16 +49,13 @@ class StateState extends State { Widget build(BuildContext context) { final t = Translations.of(context); return Scaffold( - appBar: AppBar( - title: Text(t.stateViewTitle(nickname: widget.state.username.toUpperCase(), date: dateFormat.format(widget.state.state))), - ), - backgroundColor: Colors.black, - body: SafeArea( - child: NestedScrollView( - controller: _scrollController, - headerSliverBuilder: (context, value) { - return [SliverToBoxAdapter(child: UserThingy(player: widget.state, showStateTimestamp: true, setState: _justUpdate))]; - }, - body: Container()))); + appBar: AppBar( + title: Text("State from ${timestamp(widget.state.timestamp)}"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: TLThingy(tl: widget.state, userID: widget.state.id, states: []) + ) + ); } } diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart index af7b32f..bf0fc5f 100644 --- a/lib/views/states_view.dart +++ b/lib/views/states_view.dart @@ -1,18 +1,19 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show teto; +import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/views/mathes_view.dart'; import 'package:tetra_stats/views/state_view.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:window_manager/window_manager.dart'; class StatesView extends StatefulWidget { - final List states; - const StatesView({super.key, required this.states}); + final String nickname; + final String id; + const StatesView({required this.nickname, required this.id, super.key}); @override State createState() => StatesState(); @@ -25,7 +26,7 @@ class StatesState extends State { void initState() { if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}"); + //windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}"); } super.initState(); } @@ -41,45 +42,78 @@ class StatesState extends State { final t = Translations.of(context); return Scaffold( appBar: AppBar( - title: Text(t.statesViewTitle(number: widget.states.length, nickname: widget.states.first.id)), + title: Text(t.statesViewTitle(number: "", nickname: widget.nickname)), actions: [ IconButton( onPressed: (){ Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MatchesView(userID: widget.states.first.id, username: widget.states.first.id), - ), - ); + context, + MaterialPageRoute( + builder: (context) => MatchesView(userID: widget.id, username: widget.nickname), + ), + ); }, icon: const Icon(Icons.list), tooltip: t.viewAllMatches) ], ), backgroundColor: Colors.black, body: SafeArea( - child: ListView.builder( - itemCount: widget.states.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(timestamp(widget.states[index].timestamp)), - //subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: 0)), - trailing: IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: () { - //DateTime nn = widget.states[index].state; - // teto.deleteState(widget.states[index]).then((value) => setState(() { - // ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn))))); - // })); - }, + child: FutureBuilder>(future: teto.getStates(widget.id), builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator(color: Colors.white)); + case ConnectionState.done: + if (snapshot.hasData) { + return ListView.builder( + itemCount: snapshot.data!.length, + prototypeItem: ListTile( + title: Text(""), + subtitle: Text("", style: TextStyle(color: Colors.grey)), + trailing: IconButton(icon: const Icon(Icons.delete_forever), onPressed: (){}), ), - onTap: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => StateView(state: widget.states[index]), - // ), - // ); - }, + itemBuilder: (context, index) { + return ListTile( + title: Text(timestamp(snapshot.data![index].timestamp)), + subtitle: Text( + t.statesViewEntry(level: f2.format(snapshot.data![index].tr), games: intf.format(snapshot.data![index].gamesPlayed), glicko: snapshot.data![index].glicko != null ? f2.format(snapshot.data![index].glicko) : "---", rd: snapshot.data![index].rd != null ? f2.format(snapshot.data![index].rd) : "--"), + style: TextStyle(color: Colors.grey), + ), + trailing: IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () { + teto.deleteState(snapshot.data![index].id+snapshot.data![index].timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(snapshot.data![index].timestamp))))); + })); + }, + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StateView(state: snapshot.data![index]), + ), + ); + }, + ); + }); + } else if (snapshot.hasError) { + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), + ), + ], + ) ); - }))); + } + break; + } + return const Center(child: Text('default case of FutureBuilder', style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center)); + } + )));} } -} diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart index ef4cc6c..721bce4 100644 --- a/lib/views/tracked_players_view.dart +++ b/lib/views/tracked_players_view.dart @@ -78,7 +78,7 @@ class TrackedPlayersState extends State { case ConnectionState.active: return const Center(child: CircularProgressIndicator(color: Colors.white)); case ConnectionState.done: - final allPlayers = (snapshot.data != null) ? snapshot.data as Map> : >{}; + final allPlayers = (snapshot.data != null) ? snapshot.data as Map : {}; List keys = allPlayers.keys.toList(); return NestedScrollView( headerSliverBuilder: (context, value) { @@ -105,29 +105,29 @@ class TrackedPlayersState extends State { ]; }, body: ListView.builder( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(t.trackedPlayersEntry(nickname: allPlayers[keys[index]]!.last.username, numberOfStates: allPlayers[keys[index]]!.length)), - subtitle: Text(t.trackedPlayersDescription(firstStateDate: timestamp(allPlayers[keys[index]]!.first.state), lastStateDate: timestamp(allPlayers[keys[index]]!.last.state))), - trailing: IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: () { - String nn = allPlayers[keys[index]]!.last.username; - setState(() {teto.deletePlayer(keys[index]);}); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: nn)))); - }, - ), - onTap: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => StatesView(states: allPlayers[keys[index]]!), - // ), - // ); + itemCount: allPlayers.length, + itemBuilder: (context, index) { + print(index); + return ListTile( + title: Text(allPlayers[keys[index]]??"No nickname (huh?)"), + subtitle: Text(keys[index], style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), + trailing: IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () { + setState(() {teto.deletePlayer(keys[index]);}); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: allPlayers[keys[index]]??"No nickname (huh?)")))); }, - ); - })); + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StatesView(nickname: allPlayers[keys[index]]!, id: keys[index]), + ), + ); + }, + ); + })); } })), ); diff --git a/pubspec.yaml b/pubspec.yaml index 4ac97d4..5b7c6ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.8+34 +version: 1.6.9+35 environment: sdk: '>=3.0.0' diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 789e25e..1f3ba9a 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -89,9 +89,6 @@ "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 with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied", "nanow": "Not avaliable for now...", "seasonEnds": "Season ends in ${countdown}", "seasonEnded": "Season has ended", @@ -158,7 +155,7 @@ "stateViewTitle": "${nickname} account on ${date}", "statesViewTitle": "${number} states of ${nickname} account", "matchesViewTitle": "${nickname} TL matches", - "statesViewEntry": "Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD", + "statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно", "stateRemoved": "${date} state was removed from database!", "matchRemoved": "${date} match was removed from database!", "viewAllMatches": "View all matches", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index f8dbbc9..5f48489 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -89,9 +89,6 @@ "smooth": "Гладкий", "postSeason": "Внесезонье", "seasonStarts": "Сезон начнётся через:", - "myMessadgeHeader": "Сообщение от dan63", - "myMessadgeBody": "TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.", - "preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона", "nanow": "Пока недоступно...", "seasonEnds": "Сезон закончится через ${countdown}", "seasonEnded": "Сезон закончился", @@ -158,7 +155,7 @@ "stateViewTitle": "Аккаунт ${nickname} ${date}", "statesViewTitle": "${number} состояний аккаунта ${nickname}", "matchesViewTitle": "Матчи аккаунта ${nickname}", - "statesViewEntry": "${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD", + "statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно", "stateRemoved": "Состояние от ${date} было удалено из локальной базы данных!", "matchRemoved": "Матч от ${date} был удален из локальной базы данных!", "viewAllMatches": "Все матчи", From c03ea447824f727757024dd9911229527f079689 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Thu, 5 Sep 2024 00:39:07 +0300 Subject: [PATCH 33/33] forgor to add one small thing --- .github/workflows/main.yml | 2 +- lib/widgets/zenith_thingy.dart | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 889b0b9..29964e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,8 +45,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: subosito/flutter-action@v1 - uses: ashutoshvarma/setup-ninja@master + - uses: subosito/flutter-action@v1 with: channel: 'stable' flutter-version: '3.22.3' diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index b8c8535..a5a43f3 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -124,8 +124,9 @@ class _ZenithThingyState extends State { StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "CPS\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) + StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "KO's", isScreenBig: bigScreen, higherIsBetter: true), + StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "Climb speed\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true), + StatCellNum(playerStat: record!.stats.topBtB, playerStatLabel: "Top B2B\nchain", isScreenBig: bigScreen, higherIsBetter: true) ], ), FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage),