diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f22269d..29964e1 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 @@ -40,51 +40,28 @@ 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: ashutoshvarma/setup-ninja@master - 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 + channel: 'stable' + flutter-version: '3.22.3' + - name: Install project dependencies + 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 + uses: thedoctor0/zip-release@master + with: + type: 'zip' + filename: TetraStats-${{github.ref_name}}-linux.zip + directory: build/linux/x64/release/bundle - name: Push to Releases uses: ncipollo/release-action@v1 with: @@ -92,7 +69,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/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/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 45ab41d..c2b02c2 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.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; @@ -18,9 +19,10 @@ 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, "x": 0.01, "u": 0.05, "ss": 0.11, @@ -42,28 +44,32 @@ const Map rankCutoffs = { "": 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, + "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 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, + gxe, + s1tr, rd, gp, gw, @@ -93,6 +99,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", @@ -121,7 +129,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), @@ -142,48 +151,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), @@ -261,9 +228,6 @@ class TetrioPlayer { bool? badstanding; String? botmaster; Connections? connections; - late TetraLeagueAlpha tlSeason1; - List sprint = []; - List blitz = []; TetrioZen? zen; Distinguishment? distinguishment; DateTime? cachedUntil; @@ -289,9 +253,6 @@ class TetrioPlayer { this.badstanding, this.botmaster, required this.connections, - required this.tlSeason1, - required this.sprint, - required this.blitz, this.zen, this.distinguishment, this.cachedUntil @@ -305,7 +266,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)); @@ -318,7 +279,6 @@ class TetrioPlayer { country = json['country']; supporterTier = json['supporter_tier'] ?? 0; verified = json['verified'] ?? false; - tlSeason1 = TetraLeagueAlpha.fromJson(json['league'], stateTime); avatarRevision = json['avatar_revision']; bannerRevision = json['banner_revision']; bio = json['bio']; @@ -344,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; @@ -374,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); - } - - 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); - @override String toString() { return "$username ($state)"; } - num? getStatByEnum(Stats stat){ - switch (stat) { - case Stats.tr: - return tlSeason1.rating; - case Stats.glicko: - return tlSeason1.glicko; - 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; @@ -458,6 +349,34 @@ class TetrioPlayer { bool operator ==(covariant TetrioPlayer other) => isSameState(other) && state.isAtSameMomentAs(other.state); } +class Summaries{ + late String id; + 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; + + Summaries(this.id, this.league, this.zen); + + Summaries.fromJson(Map json, String i){ + id = i; + 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(), currentSeason, i); + zen = TetrioZen.fromJson(json['zen']); + } +} + class Badge { late String badgeId; late String label; @@ -527,6 +446,8 @@ class Clears { late int tSpinMiniZeros; late int tSpinMiniSingles; late int tSpinMiniDoubles; + late int tSpinMiniTriples; + late int tSpinMiniQuads; Clears( {required this.singles, @@ -543,7 +464,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']; @@ -557,7 +480,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']; @@ -579,7 +504,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 ); } @@ -652,8 +579,7 @@ class Finesse { } } -class EndContextSingle { - late String gameType; +class ResultsStats { late int topBtB; late int topCombo; late int holds; @@ -662,20 +588,23 @@ class EndContextSingle { late int piecesPlaced; late int lines; late int score; - late double seed; + double? 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; 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; - EndContextSingle( - {required this.gameType, + ResultsStats( + { required this.topBtB, required this.topCombo, required this.holds, @@ -690,12 +619,12 @@ class EndContextSingle { required this.clears, required this.finesse}); - EndContextSingle.fromJson(Map json) { - seed = json['seed'].toDouble(); + ResultsStats.fromJson(Map json) { + seed = json['seed']?.toDouble(); 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']; @@ -703,8 +632,9 @@ class EndContextSingle { tSpins = json['tspins']; piecesPlaced = json['piecesplaced']; clears = Clears.fromJson(json['clears']); - finesse = json.containsKey("finesse") ? Finesse.fromJson(json['finesse']) : null; - gameType = json['gametype']; + kills = json['kills']; + if (json.containsKey("finesse")) finesse = Finesse.fromJson(json['finesse']); + if (json.containsKey("zenith")) zenith = ZenithResults.fromJson(json['zenith']); } Map toJson() { @@ -722,11 +652,45 @@ class EndContextSingle { data['clears'] = clears.toJson(); if (finesse != null) data['finesse'] = finesse!.toJson(); data['finalTime'] = finalTime; - data['gametype'] = gameType; return data; } } +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; @@ -828,6 +792,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; @@ -873,6 +935,109 @@ 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(List r){ + for (var entry in r) { + 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 + ) + ), + 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 + ) + ) + ], + 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}){ + 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'] != 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']; + 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']); } } @@ -1015,13 +1300,15 @@ class EndContextMulti { } } -class TetraLeagueAlpha { +class TetraLeague { + late String id; 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; @@ -1039,15 +1326,17 @@ class TetraLeagueAlpha { NerdStats? nerdStats; EstTr? estTr; Playstyle? playstyle; - List? records; + late int season; - TetraLeagueAlpha( - {required this.timestamp, + TetraLeague( + {required this.id, + 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, @@ -1062,27 +1351,35 @@ class TetraLeagueAlpha { 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; } double get winrate => gamesWon / gamesPlayed; + double get s1tr => gxe * 250; - TetraLeagueAlpha.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; - rating = json['rating'] != null ? json['rating'].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'] != 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(); 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; @@ -1097,61 +1394,81 @@ 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; + + TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) => TetrioPlayerFromLeaderboard( + id, "", "user", -1, null, timestamp, gamesPlayed, gamesWon, + tr, gxe, glicko??0, rd??noTrRd, rank, bestRank, apm??0, pps??0, vs??0, decaying); Map toJson() { final Map data = {}; + data['id'] = id+timestamp.millisecondsSinceEpoch.toRadixString(16); 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 (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; } } class RecordSingle { - late String userId; + late String? userId; late String replayId; late String ownId; + late String gamemode; late DateTime timestamp; - late EndContextSingle endContext; - int? rank; + late ResultsStats stats; + 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.endContext, 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) { - //developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio"); + RecordSingle.fromJson(Map json, int ran, int cran) { 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']; + 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() { final Map data = {}; data['_id'] = ownId; - data['endcontext'] = endContext.toJson(); + data['results']['stats'] = stats.toJson(); data['ismulti'] = false; data['replayid'] = replayId; data['ts'] = timestamp; @@ -1160,12 +1477,52 @@ 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 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; 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']; @@ -1318,6 +1675,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){ @@ -1348,6 +1710,7 @@ class TetrioPlayersLeaderboard { avgPPS = 0, avgVS = 0, avgTR = 0, + avgGlixare = 0, avgGlicko = 0, avgRD = 0, avgAPP = 0, @@ -1366,6 +1729,7 @@ class TetrioPlayersLeaderboard { avgStride = 0, avgInfDS = 0, lowestTR = 25000, + lowestGlixare = double.infinity, lowestGlicko = double.infinity, lowestRD = double.infinity, lowestWinrate = double.infinity, @@ -1388,6 +1752,7 @@ class TetrioPlayersLeaderboard { lowestStride = double.infinity, lowestInfDS = double.infinity, highestTR = double.negativeInfinity, + highestGlixare = double.negativeInfinity, highestGlicko = double.negativeInfinity, highestRD = double.negativeInfinity, highestWinrate = double.negativeInfinity, @@ -1418,6 +1783,7 @@ class TetrioPlayersLeaderboard { highestGamesPlayed = 0, highestGamesWon = 0; String lowestTRid = "", lowestTRnick = "", + lowestGlixareID = "", lowestGlixareNick = "", lowestGlickoID = "", lowestGlickoNick = "", lowestRdID = "", lowestRdNick = "", lowestGamesPlayedID = "", lowestGamesPlayedNick = "", @@ -1442,6 +1808,7 @@ class TetrioPlayersLeaderboard { lowestStrideID = "", lowestStrideNick = "", lowestInfDSid = "", lowestInfDSnick = "", highestTRid = "", highestTRnick = "", + highestGlixareID = "", highestGlixareNick = "", highestGlickoID = "", highestGlickoNick = "", highestRdID = "", highestRdNick = "", highestGamesPlayedID = "", highestGamesPlayedNick = "", @@ -1469,9 +1836,10 @@ class TetrioPlayersLeaderboard { avgAPM += entry.apm; avgPPS += entry.pps; avgVS += entry.vs; - avgTR += entry.rating; - avgGlicko += entry.glicko; - avgRD += entry.rd; + avgTR += entry.tr; + avgGlixare += entry.gxe; + 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; @@ -1489,18 +1857,23 @@ 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; } - if (entry.glicko < lowestGlicko){ - lowestGlicko = entry.glicko; + 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; 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; } @@ -1609,18 +1982,23 @@ 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; } - if (entry.glicko > highestGlicko){ - highestGlicko = entry.glicko; + 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; 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; } @@ -1734,6 +2112,7 @@ class TetrioPlayersLeaderboard { avgPPS /= filtredLeaderboard.length; avgVS /= filtredLeaderboard.length; avgTR /= filtredLeaderboard.length; + avgGlixare /= filtredLeaderboard.length; avgGlicko /= filtredLeaderboard.length; avgRD /= filtredLeaderboard.length; avgAPP /= filtredLeaderboard.length; @@ -1753,7 +2132,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(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, @@ -1762,6 +2141,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, @@ -1834,6 +2219,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, @@ -1918,31 +2309,31 @@ class TetrioPlayersLeaderboard { "avgPlonk": avgPlonk, "avgStride": avgStride, "avgInfDS": avgInfDS, - "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].rating : lowestTR, + "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{ - 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(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}]; } } - 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 { @@ -1953,6 +2344,7 @@ class TetrioPlayersLeaderboard { } Map> get averages => { + 'x+': getAverageOfRank("x+"), 'x': getAverageOfRank("x"), 'u': getAverageOfRank("u"), 'ss': getAverageOfRank("ss"), @@ -2021,6 +2413,10 @@ class TetrioPlayersLeaderboard { leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts)); } } + + addPlayers(List list){ + leaderboard.addAll(list); + } } class TetrioPlayerFromLeaderboard { @@ -2029,16 +2425,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 tr; + late double gxe; + 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,12 +2448,11 @@ class TetrioPlayerFromLeaderboard { this.role, this.xp, this.country, - this.supporter, - this.verified, this.timestamp, this.gamesPlayed, this.gamesWon, - this.rating, + this.tr, + this.gxe, this.glicko, this.rd, this.rank, @@ -2073,7 +2467,8 @@ class TetrioPlayerFromLeaderboard { } double get winrate => gamesWon / gamesPlayed; - double get esttracc => estTr.esttr - rating; + double get esttracc => estTr.esttr - tr; + double get s1tr => gxe * 250; TetrioPlayerFromLeaderboard.fromJson(Map json, DateTime ts) { userId = json['_id']; @@ -2081,14 +2476,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; + 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']; bestRank = json['league']['bestrank']; apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00; @@ -2103,11 +2497,15 @@ class TetrioPlayerFromLeaderboard { num getStatByEnum(Stats stat){ switch (stat) { case Stats.tr: - return rating; + return tr; case Stats.glicko: - return glicko; + return glicko??-1; + case Stats.gxe: + return gxe; + case Stats.s1tr: + return s1tr; case Stats.rd: - return rd; + return rd??-1; case Stats.gp: return gamesPlayed; case Stats.gw: @@ -2159,3 +2557,44 @@ 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 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"); + 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/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 9b521ad..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); } @@ -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/gen/strings.g.dart b/lib/gen/strings.g.dart index e3b9fb2..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: 1186 (593 per locale) +/// Strings: 1210 (605 per locale) /// -/// Built on 2024-07-20 at 13:24 UTC +/// Built on 2024-09-04 at 20:41 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -222,6 +222,9 @@ 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 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'; @@ -236,6 +239,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'; @@ -276,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'; @@ -703,7 +717,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'; @@ -919,6 +933,9 @@ 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 nanow => 'Пока недоступно...'; @override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}'; @override String get seasonEnded => 'Сезон закончился'; @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; @@ -933,6 +950,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 => 'Экспорт на десктопе'; @@ -973,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 => 'Все матчи'; @@ -1608,6 +1636,9 @@ 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 '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'; @@ -1622,6 +1653,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'; @@ -1662,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'; @@ -1773,7 +1815,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'; @@ -2221,6 +2263,9 @@ extension on _StringsRu { case 'verdictBetter': return 'Лучше'; case 'verdictWorse': return 'Хуже'; case 'smooth': return 'Гладкий'; + case 'postSeason': return 'Внесезонье'; + case 'seasonStarts': return 'Сезон начнётся через:'; + case 'nanow': return 'Пока недоступно...'; case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}'; case 'seasonEnded': return 'Сезон закончился'; case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; @@ -2235,6 +2280,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 'Экспорт на десктопе'; @@ -2275,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/main.dart b/lib/main.dart index 162f783..bbba219 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,39 +25,33 @@ 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: Color(0xFF00838F), + ), + 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) + ), + chipTheme: const ChipThemeData( + side: BorderSide(color: Colors.transparent), + ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide(color: Colors.transparent)), + surfaceTintColor: const WidgetStatePropertyAll(Colors.cyanAccent), + iconColor: const WidgetStatePropertyAll(Colors.cyanAccent), + shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200), + ) + ), + scaffoldBackgroundColor: Colors.black +); final router = GoRouter( initialLocation: "/", @@ -189,4 +183,4 @@ class MyAppState extends State { theme: theme ); } -} +} \ No newline at end of file diff --git a/lib/services/custom_http_client.dart b/lib/services/custom_http_client.dart index 004f2aa..c244a4f 100644 --- a/lib/services/custom_http_client.dart +++ b/lib/services/custom_http_client.dart @@ -1,3 +1,6 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; class UserAgentClient extends http.BaseClient { @@ -9,6 +12,7 @@ class UserAgentClient extends http.BaseClient { @override Future send(http.BaseRequest request) { request.headers['user-agent'] = userAgent; + 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/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 f821c73..102dc89 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -4,8 +4,9 @@ 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: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; @@ -22,6 +23,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"; @@ -68,6 +70,33 @@ const String createTetrioTLReplayStats = ''' PRIMARY KEY("id") ) '''; +const String createTetrioLeagueTable = ''' + CREATE TABLE IF NOT EXISTS "tetrioLeague" ( + "id" TEXT 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, + PRIMARY KEY("id") + ) +'''; class CacheController { late Map _cache; @@ -90,8 +119,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 +126,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 +140,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; @@ -184,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. @@ -309,15 +339,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 +355,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; @@ -408,15 +438,15 @@ 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 { - Cutoffs? cached = _cache.get("", Cutoffs); + Future fetchCutoffsTetrio() async { + CutoffsTetrio? cached = _cache.get("league_ranks", CutoffsTetrio); if (cached != null) return cached; Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLCutoffs"}); + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "cutoffs"}); } else { - url = Uri.https('api.p1nkl0bst3r.xyz', 'rankcutoff', {"users": null}); + url = Uri.https('ch.tetr.io', 'api/labs/league_ranks'); } try{ @@ -425,16 +455,58 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: Map rawData = jsonDecode(response.body); - Map data = rawData["cutoffs"] as Map; - Cutoffs result = Cutoffs({}, {}); - for (String rank in data.keys){ - result.tr[rank] = data[rank]["rating"]; - result.glicko[rank] = data[rank]["glicko"]; - } - _cache.store(result, rawData["ts"] + 300000); + CutoffsTetrio result = CutoffsTetrio.fromJson(rawData['data']); + _cache.store(result, rawData["cache"]["cached_until"]); return result; case 404: - developer.log("fetchCutoffs: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); + 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; + + Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/cutoffs.json'); + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + Map rawData = jsonDecode(response.body); + Map data = rawData["data"] as Map; + Cutoffs result = Cutoffs(DateTime.fromMillisecondsSinceEpoch(rawData["created"]), {}, {}, {}); + for (String rank in data.keys){ + result.tr[rank] = data[rank]["tr"]; + result.glicko[rank] = data[rank]["glicko"]; + result.gxe[rank] = data[rank]["gxe"]; + } + _cache.store(result, rawData["cache_until"]); + return result; + case 404: + 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: @@ -447,10 +519,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 @@ -467,7 +539,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{ @@ -476,7 +548,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: @@ -505,7 +577,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}); @@ -517,27 +589,15 @@ 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 = []; + Batch batch = db.batch(); 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: TetraLeagueAlpha( + TetraLeague state = TetraLeague( + id: id, timestamp: DateTime.parse(entry[9]), apm: entry[6] != '' ? entry[6] : null, pps: entry[7] != '' ? entry[7] : null, @@ -548,33 +608,21 @@ 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]]!, standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1 - ), - sprint: [], - blitz: [] + prevAt: -1, + season: 1 ); history.add(state); + batch.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]); + batch.commit(); return history; case 404: developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); @@ -601,7 +649,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 +664,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(); @@ -645,13 +693,9 @@ class TetrioService extends DB { 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/lists/league/all'); - } + + Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/leaderboard.json'); + try{ final response = await client.get(url); @@ -659,16 +703,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']['users'], "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: @@ -690,6 +728,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); @@ -711,7 +763,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; @@ -758,15 +810,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, TetraLeagueBetaStream); 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 +826,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; @@ -906,10 +958,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); @@ -941,6 +993,52 @@ 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"); + 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(); + } + 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(); @@ -967,7 +1065,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]. @@ -991,6 +1092,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 { @@ -999,71 +1101,30 @@ 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: 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))]; } - /// Remove state (which is [tetrioPlayer]) from the local database - Future deleteState(TetrioPlayer tetrioPlayer) async { + /// Saves state (which is [TetraLeague]) to the local database. + Future storeState(TetraLeague league) 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); + 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()); } - // 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 { + /// 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(); - 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; - } + 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. @@ -1097,6 +1158,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: @@ -1131,7 +1194,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; @@ -1141,6 +1204,8 @@ class TetrioService extends DB { } case 403: throw TetrioForbidden(); + case 404: + throw TetrioPlayerNotExist(); case 429: throw TetrioTooManyRequests(); case 418: @@ -1160,29 +1225,26 @@ 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; } - 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..5176221 100644 --- a/lib/utils/relative_timestamps.dart +++ b/lib/utils/relative_timestamps.dart @@ -1,9 +1,13 @@ +// 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'; final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); -final NumberFormat nonsecs = 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); /// Returns string, that represents time difference between [dateTime] and now @@ -69,6 +73,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; @@ -77,5 +85,12 @@ 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/compare_view.dart b/lib/views/compare_view.dart index 2916a0e..333df80 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>?, TetraLeagueAlpha? +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,8 @@ class CompareState extends State { double vs = double.parse(threeNumbers[2][0]!); theRedSide = [null, null, - TetraLeagueAlpha( + Summaries(user, TetraLeague( + id: "", timestamp: DateTime.now(), apm: apm, pps: pps, @@ -92,46 +95,49 @@ class CompareState extends State { gamesWon: -1, bestRank: "z", decaying: true, - rating: -1, + tr: -1, + gxe: -1, rank: "z", percentileRank: "z", percentile: 1, standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1) - ]; + prevAt: -1, season: currentSeason), 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)))); } _justUpdate(); } - void changeRedSide(TetrioPlayer user) { - setState(() {theRedSide[0] = user; - theRedSide[2] = user.tlSeason1;}); + void changeRedSide(TetraLeague user) { + setState(() { + //theRedSide[0] = user; + theRedSide[2].league = user; + }); } void fetchGreenSide(String user) async { @@ -139,8 +145,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"))); @@ -156,7 +163,8 @@ class CompareState extends State { double vs = double.parse(threeNumbers[2][0]!); theGreenSide = [null, null, - TetraLeagueAlpha( + Summaries(user, TetraLeague( + id: "", timestamp: DateTime.now(), apm: apm, pps: pps, @@ -166,46 +174,49 @@ class CompareState extends State { gamesWon: -1, bestRank: "z", decaying: true, - rating: -1, + tr: -1, + gxe: -1, rank: "z", percentileRank: "z", percentile: 1, standing: -1, standingLocal: -1, nextAt: -1, - prevAt: -1) - ]; + prevAt: -1, season: currentSeason), 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"))); } _justUpdate(); } - void changeGreenSide(TetrioPlayer user) { - setState(() {theGreenSide[0] = user; - theGreenSide[2] = user.tlSeason1;}); + void changeGreenSide(TetraLeague user) { + setState(() { + //theGreenSide[0] = user; + theGreenSide[2].league = user; + }); } double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { @@ -235,10 +246,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){ @@ -246,10 +257,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"); @@ -379,7 +390,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( @@ -389,14 +400,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].rating, - redSide: theRedSide[2].rating, + greenSide: theGreenSide[2].league.tr, + redSide: theRedSide[2].league.tr, fractionDigits: 2, higherIsBetter: true, ), @@ -404,16 +415,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 && @@ -421,93 +432,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, @@ -517,117 +528,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), @@ -637,20 +648,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, @@ -659,22 +670,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: const 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, + ), + ], ) ], ) @@ -682,7 +1275,7 @@ class CompareState extends State { padding: const EdgeInsets.all(8.0), child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), ) - ], + ], ), ), ), @@ -715,10 +1308,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; } } @@ -728,10 +1321,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; } } @@ -1053,14 +1646,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( @@ -1086,12 +1707,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 d5a0d95..9e52818 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -22,6 +22,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/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; @@ -33,11 +34,13 @@ 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'; int _chartsIndex = 0; +int _season = currentSeason-1; bool _gamesPlayedInsteadOfDateAndTime = false; late ZoomPanBehavior _zoomPanBehavior; bool _smooth = false; @@ -63,7 +66,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; @@ -71,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; @@ -79,6 +83,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"; @@ -86,8 +91,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, @@ -154,76 +159,80 @@ 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 TetraLeagueAlphaStream tlStream; - late UserRecords records; + List requests; + Summaries summaries = await teto.fetchSummaries(_searchFor); + late TetraLeagueBetaStream tlStream; late News news; - late SingleplayerStream recent; - late SingleplayerStream sprint; - late SingleplayerStream blitz; + // 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 (7 requests to oskware lmao) + // late TopTr? topTR; + requests = await Future.wait([ + 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.fetchCutoffsBeanserver(), + (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), ]); - tlStream = requests[0] as TetraLeagueAlphaStream; - records = requests[1] as UserRecords; + //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; + 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 + 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; + topOne = requests[6] as TetrioPlayerFromLeaderboard?; + // topTR = requests[8] as TopTr?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); if (prefs.getBool("showPositions") == true){ // Get tetra League leaderboard everyone = teto.getCachedLeaderboard(); everyone ??= await teto.fetchTLLeaderboard(); - if (meAmongEveryone == null){ - meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); + if (meAmongEveryone == null && everyone!.leaderboard.isNotEmpty){ + meAmongEveryone = await compute(everyone!.getLeaderboardPosition, {me.userId: summaries.league}); 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?.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 (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 - List tlMatches = []; bool isTracking = await teto.isPlayerTracking(me.userId); - List states = []; - TetraLeagueAlpha? compareWith; - Set uniqueTL = {}; - tlMatches = tlStream.records; + 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(me); - await teto.saveTLMatchesFromStream(tlStream); + await teto.storeState(summaries.league); + //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 { @@ -236,17 +245,17 @@ class _MainState extends State with TickerProviderStateMixin { _TLHistoryWasFetched = true; } } - 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); + 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.timestamp.isBefore(b.timestamp)) return 1; - if(a.timestamp.isAtSameMomentAs(b.timestamp)) return 0; - if(a.timestamp.isAfter(b.timestamp)) return -1; - return 0; - }); + + // 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){ @@ -264,37 +273,38 @@ 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); - } + //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); - 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")), - ]; + 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 = []; @@ -305,8 +315,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, recentZenith, recentZenithEX, states[currentSeason-1]]; + //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; } /// Triggers widgets rebuild @@ -314,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); @@ -430,12 +444,15 @@ class _MainState extends State with TickerProviderStateMixin { tabs: bigScreen ? [ Tab(text: t.tetraLeague,), Tab(text: t.history), + 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: t.quickPlay), + Tab(text: "${t.quickPlay} ${t.recent}"), Tab(text: t.sprint), Tab(text: t.blitz), Tab(text: t.recentRuns), @@ -455,54 +472,71 @@ 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: snapshot.data![6], + //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, + thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.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, + 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 ), ), 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), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: MediaQuery.of(context).size.width-450, + constraints: const BoxConstraints(maxWidth: 1024), + child: SingleChildScrollView(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]) ] : [ 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, + thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.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, + 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 ), - _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), + 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")), + _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]) ], ), ), @@ -543,6 +577,7 @@ class _MainState extends State with TickerProviderStateMixin { break; default: errText = snapshot.error.toString(); + subText = snapshot.stackTrace.toString(); } return Center(child: Column( @@ -693,7 +728,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 +767,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 +776,76 @@ 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: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), + ), + ); + }); + } +} + +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))), ), ); }); @@ -761,7 +853,7 @@ class _TLRecords extends StatelessWidget { } class _History extends StatelessWidget{ - final List>> chartsData; + final List>>> chartsData; final String userID; final Function update; final Function changePlayer; @@ -784,8 +876,7 @@ 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!; + List<_HistoryChartSpot> selectedGraph = chartsData[_season][_chartsIndex].value!; return SingleChildScrollView( scrollDirection: Axis.vertical, child: Column( @@ -795,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: [ @@ -814,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(); } ), @@ -838,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)) ], )) ], @@ -1015,17 +1120,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" && 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!.endContext.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!.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,23 +1152,23 @@ 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" && 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!.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!))), - 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)), ] ), @@ -1076,14 +1181,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 +1206,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], sprintStream.records[i].gamemode) ) ], ), @@ -1128,7 +1233,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,15 +1244,15 @@ 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" && 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!.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)), - 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))), ] ), ), @@ -1162,14 +1267,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 +1292,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], blitzStream.records[i].gamemode) ) ], ), @@ -1217,7 +1322,6 @@ class _RecentSingleplayersThingy extends StatelessWidget { child: RecentSingleplayerGames(recent: recent, hideTitle: true) ); } - } class _OtherThingy extends StatelessWidget { @@ -1277,7 +1381,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 +1412,16 @@ 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.", + "zenithex" => "${f2.format(news.data["result"])} m.", + _ => "unknown" + }, + style: const TextStyle(fontWeight: FontWeight.bold) + ), ] ) ), @@ -1452,6 +1567,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: const BoxConstraints(maxWidth: 300.0), + child: Row(children: [ + 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 new file mode 100644 index 0000000..c749d5b --- /dev/null +++ b/lib/views/main_view_tiles.dart @@ -0,0 +1,2934 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +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/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'; +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'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/widgets/tl_progress_bar.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. + /// 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(); +} + +enum Page {home, leaderboards, leagueAverages, calculator, settings} +enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} +enum CardMod {info, records, ex, exRecords} +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 +}; + +late ScrollController controller; + +class _MainState extends State with TickerProviderStateMixin { + int destination = 0; + String _searchFor = "6098518e3d5155e6ec429cdc"; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + teto.open(); + controller = ScrollController(); + super.initState(); + } + + void changePlayer(String player) { + setState(() { + _searchFor = player; + }); + } + + @override + void dispose() { + controller.dispose(); + _searchController.dispose(); + 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( + drawer: SearchDrawer(changePlayer: changePlayer, controller: _searchController), + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NavigationRail( + leading: FloatingActionButton( + elevation: 0, + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + child: const Icon(Icons.search), + ), + trailing: IconButton( + onPressed: () { + // Add your onPressed code here! + }, + icon: const Icon(Icons.more_horiz_rounded), + ), + 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) { + setState(() { + destination = value; + }); + }, + ), + Expanded( + child: 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: [ + const Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + 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: const Card( + 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: [ + 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.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")), + ]; + }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 && snapshot.data!.isNotEmpty){ + 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: [ + 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!; + }); + } + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + 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); + }); + } + ), + ], + ), + 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 || snapshot.data!.isEmpty){ + return Center(child: + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + 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), + ), + ], + ) + ); + } + } + return const Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("lol", style: 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 FetchResults{ + bool success; + TetrioPlayer? player; + Summaries? summaries; + Cutoffs? cutoffs; + Exception? exception; + + FetchResults(this.success, this.player, this.summaries, this.cutoffs, 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" && 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){ + "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: const TextSpan( + text: "---", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), + ), + ) + ], + ); + } + +} + +class _DestinationHomeState extends State { + Cards rightCard = Cards.overview; + 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; + + 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, null, TetrioPlayerNotExist()); + } + 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){ + return Column( + children: [ + const 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: 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, 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)) + ], + ), + ), + ), + ), + 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: [ + 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("${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)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 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("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)) + ], + ), + ), + ), + ), + ], + ), + 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: [ + 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.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)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 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.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)) + ], + ), + ), + ), + ), + ], + ), + 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: [ + 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: const 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.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, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + Row( + children: [ + 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: [ + 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!) : "---"), + ], + ) + ], + ), + ), + ), + ), + ], + ), + if (summaries.achievements.isNotEmpty) Card( + child: Padding( + 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: [ + 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: [ + const Text("KO's in QP"), + const Spacer(), + Text(intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)), + ], + ) + ], + ), + ), + ), + ] + ); + } + + Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs){ + 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: data, cutoffs: cutoffs), + if (data.nerdStats != 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 (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!) + ], + ); + } + + Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ + return Column( + children: [ + const Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const TabBar( + tabs: [ + Tab(text: "Recent"), + Tab(text: "Top"), + ], + ), + SizedBox( + height: 400, + child: TabBarView( + children: [ + 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 const Text("what?"); + }, + ), + 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 const 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( + 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 const Text("what?"); + }, + ), + ), + ], + ); + } + + 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) 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, + height: 318, + 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: const 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, + 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, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ + if (record == null) { + return const Card( + child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), + ); + } + 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(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: 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" && 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" || 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() + }, 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)), + ] + ), + ), + ], + ), + ], + ), + Row( + children: [ + Expanded( + child: 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: const 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: const TextStyle(fontSize: 21)), + const Text(" PPS", textAlign: TextAlign.left, style: 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: const TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " KPP", + "blitz" => " SPP", + "5mblast" => " Pieces", + _ => " no" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + 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: const TextStyle(fontSize: 21)), + const Text(" KPS", textAlign: TextAlign.left, style: 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: const 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(){ + 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.records, + label: Text('Recent Matches'), + ), + ], + Cards.quickPlay: [ + const ButtonSegment( + value: CardMod.info, + label: Text('Normal'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ), + const ButtonSegment( + value: CardMod.ex, + label: Text('Expert'), + ), + const ButtonSegment( + value: CardMod.exRecords, + label: Text('Expert Records'), + ) + ], + Cards.blitz: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ) + ], + Cards.sprint: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ) + ] + }; + super.initState(); + } + + @override + Widget build(BuildContext context) { + 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!.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; + } + 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!.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( + 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!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], + ), + ), + //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 const 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 const Text("what?"); + } + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: Column( + children: [ + SizedBox( + 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, snapshot.data!.cutoffs), + CardMod.records => getRecentTLrecords(widget.constraints), + _ => 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), + _ => 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), + _ => 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), + _ => const 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 const Text("End of FutureBuilder"); + }, + ); + } +} + +class NewsThingy extends StatelessWidget{ + final News news; + + const NewsThingy(this.news, {super.key}); + + ListTile getNewsTile(NewsEntry news){ + Map gametypes = { + "40l": t.sprint, + "blitz": t.blitz, + "5mblast": "5,000,000 Blast", + "zenith": "Quick Play", + "zenithex": "Quick Play Expert", + }; + + // 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: 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) + ), + ] + ) + ), + 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( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ] + ), + if (news.news.isEmpty) const Center(child: Text("Empty list")) + else for (NewsEntry entry in news.news) getNewsTile(entry) + ], + ), + ), + ); + } + +} + +class DistinguishmentThingy extends StatelessWidget{ + final Distinguishment distinguishment; + + const 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 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 const Color(0xAACCF5F6); + case "league": return const Color(0xAAFFDB31); + } + case "twc": return const 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: [ + const Spacer(), + Text(t.distinguishment, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const 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 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 const 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: Container( + decoration: banned ? const BoxDecoration( + gradient: LinearGradient( + 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, + 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), + ], + ), + ), + ); + } + +} + +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; + 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 const Color.fromARGB(255, 23, 165, 133); + case "admin": + return const Color.fromARGB(255, 255, 78, 138); + case "mod": + return const Color.fromARGB(255, 204, 128, 242); + case "halfmod": + return const Color.fromARGB(255, 95, 118, 254); + case "bot": + return const Color.fromARGB(255, 60, 93, 55); + case "banned": + return const Color.fromARGB(255, 248, 28, 28); + default: + return Colors.white10; + } + } + + 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); + return LayoutBuilder(builder: (context, constraints) { + double pfpHeight = 128; + int xpTableID = 0; + + while (player.xp > xpTableScuffed.values.toList()[xpTableID]) { + xpTableID++; + } + + return Card( + clipBehavior: Clip.antiAlias, + 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( + //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, + 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}", + 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: 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( + 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: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(player.role))), + ), + RichText( + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: + [ + 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)), + ] + ) + ) + ], + ), + ), + Positioned( + top: player.bannerRevision != null ? 193.0 : 113.0, + left: 160.0, + 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( + top: player.bannerRevision != null ? 126.0 : 46.0, + right: 16.0, + child: RichText( + textAlign: TextAlign.end, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + 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( + title: Text("Level ${intf.format(player.level.floor())}", textAlign: TextAlign.center), + 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: const Text("OK"), + onPressed: () {Navigator.of(context).pop();} + ) + ] + ) + ); + }), + const TextSpan(text:"\n"), + 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, textAlign: TextAlign.center), + content: SingleChildScrollView( + child: ListBody(children: [ + Text( + "${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"), + ] + ), + ), + actions: [ + TextButton( + child: const Text("OK"), + onPressed: () {Navigator.of(context).pop();} + ) + ] + ) + ); + }) : 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)), + ] + ) + ) + ) + ], + ), + ), + ), + 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))))))) + ], + ) + ], + ), + ); + }); + } +} + +class SearchDrawer extends StatefulWidget{ + final Function changePlayer; + final TextEditingController controller; + const SearchDrawer({super.key, required this.changePlayer, required this.controller}); + + @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( + controller: widget.controller, + hintText: "Hello", + hintStyle: const WidgetStatePropertyAll(TextStyle(color: Colors.grey)), + trailing: [ + 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(); + }); + }, + ), + ), + SliverToBoxAdapter( + child: ListTile( + title: Text(prefs.getString("player") ?? "dan63"), + onTap: () { + widget.changePlayer("6098518e3d5155e6ec429cdc"); + Navigator.of(context).pop(); + }, + ), + ) + ]; + }, + 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. + }, + ); + }) + ); + } + } + ) + ); + } +} + +class TetraLeagueThingy extends StatelessWidget{ + final TetraLeague league; + final Cutoffs? cutoffs; + + const TetraLeagueThingy({super.key, required this.league, this.cutoffs}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + TLRatingThingy(userID: "w", 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, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + const 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(f2.format(league.pps??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + const Text("VS: ", style: TextStyle(fontSize: 21)), + Text(f2.format(league.vs??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + ), + SizedBox( + height: 128.0, + width: 128.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: SfRadialGauge( + backgroundColor: Colors.black, + axes: [ + RadialAxis( + minimum: 0.0, + maximum: 1.0, + radiusFactor: 1.01, + showTicks: true, + showLabels: false, + interval: 0.25, + minorTicksPerInterval: 0, + 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( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + //Text("VS: ", 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)), + 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: const TextStyle(fontSize: 21)), + const Text(" Won", style: TextStyle(fontSize: 21)) + ]) + ], + ), + ), + ), + ], + ), + ], + ), + ); + } +} + +class NerdStatsThingy extends StatelessWidget{ + final NerdStats nerdStats; + + const NerdStatsThingy({super.key, required this.nerdStats}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + 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( + 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), + ], + ), + ) + ] + ), + ), + ], + ) + ); + } +} + +class EstTrThingy extends StatelessWidget{ + final EstTr estTr; + + const EstTrThingy({super.key, required this.estTr}); + + @override + Widget build(BuildContext context) { + 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; + 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.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 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 + ) + ], + ) + ] + ), + ), + ); + } +} + +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: "№ ${intf.format(zenith!.rank)}", style: TextStyle(color: getColorOfRank(zenith!.rank))), + if (zenith!.rank != -1) const TextSpan(text: " • "), + 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)), + ] + ), + ), + ], + ), + 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)) + ]) + ], + ), + ), + ), + ], + ) + ] + ), + ) + ); + } + +} + +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))), + ), + ); + }); + } +} + +class TLRatingThingy extends StatelessWidget{ + final String userID; + 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, this.showPositions}); + + @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)) + ], + ), + ), + ], + ), + if (showPositions == true) 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/views/mathes_view.dart b/lib/views/mathes_view.dart index 8f03a82..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; @@ -73,12 +72,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/rank_averages_view.dart b/lib/views/rank_averages_view.dart index b55fae0..14c6ad0 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), ], ), @@ -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), @@ -412,7 +414,9 @@ 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].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: const TextStyle(color: Colors.grey, fontWeight: FontWeight.w100),) ], ), onTap: id.isNotEmpty diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index dbfa0e9..c2f6387 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -1,9 +1,11 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/rank_averages_view.dart'; +import 'package:tetra_stats/utils/text_shadow.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 +19,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}"); @@ -46,29 +43,133 @@ class RanksAverages extends State { ), 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.fetchCutoffsTetrio(), 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: 900, + 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), + 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)), + 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("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!.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!.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!.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(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!.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!.timestamp))) + ], ), - ); - } - }, + ), + ), + ), ); - }) - ), + } + 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/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..b97f141 100644 --- a/lib/views/state_view.dart +++ b/lib/views/state_view.dart @@ -4,6 +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/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'; @@ -11,7 +12,7 @@ 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: TLThingy(tl: widget.state.tlSeason1, userID: widget.state.userId, states: const [],)))); + 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 4263129..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.username.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.last.username.toUpperCase())), + title: Text(t.statesViewTitle(number: "", nickname: widget.nickname)), actions: [ IconButton( onPressed: (){ Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MatchesView(userID: widget.states.first.userId, username: widget.states.first.username), - ), - ); + 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].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))), - 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/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart index 8b16a28..d7699ad 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!.length)}"); - 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].rating)} 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 const Text("end of FutureBuilder"); + } + })), ); } } diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 7d001ae..fc4b50d 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 = const 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,17 @@ class TlMatchResultState extends State { Widget buildComparison(double width, bool showMobileSelector){ bool bigScreen = width >= 768; + if (roundSelector.isNegative){ + time = totalTime; + readableTime = !time.isNegative ? "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "${t.matchLength}: ---"; + }else{ + time = roundLengths[roundSelector]; + 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, - 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 +137,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 +163,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 +199,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 +214,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 +256,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.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, 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.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, + 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 +444,34 @@ 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: !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, 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 ), ), ) @@ -572,109 +480,63 @@ 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(() {}); } : 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: !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( + 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 +569,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 +586,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: } diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart index 905e3f4..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/lib/views/zenith_record_view.dart b/lib/views/zenith_record_view.dart new file mode 100644 index 0000000..b9f5c29 --- /dev/null +++ b/lib/views/zenith_record_view.dart @@ -0,0 +1,38 @@ +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" => t.quickPlay, + "zenithex" => "${t.quickPlay} ${t.expert}", + String() => "5000000 Blast", + } + } ${timestamp(record.timestamp)}"), + ), + body: SafeArea( + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: SingleChildScrollView( + child: ZenithThingy(record: record, switchable: false), + ), + ) + ), + ); + } + +} \ No newline at end of file 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/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 9cb033f..e3e1b7a 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, record.gamemode) ) ], ); diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index 5d19c94..f717ca2 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,30 +23,21 @@ 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))); late MapEntry closestAverageBlitz; late bool blitzBetterThanClosestAverage; - bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.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!.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" && 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; + }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 +53,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,20 +74,20 @@ 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" && 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!.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" || 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!.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" && 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!.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" || 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 != 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)), ] ), @@ -103,29 +95,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 +133,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], stream!.records[i].gamemode) ) ] ), diff --git a/lib/widgets/sp_trailing_stats.dart b/lib/widgets/sp_trailing_stats.dart index 4a5ac72..fc679c6 100644 --- a/lib/widgets/sp_trailing_stats.dart +++ b/lib/widgets/sp_trailing_stats.dart @@ -1,11 +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 EndContextSingle endContext; + final RecordSingle record; + final String gamemode; - const SpTrailingStats(this.endContext, {super.key}); + const SpTrailingStats(this.record, this.gamemode, {super.key}); @override Widget build(BuildContext context) { @@ -14,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(endContext.gameType){ - "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", + Text(switch(gamemode){ + "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/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index 6e415ed..f837bb4 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, @@ -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", @@ -88,25 +88,25 @@ 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( - padding: MaterialStateProperty.all(EdgeInsets.zero)), + padding: WidgetStateProperty.all(EdgeInsets.zero)), child: Text( playerStatLabel, textAlign: TextAlign.center, diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index e1afece..bc8f94f 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -9,9 +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 String? nextRank; - final String? previousRank; + final TetraLeague tlData; 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,65 +29,57 @@ 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.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)) + ] + ) ), - 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)) - ] - ) - ), + const 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.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)) + ] + ) ) - ],), + ], ), SfLinearGauge( minimum: 0, 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 0ebaecb..ac7d4c3 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'; @@ -11,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; @@ -23,13 +22,13 @@ 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 formatedGlicko = f4.format(tlData.glicko).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(); - 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()); + //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, @@ -44,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]), @@ -60,23 +59,23 @@ 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: 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.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 ), ), - Column( + if (tlData.gamesPlayed > 9) Column( children: [ RichText( textAlign: TextAlign.center, @@ -84,14 +83,14 @@ 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: ${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)) + //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..853adde 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.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'; @@ -19,15 +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 List states; final bool showTitle; final bool bot; final bool guest; final double? topTR; final PlayerLeaderboardPosition? lbPositions; - final TetraLeagueAlpha? averages; + final TetraLeague? averages; final double? thatRankCutoff; final double? thatRankCutoffGlicko; final double? thatRankTarget; @@ -43,33 +44,22 @@ 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()); - + 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(); - _countdownTimer = Timer.periodic( - Durations.extralong4, - (Timer timer) { - setState(() { - seasonLeft = seasonEnd.difference(DateTime.now()); - }); - }, - ); } @override void dispose() { - _countdownTimer.cancel(); super.dispose(); } @@ -90,8 +80,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,37 +95,26 @@ 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]; } }); }, ), - 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, 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), - 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( diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index f373f3a..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), ), ); }, @@ -239,7 +238,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, diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart new file mode 100644 index 0000000..a5a43f3 --- /dev/null +++ b/lib/widgets/zenith_thingy.dart @@ -0,0 +1,283 @@ +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'; +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; + + const ZenithThingy({super.key, 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 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: "№ ${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(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: "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), + 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: [ + 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: const TextStyle( + shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + 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: [ + 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/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..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.2+22 +version: 1.6.9+35 environment: sdk: '>=3.0.0' @@ -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 diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 0cc9abb..1f3ba9a 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -87,6 +87,9 @@ "verdictBetter": "better", "verdictWorse": "worse", "smooth": "Smooth", + "postSeason": "Off-season", + "seasonStarts": "Season starts in:", + "nanow": "Not avaliable for now...", "seasonEnds": "Season ends in ${countdown}", "seasonEnded": "Season has ended", "gamesUntilRanked": "${left} games until being ranked", @@ -101,6 +104,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", @@ -141,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", @@ -255,7 +269,7 @@ "lbpcShort": "№ in local LB", "gamesPlayed": "Games\nplayed", "gamesWonTL": "Games\nWon", - "winrate": "Winrate\nprecentage", + "winrate": "Winrate", "level": "Level", "score": "Score", "spp": "Score\nPer Piece", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index e23b1cb..5f48489 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -87,6 +87,9 @@ "verdictBetter": "Лучше", "verdictWorse": "Хуже", "smooth": "Гладкий", + "postSeason": "Внесезонье", + "seasonStarts": "Сезон начнётся через:", + "nanow": "Пока недоступно...", "seasonEnds": "Сезон закончится через ${countdown}", "seasonEnded": "Сезон закончился", "gamesUntilRanked": "${left} матчей до получения рейтинга", @@ -101,6 +104,17 @@ "neverPlayedTL": "Этот игрок никогда не играл в Тетра Лигу", "botTL": "Ботам нельзя играть в Тетра Лигу", "anonTL": "Гостям нельзя играть в Тетра Лигу", + "quickPlay": "Быстрая Игра", + "expert": "Эксперт", + "withMods": "С модами", + "withModsPlural":{ + "zero": "с $n модами", + "one": "с $n модом", + "two": "с $n модами", + "few": "с $n модами", + "many": "с $n модами", + "other": "с $n модами" + }, "exportDB": "Экспортировать локальную базу данных", "exportDBDescription": "Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.", "desktopExportAlertTitle": "Экспорт на десктопе", @@ -141,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": "Все матчи", 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/allspin.png b/res/icons/allspin.png new file mode 100644 index 0000000..f40c752 Binary files /dev/null and b/res/icons/allspin.png differ 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/doublehole.png b/res/icons/doublehole.png new file mode 100644 index 0000000..687d979 Binary files /dev/null and b/res/icons/doublehole.png differ diff --git a/res/icons/expert.png b/res/icons/expert.png new file mode 100644 index 0000000..678ce4f Binary files /dev/null and b/res/icons/expert.png differ diff --git a/res/icons/gravity.png b/res/icons/gravity.png new file mode 100644 index 0000000..54fe2ff Binary files /dev/null and b/res/icons/gravity.png differ diff --git a/res/icons/invisible.png b/res/icons/invisible.png new file mode 100644 index 0000000..1340941 Binary files /dev/null and b/res/icons/invisible.png differ 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/messy.png b/res/icons/messy.png new file mode 100644 index 0000000..7fc7964 Binary files /dev/null and b/res/icons/messy.png differ diff --git a/res/icons/nohold.png b/res/icons/nohold.png new file mode 100644 index 0000000..38811ec Binary files /dev/null and b/res/icons/nohold.png differ 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 + + + + + + + + + + + + + + + diff --git a/res/icons/volatile.png b/res/icons/volatile.png new file mode 100644 index 0000000..aef5dcd Binary files /dev/null and b/res/icons/volatile.png differ diff --git a/res/tetrio_tl_alpha_ranks/x+.png b/res/tetrio_tl_alpha_ranks/x+.png new file mode 100644 index 0000000..f23e262 Binary files /dev/null and b/res/tetrio_tl_alpha_ranks/x+.png differ diff --git a/test/api_test.dart b/test/api_test.dart index 52f3e87..b05b70a 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.tr > -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.tr > -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.tr, -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.tr, -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.tr, -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