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": "Экспорт на десктопе",