Merge pull request #139 from dan63047/master

Put 1.6.9 into stable, so i can refactor without the fear that i will not be able fix 1.6.9 issues
This commit is contained in:
dan63047 2024-09-04 22:02:27 +00:00 committed by GitHub
commit 6d17590a04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 7084 additions and 2029 deletions

View File

@ -18,7 +18,7 @@ jobs:
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.16.5' flutter-version: '3.22.3'
- name: Install project dependencies - name: Install project dependencies
run: flutter pub get run: flutter pub get
- name: Build artifacts - name: Build artifacts
@ -40,51 +40,28 @@ jobs:
tag: Auto-${{ github.run_number }} tag: Auto-${{ github.run_number }}
body: Builded with GitHub Action workflow body: Builded with GitHub Action workflow
token: ${{ secrets.TOKEN }} token: ${{ secrets.TOKEN }}
# build-and-release-linux: build-and-release-linux:
# name: Build Linux App 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
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: ashutoshvarma/setup-ninja@master
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
with: with:
flutter-version: '3.16.5' channel: 'stable'
- run: flutter pub get flutter-version: '3.22.3'
# - run: flutter test // lmao. Tests? Who needs it? - name: Install project dependencies
- run: flutter build apk --split-per-abi 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 - name: Push to Releases
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
@ -92,7 +69,32 @@ jobs:
allowUpdates: true allowUpdates: true
replacesArtifacts: false replacesArtifacts: false
discussionCategory: autobuilded-releases 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 }} tag: Auto-${{ github.run_number }}
body: Builded with GitHub Action workflow body: Builded with GitHub Action workflow
token: ${{ secrets.TOKEN }} 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 }}

View File

@ -1,83 +1,83 @@
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader -> localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader) localProperties.load(reader)
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk') def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) { if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
} }
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
} }
def flutterVersionName = localProperties.getProperty('flutter.versionName') def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) { if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} }
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '1.8'
} }
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.dan63.tetra_stats" applicationId "com.dan63.tetra_stats"
// You can update the following values to match your application needs. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 19 minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
signingConfigs { signingConfigs {
release { release {
keyAlias keystoreProperties['keyAlias'] keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword'] keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword'] storePassword keystoreProperties['storePassword']
} }
} }
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.release
} }
} }
} }
flutter { flutter {
source '../..' source '../..'
} }
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
} }

View File

@ -1,10 +1,12 @@
// p1nkl0bst3r data objects // p1nkl0bst3r data objects
class Cutoffs{ class Cutoffs{
DateTime ts;
Map<String, double> tr; Map<String, double> tr;
Map<String, double> glicko; Map<String, double> glicko;
Map<String, double> gxe;
Cutoffs(this.tr, this.glicko); Cutoffs(this.ts, this.tr, this.glicko, this.gxe);
} }
class TopTr{ class TopTr{

File diff suppressed because it is too large Load Diff

View File

@ -134,7 +134,7 @@ class ReplayStats{
topSpike = 0; topSpike = 0;
tspins = 0; tspins = 0;
roundLength = 0.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); garbage = Garbage(sent: 0, recived: 0, attack: 0, cleared: 0);
finesse = Finesse(combo: 0, faults: 0, perfectPieces: 0); finesse = Finesse(combo: 0, faults: 0, perfectPieces: 0);
} }
@ -208,12 +208,12 @@ class ReplayData{
stats = []; stats = [];
roundWinners = []; roundWinners = [];
int roundID = 0; int roundID = 0;
List<double> APMmultipliedByWeights = [0, 0]; List<double> apmMultipliedByWeights = [0, 0];
List<double> PPSmultipliedByWeights = [0, 0]; List<double> ppsMultipliedByWeights = [0, 0];
List<double> VSmultipliedByWeights = [0, 0]; List<double> vsMultipliedByWeights = [0, 0];
List<double> SPPmultipliedByWeights = [0, 0]; List<double> sppMultipliedByWeights = [0, 0];
List<double> KPPmultipliedByWeights = [0, 0]; List<double> kppMultipliedByWeights = [0, 0];
List<double> KPSmultipliedByWeights = [0, 0]; List<double> kpsMultipliedByWeights = [0, 0];
totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()]; totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()];
for(var round in json['data']) { for(var round in json['data']) {
int firstInEndContext = round['replays'][0]["events"].last['data']['export']['options']['username'].startsWith(endcontext[0].username) ? 0 : 1; 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']); int roundLength = max(round['replays'][0]['frames'], round['replays'][1]['frames']);
roundLengths.add(roundLength); roundLengths.add(roundLength);
totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']); totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']);
APMmultipliedByWeights[0] += endcontext[0].secondaryTracking[roundID]*roundLength; apmMultipliedByWeights[0] += endcontext[0].secondaryTracking[roundID]*roundLength;
APMmultipliedByWeights[1] += endcontext[1].secondaryTracking[roundID]*roundLength; apmMultipliedByWeights[1] += endcontext[1].secondaryTracking[roundID]*roundLength;
PPSmultipliedByWeights[0] += endcontext[0].tertiaryTracking[roundID]*roundLength; ppsMultipliedByWeights[0] += endcontext[0].tertiaryTracking[roundID]*roundLength;
PPSmultipliedByWeights[1] += endcontext[1].tertiaryTracking[roundID]*roundLength; ppsMultipliedByWeights[1] += endcontext[1].tertiaryTracking[roundID]*roundLength;
VSmultipliedByWeights[0] += endcontext[0].extraTracking[roundID]*roundLength; vsMultipliedByWeights[0] += endcontext[0].extraTracking[roundID]*roundLength;
VSmultipliedByWeights[1] += endcontext[1].extraTracking[roundID]*roundLength; vsMultipliedByWeights[1] += endcontext[1].extraTracking[roundID]*roundLength;
int winner = round['board'].indexWhere((element) => element['success'] == true); 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']]); 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 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']); 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[0] += playerOne.spp*roundLength;
SPPmultipliedByWeights[1] += playerTwo.spp*roundLength; sppMultipliedByWeights[1] += playerTwo.spp*roundLength;
KPPmultipliedByWeights[0] += playerOne.kpp*roundLength; kppMultipliedByWeights[0] += playerOne.kpp*roundLength;
KPPmultipliedByWeights[1] += playerTwo.kpp*roundLength; kppMultipliedByWeights[1] += playerTwo.kpp*roundLength;
KPSmultipliedByWeights[0] += playerOne.kps*roundLength; kpsMultipliedByWeights[0] += playerOne.kps*roundLength;
KPSmultipliedByWeights[1] += playerTwo.kps*roundLength; kpsMultipliedByWeights[1] += playerTwo.kps*roundLength;
stats.add([playerOne, playerTwo]); stats.add([playerOne, playerTwo]);
totalStats[0] = totalStats[0] + playerOne; totalStats[0] = totalStats[0] + playerOne;
totalStats[1] = totalStats[1] + playerTwo; totalStats[1] = totalStats[1] + playerTwo;
roundID ++; roundID ++;
} }
timeWeightedStats = [ timeWeightedStats = [
AggregateStats(APMmultipliedByWeights[0]/totalLength, PPSmultipliedByWeights[0]/totalLength, VSmultipliedByWeights[0]/totalLength, SPPmultipliedByWeights[0]/totalLength, KPPmultipliedByWeights[0]/totalLength, KPSmultipliedByWeights[0]/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) AggregateStats(apmMultipliedByWeights[1]/totalLength, ppsMultipliedByWeights[1]/totalLength, vsMultipliedByWeights[1]/totalLength, sppMultipliedByWeights[1]/totalLength, kppMultipliedByWeights[1]/totalLength, kpsMultipliedByWeights[1]/totalLength)
]; ];
} }

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 2 /// Locales: 2
/// Strings: 1186 (593 per locale) /// Strings: 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 // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
@ -222,6 +222,9 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get verdictBetter => 'better'; String get verdictBetter => 'better';
String get verdictWorse => 'worse'; String get verdictWorse => 'worse';
String get smooth => 'Smooth'; String get smooth => 'Smooth';
String get postSeason => 'Off-season';
String get seasonStarts => 'Season starts in:';
String get nanow => 'Not avaliable for now...';
String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}'; String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}';
String get seasonEnded => 'Season has ended'; String get seasonEnded => 'Season has ended';
String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; String gamesUntilRanked({required Object left}) => '${left} games until being ranked';
@ -236,6 +239,17 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get neverPlayedTL => 'That user never played Tetra League'; String get neverPlayedTL => 'That user never played Tetra League';
String get botTL => 'Bots are not allowed to play 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 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 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 exportDBDescription => 'It contains states and Tetra League records of the tracked players and list of tracked players.';
String get desktopExportAlertTitle => 'Desktop export'; String get desktopExportAlertTitle => 'Desktop export';
@ -276,7 +290,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}'; 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 statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
String matchesViewTitle({required Object nickname}) => '${nickname} TL matches'; 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 stateRemoved({required Object date}) => '${date} state was removed from database!';
String matchRemoved({required Object date}) => '${date} match was removed from database!'; String matchRemoved({required Object date}) => '${date} match was removed from database!';
String get viewAllMatches => 'View all matches'; String get viewAllMatches => 'View all matches';
@ -703,7 +717,7 @@ class _StringsStatCellNumEn {
String get lbpcShort => '№ in local LB'; String get lbpcShort => '№ in local LB';
String get gamesPlayed => 'Games\nplayed'; String get gamesPlayed => 'Games\nplayed';
String get gamesWonTL => 'Games\nWon'; String get gamesWonTL => 'Games\nWon';
String get winrate => 'Winrate\nprecentage'; String get winrate => 'Winrate';
String get level => 'Level'; String get level => 'Level';
String get score => 'Score'; String get score => 'Score';
String get spp => 'Score\nPer Piece'; String get spp => 'Score\nPer Piece';
@ -919,6 +933,9 @@ class _StringsRu implements Translations {
@override String get verdictBetter => 'Лучше'; @override String get verdictBetter => 'Лучше';
@override String get verdictWorse => 'Хуже'; @override String get verdictWorse => 'Хуже';
@override String get smooth => 'Гладкий'; @override String get smooth => 'Гладкий';
@override String get postSeason => 'Внесезонье';
@override String get seasonStarts => 'Сезон начнётся через:';
@override String get nanow => 'Пока недоступно...';
@override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}'; @override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}';
@override String get seasonEnded => 'Сезон закончился'; @override String get seasonEnded => 'Сезон закончился';
@override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга';
@ -933,6 +950,17 @@ class _StringsRu implements Translations {
@override String get neverPlayedTL => 'Этот игрок никогда не играл в Тетра Лигу'; @override String get neverPlayedTL => 'Этот игрок никогда не играл в Тетра Лигу';
@override String get botTL => 'Ботам нельзя играть в Тетра Лигу'; @override String get botTL => 'Ботам нельзя играть в Тетра Лигу';
@override String get anonTL => 'Гостям нельзя играть в Тетра Лигу'; @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 exportDB => 'Экспортировать локальную базу данных';
@override String get exportDBDescription => 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; @override String get exportDBDescription => 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.';
@override String get desktopExportAlertTitle => 'Экспорт на десктопе'; @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 stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
@override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; @override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
@override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${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 stateRemoved({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!';
@override String matchRemoved({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; @override String matchRemoved({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!';
@override String get viewAllMatches => 'Все матчи'; @override String get viewAllMatches => 'Все матчи';
@ -1608,6 +1636,9 @@ extension on Translations {
case 'verdictBetter': return 'better'; case 'verdictBetter': return 'better';
case 'verdictWorse': return 'worse'; case 'verdictWorse': return 'worse';
case 'smooth': return 'Smooth'; case 'smooth': return 'Smooth';
case 'postSeason': return 'Off-season';
case 'seasonStarts': return 'Season starts in:';
case 'nanow': return 'Not avaliable for now...';
case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}'; case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}';
case 'seasonEnded': return 'Season has ended'; case 'seasonEnded': return 'Season has ended';
case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked';
@ -1622,6 +1653,17 @@ extension on Translations {
case 'neverPlayedTL': return 'That user never played Tetra League'; case 'neverPlayedTL': return 'That user never played Tetra League';
case 'botTL': return 'Bots are not allowed to play 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 '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 '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 'exportDBDescription': return 'It contains states and Tetra League records of the tracked players and list of tracked players.';
case 'desktopExportAlertTitle': return 'Desktop export'; 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 '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 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches'; 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 'stateRemoved': return ({required Object date}) => '${date} state was removed from database!';
case 'matchRemoved': return ({required Object date}) => '${date} match was removed from database!'; case 'matchRemoved': return ({required Object date}) => '${date} match was removed from database!';
case 'viewAllMatches': return 'View all matches'; case 'viewAllMatches': return 'View all matches';
@ -1773,7 +1815,7 @@ extension on Translations {
case 'statCellNum.lbpcShort': return '№ in local LB'; case 'statCellNum.lbpcShort': return '№ in local LB';
case 'statCellNum.gamesPlayed': return 'Games\nplayed'; case 'statCellNum.gamesPlayed': return 'Games\nplayed';
case 'statCellNum.gamesWonTL': return 'Games\nWon'; case 'statCellNum.gamesWonTL': return 'Games\nWon';
case 'statCellNum.winrate': return 'Winrate\nprecentage'; case 'statCellNum.winrate': return 'Winrate';
case 'statCellNum.level': return 'Level'; case 'statCellNum.level': return 'Level';
case 'statCellNum.score': return 'Score'; case 'statCellNum.score': return 'Score';
case 'statCellNum.spp': return 'Score\nPer Piece'; case 'statCellNum.spp': return 'Score\nPer Piece';
@ -2221,6 +2263,9 @@ extension on _StringsRu {
case 'verdictBetter': return 'Лучше'; case 'verdictBetter': return 'Лучше';
case 'verdictWorse': return 'Хуже'; case 'verdictWorse': return 'Хуже';
case 'smooth': return 'Гладкий'; case 'smooth': return 'Гладкий';
case 'postSeason': return 'Внесезонье';
case 'seasonStarts': return 'Сезон начнётся через:';
case 'nanow': return 'Пока недоступно...';
case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}'; case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}';
case 'seasonEnded': return 'Сезон закончился'; case 'seasonEnded': return 'Сезон закончился';
case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга';
@ -2235,6 +2280,17 @@ extension on _StringsRu {
case 'neverPlayedTL': return 'Этот игрок никогда не играл в Тетра Лигу'; case 'neverPlayedTL': return 'Этот игрок никогда не играл в Тетра Лигу';
case 'botTL': return 'Ботам нельзя играть в Тетра Лигу'; case 'botTL': return 'Ботам нельзя играть в Тетра Лигу';
case 'anonTL': 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 'exportDB': return 'Экспортировать локальную базу данных';
case 'exportDBDescription': return 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; case 'exportDBDescription': return 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.';
case 'desktopExportAlertTitle': return 'Экспорт на десктопе'; case 'desktopExportAlertTitle': return 'Экспорт на десктопе';
@ -2275,7 +2331,7 @@ extension on _StringsRu {
case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${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 'stateRemoved': return ({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!';
case 'matchRemoved': return ({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; case 'matchRemoved': return ({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!';
case 'viewAllMatches': return 'Все матчи'; case 'viewAllMatches': return 'Все матчи';

View File

@ -25,39 +25,33 @@ import 'package:go_router/go_router.dart';
late final PackageInfo packageInfo; late final PackageInfo packageInfo;
late SharedPreferences prefs; late SharedPreferences prefs;
late TetrioService teto; late TetrioService teto;
ThemeData theme = ThemeData(fontFamily: 'Eurostile Round', colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white), scaffoldBackgroundColor: Colors.black); ThemeData theme = ThemeData(
fontFamily: 'Eurostile Round',
// Future<dynamic> computeIsolate(Future Function() function) async { colorScheme: const ColorScheme.dark(
// final receivePort = ReceivePort(); primary: Colors.cyanAccent,
// var rootToken = RootIsolateToken.instance!; surface: Color.fromARGB(255, 10, 10, 10),
// await Isolate.spawn<_IsolateData>( secondary: Color(0xFF00838F),
// _isolateEntry, ),
// _IsolateData( cardTheme: const CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)),
// token: rootToken, drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)),
// function: function, searchBarTheme: const SearchBarThemeData(
// answerPort: receivePort.sendPort, shadowColor: WidgetStatePropertyAll(Colors.black),
// ), shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(12.0), right: Radius.circular(12.0)))),
// ); elevation: WidgetStatePropertyAll(8.0)
// return await receivePort.first; ),
// } chipTheme: const ChipThemeData(
side: BorderSide(color: Colors.transparent),
// void _isolateEntry(_IsolateData isolateData) async { ),
// BackgroundIsolateBinaryMessenger.ensureInitialized(isolateData.token); segmentedButtonTheme: SegmentedButtonThemeData(
// final answer = await isolateData.function(); style: ButtonStyle(
// isolateData.answerPort.send(answer); side: const WidgetStatePropertyAll(BorderSide(color: Colors.transparent)),
// } surfaceTintColor: const WidgetStatePropertyAll(Colors.cyanAccent),
iconColor: const WidgetStatePropertyAll(Colors.cyanAccent),
// class _IsolateData { shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200),
// final RootIsolateToken token; )
// final Function function; ),
// final SendPort answerPort; scaffoldBackgroundColor: Colors.black
);
// _IsolateData({
// required this.token,
// required this.function,
// required this.answerPort,
// });
// }
final router = GoRouter( final router = GoRouter(
initialLocation: "/", initialLocation: "/",
@ -189,4 +183,4 @@ class MyAppState extends State<MyApp> {
theme: theme theme: theme
); );
} }
} }

View File

@ -1,3 +1,6 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
class UserAgentClient extends http.BaseClient { class UserAgentClient extends http.BaseClient {
@ -9,6 +12,7 @@ class UserAgentClient extends http.BaseClient {
@override @override
Future<http.StreamedResponse> send(http.BaseRequest request) { Future<http.StreamedResponse> send(http.BaseRequest request) {
request.headers['user-agent'] = userAgent; request.headers['user-agent'] = userAgent;
if (!kIsWeb) request.headers['X-Session-ID'] = "${Random().nextInt(1<<32)}";
return _inner.send(request); return _inner.send(request);
} }
} }

View File

@ -33,6 +33,7 @@ class DB {
await db.execute(createTetrioUsersToTrack); await db.execute(createTetrioUsersToTrack);
await db.execute(createTetrioTLRecordsTable); await db.execute(createTetrioTLRecordsTable);
await db.execute(createTetrioTLReplayStats); await db.execute(createTetrioTLReplayStats);
await db.execute(createTetrioLeagueTable);
} on MissingPlatformDirectoryException { } on MissingPlatformDirectoryException {
throw UnableToGetDocuments(); throw UnableToGetDocuments();
} }

View File

@ -4,8 +4,9 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.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/tetra_stats.dart';
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
import 'package:tetra_stats/main.dart' show packageInfo; import 'package:tetra_stats/main.dart' show packageInfo;
@ -22,6 +23,7 @@ const String tetrioUsersTable = "tetrioUsers";
const String tetrioUsersToTrackTable = "tetrioUsersToTrack"; const String tetrioUsersToTrackTable = "tetrioUsersToTrack";
const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces"; const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces";
const String tetrioTLReplayStatsTable = "tetrioTLReplayStats"; const String tetrioTLReplayStatsTable = "tetrioTLReplayStats";
const String tetrioLeagueTable = "tetrioLeague";
const String idCol = "id"; const String idCol = "id";
const String replayID = "replayId"; const String replayID = "replayId";
const String nickCol = "nickname"; const String nickCol = "nickname";
@ -68,6 +70,33 @@ const String createTetrioTLReplayStats = '''
PRIMARY KEY("id") 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 { class CacheController {
late Map<String, dynamic> _cache; late Map<String, dynamic> _cache;
@ -90,8 +119,6 @@ class CacheController {
return object.runtimeType.toString(); return object.runtimeType.toString();
case TetrioPlayerFromLeaderboard: // i may be a little stupid case TetrioPlayerFromLeaderboard: // i may be a little stupid
return "${object.runtimeType}topone"; return "${object.runtimeType}topone";
case TetraLeagueAlphaStream:
return object.runtimeType.toString()+object.userId;
case SingleplayerStream: case SingleplayerStream:
return object.type+object.userId; return object.type+object.userId;
default: default:
@ -99,8 +126,8 @@ class CacheController {
} }
} }
void store(dynamic object, int? cachedUntil) async { void store(dynamic object, int cachedUntil) async {
String key = _getObjectId(object) + cachedUntil!.toString(); String key = _getObjectId(object) + cachedUntil.toString();
_cache[key] = object; _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)); 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?"; if (id.length <= 16) id = _nicknames[id]??"huh?";
break; break;
case SingleplayerStream:
objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(id));
default: default:
objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(datatype.toString()+id)); objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(datatype.toString()+id));
id = datatype.toString()+id; id = datatype.toString()+id;
@ -184,6 +213,7 @@ class TetrioService extends DB {
_players.removeWhere((key, value) => key == id); _players.removeWhere((key, value) => key == id);
_tetrioStreamController.add(_players); _tetrioStreamController.add(_players);
} }
await db.delete(tetrioLeagueTable, where: "id LIKE ?", whereArgs: ["$id%"]);
} }
/// Gets nickname from database or requests it from API if missing. /// 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). /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve. /// Throws an exception if fails to retrieve.
Future<SingleplayerStream> fetchSingleplayerStream(String userID, String stream) async { Future<SingleplayerStream> fetchStream(String userID, String stream) async {
SingleplayerStream? cached = _cache.get(userID, SingleplayerStream); SingleplayerStream? cached = _cache.get(stream+userID, SingleplayerStream);
if (cached != null) return cached; if (cached != null) return cached;
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream});
} else { } 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 { try {
final response = await client.get(url); final response = await client.get(url);
@ -325,7 +355,7 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
if (jsonDecode(response.body)['success']) { if (jsonDecode(response.body)['success']) {
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']); _cache.store(records, jsonDecode(response.body)['cache']['cached_until']);
developer.log("fetchSingleplayerStream: $stream $userID stream retrieved and cached", name: "services/tetrio_crud"); developer.log("fetchSingleplayerStream: $stream $userID stream retrieved and cached", name: "services/tetrio_crud");
return records; 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, // 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 // so i'm going to document only unique differences between them
Future<Cutoffs?> fetchCutoffs() async { Future<CutoffsTetrio?> fetchCutoffsTetrio() async {
Cutoffs? cached = _cache.get("", Cutoffs); CutoffsTetrio? cached = _cache.get("league_ranks", CutoffsTetrio);
if (cached != null) return cached; if (cached != null) return cached;
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLCutoffs"}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "cutoffs"});
} else { } else {
url = Uri.https('api.p1nkl0bst3r.xyz', 'rankcutoff', {"users": null}); url = Uri.https('ch.tetr.io', 'api/labs/league_ranks');
} }
try{ try{
@ -425,16 +455,58 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
Map<String, dynamic> rawData = jsonDecode(response.body); Map<String, dynamic> rawData = jsonDecode(response.body);
Map<String, dynamic> data = rawData["cutoffs"] as Map<String, dynamic>; CutoffsTetrio result = CutoffsTetrio.fromJson(rawData['data']);
Cutoffs result = Cutoffs({}, {}); _cache.store(result, rawData["cache"]["cached_until"]);
for (String rank in data.keys){
result.tr[rank] = data[rank]["rating"];
result.glicko[rank] = data[rank]["glicko"];
}
_cache.store(result, rawData["ts"] + 300000);
return result; return result;
case 404: 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<Cutoffs?> 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<String, dynamic> rawData = jsonDecode(response.body);
Map<String, dynamic> data = rawData["data"] as Map<String, dynamic>;
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; return null;
// if not 200 or 404 - throw a unique for each code exception // if not 200 or 404 - throw a unique for each code exception
case 403: case 403:
@ -447,10 +519,10 @@ class TetrioService extends DB {
case 502: case 502:
case 503: case 503:
case 504: 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; return null;
default: 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"); throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
} }
} on http.ClientException catch (e, s) { // If local http client fails } on http.ClientException catch (e, s) { // If local http client fails
@ -467,7 +539,7 @@ class TetrioService extends DB {
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLTopOne"}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLTopOne"});
} else { } 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{ try{
@ -476,7 +548,7 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
var rawJson = jsonDecode(response.body); 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"]); _cache.store(result, rawJson["cache"]["cached_until"]);
return result; return result;
case 404: 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 /// 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. /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data.
Future<List<TetrioPlayer>> fetchAndsaveTLHistory(String id) async { Future<List<TetraLeague>> fetchAndsaveTLHistory(String id) async {
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id});
@ -517,27 +589,15 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
// that one api returns csv instead of json // that one api returns csv instead of json
List<List<dynamic>> csv = const CsvToListConverter().convert(response.body)..removeAt(0); List<List<dynamic>> csv = const CsvToListConverter().convert(response.body)..removeAt(0);
List<TetrioPlayer> history = []; List<TetraLeague> history = [];
// doesn't return nickname, need to retrieve it separately Batch batch = db.batch();
String nick = await getNicknameByID(id);
for (List<dynamic> entry in csv){ // each entry is one state for (List<dynamic> entry in csv){ // each entry is one state
TetrioPlayer state = TetrioPlayer( TetraLeague state = TetraLeague(
userId: id, id: 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(
timestamp: DateTime.parse(entry[9]), timestamp: DateTime.parse(entry[9]),
apm: entry[6] != '' ? entry[6] : null, apm: entry[6] != '' ? entry[6] : null,
pps: entry[7] != '' ? entry[7] : null, pps: entry[7] != '' ? entry[7] : null,
@ -548,33 +608,21 @@ class TetrioService extends DB {
gamesWon: entry[2], gamesWon: entry[2],
bestRank: "z", bestRank: "z",
decaying: false, decaying: false,
rating: entry[3], tr: entry[3],
gxe: -1,
rank: entry[5], rank: entry[5],
percentileRank: entry[5], percentileRank: entry[5],
percentile: rankCutoffs[entry[5]]!, percentile: rankCutoffs[entry[5]]!,
standing: -1, standing: -1,
standingLocal: -1, standingLocal: -1,
nextAt: -1, nextAt: -1,
prevAt: -1 prevAt: -1,
), season: 1
sprint: [],
blitz: []
); );
history.add(state); history.add(state);
batch.insert(tetrioLeagueTable, state.toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
} }
batch.commit();
// trying to dump it to local DB
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
List<TetrioPlayer> states = await getPlayer(id);
if (states.isEmpty) await createPlayer(history.first);
states.insertAll(0, history.reversed);
final Map<String, dynamic> statesJson = {};
for (var e in states) { // making one big json out of this list
statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
}
// and putting it to local DB
await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]);
return history; return history;
case 404: case 404:
developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); 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 /// Docs later
Future<List<TetraLeagueAlphaRecord>> fetchAndSaveOldTLmatches(String userID) async { Future<TetraLeagueAlphaStream> fetchAndSaveOldTLmatches(String userID) async {
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID});
@ -616,7 +664,7 @@ class TetrioService extends DB {
case 200: case 200:
TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID); TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID);
saveTLMatchesFromStream(stream); saveTLMatchesFromStream(stream);
return stream.records; return stream;
case 404: case 404:
developer.log("fetchAndSaveOldTLmatches: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); developer.log("fetchAndSaveOldTLmatches: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode);
throw TetrioHistoryNotExist(); throw TetrioHistoryNotExist();
@ -645,13 +693,9 @@ class TetrioService extends DB {
Future<TetrioPlayersLeaderboard> fetchTLLeaderboard() async { Future<TetrioPlayersLeaderboard> fetchTLLeaderboard() async {
TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
if (cached != null) return cached; if (cached != null) return cached;
Uri url; Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/leaderboard.json');
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');
}
try{ try{
final response = await client.get(url); final response = await client.get(url);
@ -659,16 +703,10 @@ class TetrioService extends DB {
case 200: case 200:
_lbPositions.clear(); _lbPositions.clear();
var rawJson = jsonDecode(response.body); var rawJson = jsonDecode(response.body);
if (rawJson['success']) { // if api confirmed that everything ok TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['created']));
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");
developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); _cache.store(leaderboard, rawJson['cache_until']);
//_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; return 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?
}
case 403: case 403:
throw TetrioForbidden(); throw TetrioForbidden();
case 429: case 429:
@ -690,6 +728,20 @@ class TetrioService extends DB {
} }
} }
// Stream<TetrioPlayersLeaderboard> 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: // i want to know progress, so i trying to figure out this thing:
// Stream<TetrioPlayersLeaderboard> fetchTLLeaderboardAsStream() async { // Stream<TetrioPlayersLeaderboard> fetchTLLeaderboardAsStream() async {
// TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); // 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. /// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve.
Future<News> fetchNews(String userID) async{ Future<News> fetchNews(String userID) async{
News? cached = _cache.get(userID, News); News? cached = _cache.get("user_$userID", News);
if (cached != null) return cached; if (cached != null) return cached;
Uri url; Uri url;
@ -758,15 +810,15 @@ class TetrioService extends DB {
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream). /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve. /// Throws an exception if fails to retrieve.
Future<TetraLeagueAlphaStream> fetchTLStream(String userID) async { Future<TetraLeagueBetaStream> fetchTLStream(String userID) async {
TetraLeagueAlphaStream? cached = _cache.get(userID, TetraLeagueAlphaStream); TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream);
if (cached != null) return cached; if (cached != null) return cached;
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()});
} else { } else {
url = Uri.https('ch.tetr.io', 'api/streams/league_userrecent_${userID.toLowerCase().trim()}'); url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/league/recent');
} }
try { try {
final response = await client.get(url); final response = await client.get(url);
@ -774,7 +826,7 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
if (jsonDecode(response.body)['success']) { if (jsonDecode(response.body)['success']) {
TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID); TetraLeagueBetaStream stream = TetraLeagueBetaStream.fromJson(jsonDecode(response.body)['data']['entries'], userID);
_cache.store(stream, jsonDecode(response.body)['cache']['cached_until']); _cache.store(stream, jsonDecode(response.body)['cache']['cached_until']);
developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud");
return stream; return stream;
@ -906,10 +958,10 @@ class TetrioService extends DB {
if (jsonDecode(response.body)['success']) { if (jsonDecode(response.body)['success']) {
Map jsonRecords = jsonDecode(response.body); Map jsonRecords = jsonDecode(response.body);
var sprint = jsonRecords['data']['records']['40l']['record'] != null 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; : null;
var blitz = jsonRecords['data']['records']['blitz']['record'] != 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; : null;
var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); var zen = TetrioZen.fromJson(jsonRecords['data']['zen']);
UserRecords result = UserRecords(userID, sprint, blitz, zen); UserRecords result = UserRecords(userID, sprint, blitz, zen);
@ -941,6 +993,52 @@ class TetrioService extends DB {
} }
} }
Future<Summaries> fetchSummaries(String id) async {
Summaries? cached = _cache.get(id, Summaries);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "Summaries", "id": id});
} else {
url = Uri.https('ch.tetr.io', 'api/users/$id/summaries');
}
try{
final response = await client.get(url);
switch (response.statusCode) {
case 200:
if (jsonDecode(response.body)['success']) {
developer.log("fetchSummaries: $id summaries retrieved and cached", name: "services/tetrio_crud");
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. /// Creates an entry in local DB for [tetrioPlayer]. Throws an exception if that player already here.
Future<void> createPlayer(TetrioPlayer tetrioPlayer) async { Future<void> createPlayer(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
@ -967,7 +1065,10 @@ class TetrioService extends DB {
if (results.isNotEmpty) { if (results.isNotEmpty) {
throw TetrioPlayerAlreadyExist(); throw TetrioPlayerAlreadyExist();
} }
await db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username}, conflictAlgorithm: ConflictAlgorithm.replace);
db.insert(tetrioUsersToTrackTable, {idCol: tetrioPlayer.userId}); 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]. /// Returns bool, which tells whether is given [id] is in [tetrioUsersToTrackTable].
@ -991,6 +1092,7 @@ class TetrioService extends DB {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
final deletedPlayer = await db.delete(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); final deletedPlayer = await db.delete(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]);
await db.delete(tetrioUsersTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]);
if (deletedPlayer != 1) { if (deletedPlayer != 1) {
throw CouldNotDeletePlayer(); throw CouldNotDeletePlayer();
} else { } else {
@ -999,71 +1101,30 @@ class TetrioService extends DB {
} }
} }
/// Saves state (which is [tetrioPlayer]) to the local database. Future<List<TetraLeague>> getStates(String userID, {int? season}) async {
Future<void> storeState(TetrioPlayer tetrioPlayer) async {
// if tetrio player doesn't have entry in database - just calling different function
List<TetrioPlayer> 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<String, dynamic> 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
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, List<Map> query = await db.query(tetrioLeagueTable, where: season != null ? '"id" LIKE ? AND "season" = ?' : '"id" LIKE ?', whereArgs: season != null ? ["${userID}%", season] : ["${userID}%"], orderBy: '"id" ASC');
where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); return [for (var entry in query) TetraLeague.fromJson(entry as Map<String, dynamic>, 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 /// Saves state (which is [TetraLeague]) to the local database.
Future<void> deleteState(TetrioPlayer tetrioPlayer) async { Future<void> storeState(TetraLeague league) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
List<TetrioPlayer> states = await getPlayer(tetrioPlayer.userId); List<Map> test = await db.query(tetrioLeagueTable, where: '"id" LIKE ? AND "gamesplayed" = ? AND "rd" = ?', whereArgs: ["${league.id}%", league.gamesPlayed, league.rd]);
// removing state from map that contain every state of each user if (test.isEmpty) {
states.removeWhere((element) => element.state == tetrioPlayer.state); await db.insert(tetrioLeagueTable, league.toJson());
// Making map of the states (without deleted one)
final Map<String, dynamic> statesJson = {};
for (var e in states) {
statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
} }
// Rewriting database entry with new json
await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)},
where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]);
_tetrioStreamController.add(_players);
} }
/// Returns list of all states of player with given [id] from database. Can return empty list if player /// Remove state, which has [dbID] from the local database
/// was not found. /// ([dbid] is a concatenation of player id and UINX milliseconds in hex)
Future<List<TetrioPlayer>> getPlayer(String id) async { Future<void> deleteState(String dbID) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
List<TetrioPlayer> states = []; int result = await db.delete(tetrioLeagueTable, where: "id = ?", whereArgs: [dbID]);
final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); if (result == 0) throw Exception("Failed to remove a row $dbID - it's probably not exist");
if (results.isEmpty) {
return states; // it empty
} else {
dynamic rawStates = results.first['jsonStates'] as String;
rawStates = json.decode(rawStates);
// recreating objects of states
rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String)));
// updating the stream
_players.removeWhere((key, value) => key == id);
_players.addEntries({states.last.userId: states.last.username}.entries);
_tetrioStreamController.add(_players);
return states;
}
} }
/// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user. /// 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 // more exceptions to god of exceptions
case 403: case 403:
throw TetrioForbidden(); throw TetrioForbidden();
case 404:
throw TetrioPlayerNotExist();
case 429: case 429:
throw TetrioTooManyRequests(); throw TetrioTooManyRequests();
case 418: case 418:
@ -1131,7 +1194,7 @@ class TetrioService extends DB {
var json = jsonDecode(response.body); var json = jsonDecode(response.body);
if (json['success']) { if (json['success']) {
// parse and count stats // parse and count stats
TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true)); TetrioPlayer player = TetrioPlayer.fromJson(json['data'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['_id'], json['data']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true));
_cache.store(player, json['cache']['cached_until']); _cache.store(player, json['cache']['cached_until']);
developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud");
return player; return player;
@ -1141,6 +1204,8 @@ class TetrioService extends DB {
} }
case 403: case 403:
throw TetrioForbidden(); throw TetrioForbidden();
case 404:
throw TetrioPlayerNotExist();
case 429: case 429:
throw TetrioTooManyRequests(); throw TetrioTooManyRequests();
case 418: case 418:
@ -1160,29 +1225,26 @@ class TetrioService extends DB {
} }
} }
/// Retrieves whole [tetrioUsersTable] and returns Map with [TetrioPlayer] objects of everyone in database /// Retrieves whole [tetrioUsersTable] and returns Map {id: nickname} of everyone in database
Future<Map<String, List<TetrioPlayer>>> getAllPlayers() async { Future<Map<String, String>> getAllPlayers() async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
final players = await db.query(tetrioUsersTable); final players = await db.query(tetrioUsersTable);
Map<String, List<TetrioPlayer>> data = {}; Map<String, String> data = {};
for (var entry in players){ for (var entry in players){
var test = json.decode(entry['jsonStates'] as String); data[entry[idCol] as String] = entry[nickCol] as String;
List<TetrioPlayer> 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);
} }
return data; return data;
} }
Future<void> fetchTracked() async { // Future<void> fetchTracked() async {
for (String userID in (await getAllPlayerToTrack())) { // for (String userID in (await getAllPlayerToTrack())) {
TetrioPlayer player = await fetchPlayer(userID); // TetrioPlayer player = await fetchPlayer(userID);
storeState(player); // storeState(player);
sleep(Durations.extralong4); // sleep(Durations.extralong4);
TetraLeagueAlphaStream matches = await fetchTLStream(userID); // TetraLeagueBetaStream matches = await fetchTLStream(userID);
saveTLMatchesFromStream(matches); // saveTLMatchesFromStream(matches);
sleep(Durations.extralong4); // sleep(Durations.extralong4);
} // }
} // }
} }

View File

@ -1,9 +1,13 @@
// ignore_for_file: curly_braces_in_flow_control_structures
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode);
final NumberFormat nonsecs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode); final NumberFormat 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); final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode);
/// Returns string, that represents time difference between [dateTime] and now /// Returns string, that represents time difference between [dateTime] and now
@ -69,6 +73,10 @@ String get40lTime(int microseconds){
return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); 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 /// Readable [a] - [b], without sign
String readableTimeDifference(Duration a, Duration b){ String readableTimeDifference(Duration a, Duration b){
Duration result = a - b; Duration result = a - b;
@ -77,5 +85,12 @@ String readableTimeDifference(Duration a, Duration b){
} }
String countdown(Duration difference){ String countdown(Duration difference){
return "${difference.inDays}:${nonsecs.format(difference.inHours%24)}:${nonsecs.format(difference.inMinutes%60)}:${secs.format(difference.inSeconds%60)}"; return "${difference.inDays}d ${nonsecs.format(difference.inHours%24)}h ${nonsecs.format(difference.inMinutes%60)}m ${secs.format(difference.inSeconds%60)}s";
}
String playtime(Duration difference){
if (difference.inHours > 0) {
return "${intf.format(difference.inHours)}h ${nonsecs.format(difference.inMinutes%60)}m";
} else if (difference.inMinutes > 0) return "${difference.inMinutes}m ${nonsecs.format(difference.inSeconds%60)}s";
else return "${secs.format(difference.inMilliseconds/1000)}s";
} }

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/singleplayer_record_view.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/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/finesse_thingy.dart';
import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/lineclears_thingy.dart';
import 'package:tetra_stats/widgets/list_tile_trailing_stats.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/text_timestamp.dart';
import 'package:tetra_stats/widgets/tl_thingy.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart';
import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart';
import 'package:tetra_stats/widgets/zenith_thingy.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
int _chartsIndex = 0; int _chartsIndex = 0;
int _season = currentSeason-1;
bool _gamesPlayedInsteadOfDateAndTime = false; bool _gamesPlayedInsteadOfDateAndTime = false;
late ZoomPanBehavior _zoomPanBehavior; late ZoomPanBehavior _zoomPanBehavior;
bool _smooth = false; bool _smooth = false;
@ -63,7 +66,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
Future<List> me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up Future<List> me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up
TetrioPlayersLeaderboard? everyone; TetrioPlayersLeaderboard? everyone;
PlayerLeaderboardPosition? meAmongEveryone; PlayerLeaderboardPosition? meAmongEveryone;
TetraLeagueAlpha? rankAverages; TetraLeague? rankAverages;
double? thatRankCutoff; double? thatRankCutoff;
double? nextRankCutoff; double? nextRankCutoff;
double? thatRankGlickoCutoff; double? thatRankGlickoCutoff;
@ -71,7 +74,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for
String _titleNickname = ""; String _titleNickname = "";
/// Each dropdown menu item contains list of dots for the graph /// Each dropdown menu item contains list of dots for the graph
List<DropdownMenuItem<List<_HistoryChartSpot>>> chartsData = []; /// chartsData[season-1][chart]
List<List<DropdownMenuItem<List<_HistoryChartSpot>>>> chartsData = [];
//var tableData = <TableRow>[]; //var tableData = <TableRow>[];
final bodyGlobalKey = GlobalKey(); final bodyGlobalKey = GlobalKey();
bool _showSearchBar = false; bool _showSearchBar = false;
@ -79,6 +83,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
bool _TLHistoryWasFetched = false; bool _TLHistoryWasFetched = false;
late TabController _tabController; late TabController _tabController;
late TabController _wideScreenTabController; late TabController _wideScreenTabController;
bool zenithEX = false;
String get title => "Tetra Stats: $_titleNickname"; String get title => "Tetra Stats: $_titleNickname";
@ -86,8 +91,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
void initState() { void initState() {
initDB(); initDB();
_scrollController = ScrollController(); _scrollController = ScrollController();
_tabController = TabController(length: 7, vsync: this); _tabController = TabController(length: 9, vsync: this);
_wideScreenTabController = TabController(length: 4, vsync: this); _wideScreenTabController = TabController(length: 5, vsync: this);
_zoomPanBehavior = ZoomPanBehavior( _zoomPanBehavior = ZoomPanBehavior(
enablePinching: true, enablePinching: true,
enableSelectionZooming: true, enableSelectionZooming: true,
@ -154,76 +159,80 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) await windowManager.setTitle(title); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) await windowManager.setTitle(title);
// Requesting Tetra League (alpha), records, news and top TR of player // Requesting Tetra League (alpha), records, news and top TR of player
late List<dynamic> requests; List<dynamic> requests;
late TetraLeagueAlphaStream tlStream; Summaries summaries = await teto.fetchSummaries(_searchFor);
late UserRecords records; late TetraLeagueBetaStream tlStream;
late News news; late News news;
late SingleplayerStream recent; // late SingleplayerStream recentSprint;
late SingleplayerStream sprint; // late SingleplayerStream recentBlitz;
late SingleplayerStream blitz; // late SingleplayerStream sprint;
// late SingleplayerStream blitz;
late SingleplayerStream recentZenith;
late SingleplayerStream recentZenithEX;
late TetrioPlayerFromLeaderboard? topOne; late TetrioPlayerFromLeaderboard? topOne;
late TopTr? topTR; // late TopTr? topTR;
requests = await Future.wait([ // all at once (7 requests to oskware lmao) requests = await Future.wait([
teto.fetchSummaries(_searchFor),
teto.fetchTLStream(_searchFor), teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor), teto.fetchNews(_searchFor),
teto.fetchSingleplayerStream(_searchFor, "any_userrecent"), teto.fetchStream(_searchFor, "zenith/recent"),
teto.fetchSingleplayerStream(_searchFor, "40l_userbest"), teto.fetchStream(_searchFor, "zenithex/recent"),
teto.fetchSingleplayerStream(_searchFor, "blitz_userbest"), teto.fetchCutoffsBeanserver(),
prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=><Map<String, double>>[]), (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null),
(me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null),
(me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR
]); ]);
tlStream = requests[0] as TetraLeagueAlphaStream; //prefs.getBool("showPositions") != true ? teto.fetchCutoffsBeanserver() : Future.delayed(Duration.zero, ()=><Map<String, double>>[]),
records = requests[1] as UserRecords;
//(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; news = requests[2] as News;
recent = requests[3] as SingleplayerStream; recentZenith = requests[3] as SingleplayerStream;
sprint = requests[4] as SingleplayerStream; recentZenithEX = requests[4] as SingleplayerStream;
blitz = requests[5] as SingleplayerStream; // recent = requests[3] as SingleplayerStream;
topOne = requests[7] as TetrioPlayerFromLeaderboard?; // sprint = requests[4] as SingleplayerStream;
topTR = requests[8] as TopTr?; // No TR - no Top TR // 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); meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId);
if (prefs.getBool("showPositions") == true){ if (prefs.getBool("showPositions") == true){
// Get tetra League leaderboard // Get tetra League leaderboard
everyone = teto.getCachedLeaderboard(); everyone = teto.getCachedLeaderboard();
everyone ??= await teto.fetchTLLeaderboard(); everyone ??= await teto.fetchTLLeaderboard();
if (meAmongEveryone == null){ if (meAmongEveryone == null && everyone!.leaderboard.isNotEmpty){
meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); meAmongEveryone = await compute(everyone!.getLeaderboardPosition, {me.userId: summaries.league});
if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!);
} }
} }
Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[6] as Cutoffs?)?.tr; Map<String, double>? cutoffs = (requests[5] as Cutoffs?)?.tr;
Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[6] as Cutoffs?)?.glicko; Map<String, double>? cutoffsGlicko = (requests[5] as Cutoffs?)?.glicko;
if (me.tlSeason1.gamesPlayed > 9) { if (summaries.league.gamesPlayed > 9) {
thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; thatRankCutoff = cutoffs?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank];
thatRankGlickoCutoff = cutoffsGlicko?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; thatRankGlickoCutoff = cutoffsGlicko?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank];
nextRankCutoff = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.rating??25000 : cutoffs?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; nextRankCutoff = (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 = (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; nextRankGlickoCutoff = (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 // Making list of Tetra League matches
List<TetraLeagueAlphaRecord> tlMatches = [];
bool isTracking = await teto.isPlayerTracking(me.userId); bool isTracking = await teto.isPlayerTracking(me.userId);
List<TetrioPlayer> states = []; List<List<TetraLeague>> states = await Future.wait<List<TetraLeague>>([
TetraLeagueAlpha? compareWith; teto.getStates(me.userId, season: 1), teto.getStates(me.userId, season: 2),
Set<TetraLeagueAlpha> uniqueTL = {}; ]);
tlMatches = tlStream.records;
List<TetraLeagueAlphaRecord> storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches List<TetraLeagueAlphaRecord> storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches
if (isTracking){ // if tracked - save data to local DB if (isTracking){ // if tracked - save data to local DB
await teto.storeState(me); await teto.storeState(summaries.league);
await teto.saveTLMatchesFromStream(tlStream); //await teto.saveTLMatchesFromStream(tlStream);
} }
TetraLeagueAlphaStream? oldMatches;
// building list of TL matches // building list of TL matches
if(fetchTLmatches) { if(fetchTLmatches) {
try{ try{
List<TetraLeagueAlphaRecord> oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor); oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor);
storedRecords.addAll(oldMatches); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.records.length))));
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.length))));
}on TetrioHistoryNotExist{ }on TetrioHistoryNotExist{
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches))); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches)));
}on P1nkl0bst3rForbidden { }on P1nkl0bst3rForbidden {
@ -236,17 +245,17 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
_TLHistoryWasFetched = true; _TLHistoryWasFetched = true;
} }
} }
if (storedRecords.isNotEmpty) _TLHistoryWasFetched = true; if (storedRecords.isNotEmpty) {
for (var match in storedRecords) { _TLHistoryWasFetched = true;
// add stored match to list only if it missing from retrived ones tlStream.addFromAlphaStream(storedRecords);
if (!tlMatches.contains(match)) tlMatches.add(match);
} }
tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list
if(a.timestamp.isBefore(b.timestamp)) return 1; // tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list
if(a.timestamp.isAtSameMomentAs(b.timestamp)) return 0; // if(a.ts.isBefore(b.ts)) return 1;
if(a.timestamp.isAfter(b.timestamp)) return -1; // if(a.ts.isAtSameMomentAs(b.ts)) return 0;
return 0; // if(a.ts.isAfter(b.ts)) return -1;
}); // return 0;
// });
// Handling history // Handling history
if(fetchHistory){ if(fetchHistory){
@ -264,37 +273,38 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
} }
} }
states.addAll(await teto.getPlayer(me.userId)); //states.addAll(await teto.getPlayer(me.userId));
for (var element in states) { // For graphs I need only unique entries // for (var element in states) { // For graphs I need only unique entries
if (uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1); // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!);
if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1); // if (uniqueTL.isEmpty) uniqueTL.add(summaries.league);
} // }
// Also i need previous Tetra League State for comparison if avaliable // Also i need previous Tetra League State for comparison if avaliable
if (uniqueTL.length >= 2){ TetraLeague? compareWith;
compareWith = uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2); if (states[1].length >= 2 || states[0].length >= 2){
chartsData = <DropdownMenuItem<List<_HistoryChartSpot>>>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid compareWith = states[1].length >= 2 ? states[1].elementAtOrNull(states.length - 2) : null;
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)), chartsData = [for (List<TetraLeague> s in states) <DropdownMenuItem<List<_HistoryChartSpot>>>[ // 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.glicko!)], child: const Text("Glicko")), 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 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 s) 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.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.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.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.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.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.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.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.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!.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!.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!.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!.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!.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!.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!.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!.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!.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!.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!.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!.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!.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!.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!.area)], child: Text(t.statCellNum.area.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 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 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 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 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 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 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 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 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 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 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 uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), 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{ }else{
compareWith = null; compareWith = null;
chartsData = []; chartsData = [];
@ -305,8 +315,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
changePlayer(me.userId); changePlayer(me.userId);
}); });
} }
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]; //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp];
} }
/// Triggers widgets rebuild /// Triggers widgets rebuild
@ -314,6 +324,10 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
setState(() {}); setState(() {});
} }
void toggleZenith(){
setState(() {zenithEX = !zenithEX;});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = Translations.of(context); final t = Translations.of(context);
@ -430,12 +444,15 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
tabs: bigScreen ? [ tabs: bigScreen ? [
Tab(text: t.tetraLeague,), Tab(text: t.tetraLeague,),
Tab(text: t.history), Tab(text: t.history),
Tab(text: t.quickPlay),
Tab(text: "${t.sprint} & ${t.blitz}"), Tab(text: "${t.sprint} & ${t.blitz}"),
Tab(text: t.other), Tab(text: t.other),
] : [ ] : [
Tab(text: t.tetraLeague), Tab(text: t.tetraLeague),
Tab(text: t.tlRecords), Tab(text: t.tlRecords),
Tab(text: t.history), Tab(text: t.history),
Tab(text: t.quickPlay),
Tab(text: "${t.quickPlay} ${t.recent}"),
Tab(text: t.sprint), Tab(text: t.sprint),
Tab(text: t.blitz), Tab(text: t.blitz),
Tab(text: t.recentRuns), Tab(text: t.recentRuns),
@ -455,54 +472,71 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
width: MediaQuery.of(context).size.width-450, width: MediaQuery.of(context).size.width-450,
constraints: const BoxConstraints(maxWidth: 1024), constraints: const BoxConstraints(maxWidth: 1024),
child: TLThingy( child: TLThingy(
tl: snapshot.data![0].tlSeason1, tl: snapshot.data![1].league,
userID: snapshot.data![0].userId, userID: snapshot.data![0].userId,
states: snapshot.data![2], states: snapshot.data![6],
topTR: snapshot.data![7]?.tr, //topTR: snapshot.data![7]?.tr,
lastMatchPlayed: snapshot.data![11], //lastMatchPlayed: snapshot.data![11],
bot: snapshot.data![0].role == "bot", bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon", guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff, thatRankCutoff: thatRankCutoff,
thatRankCutoffGlicko: thatRankGlickoCutoff, thatRankCutoffGlicko: thatRankGlickoCutoff,
thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null,
nextRankCutoff: nextRankCutoff, nextRankCutoff: nextRankCutoff,
nextRankCutoffGlicko: nextRankGlickoCutoff, nextRankCutoffGlicko: nextRankGlickoCutoff,
nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null, nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null,
averages: rankAverages, averages: rankAverages,
lbPositions: meAmongEveryone lbPositions: meAmongEveryone
), ),
), ),
SizedBox( SizedBox(
width: 450, width: 450,
child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true,) child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true)
), ),
],), ],),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0),
_TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, recent: snapshot.data![8], sprintStream: snapshot.data![9], blitzStream: snapshot.data![10]), Row(
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) 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( TLThingy(
tl: snapshot.data![0].tlSeason1, tl: snapshot.data![1].league,
userID: snapshot.data![0].userId, userID: snapshot.data![0].userId,
states: snapshot.data![2], states: const [], //snapshot.data![2],
topTR: snapshot.data![7]?.tr, //topTR: snapshot.data![7]?.tr,
//lastMatchPlayed: snapshot.data![11],
bot: snapshot.data![0].role == "bot", bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon", guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff, thatRankCutoff: thatRankCutoff,
thatRankCutoffGlicko: thatRankGlickoCutoff, thatRankCutoffGlicko: thatRankGlickoCutoff,
thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null,
nextRankCutoff: nextRankCutoff, nextRankCutoff: nextRankCutoff,
nextRankCutoffGlicko: nextRankGlickoCutoff, nextRankCutoffGlicko: nextRankGlickoCutoff,
nextRankTarget: (snapshot.data![0].tlSeason1.rank != "z" && snapshot.data![0].tlSeason1.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null, nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null,
averages: rankAverages, averages: rankAverages,
lbPositions: meAmongEveryone lbPositions: meAmongEveryone
), ),
_TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched), _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0),
SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![9]), SingleChildScrollView(child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)),
SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![10]), _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true),
_RecentSingleplayersThingy(snapshot.data![8]), SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")),
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6]) 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<MainView> with TickerProviderStateMixin {
break; break;
default: default:
errText = snapshot.error.toString(); errText = snapshot.error.toString();
subText = snapshot.stackTrace.toString();
} }
return Center(child: return Center(child:
Column( Column(
@ -693,7 +728,7 @@ class _NavDrawerState extends State<NavDrawer> {
class _TLRecords extends StatelessWidget { class _TLRecords extends StatelessWidget {
final String userID; final String userID;
final Function changePlayer; final Function changePlayer;
final List<TetraLeagueAlphaRecord> data; final List<BetaRecord> data;
final bool wasActiveInTL; final bool wasActiveInTL;
final bool oldMathcesHere; final bool oldMathcesHere;
final bool separateScrollController; final bool separateScrollController;
@ -732,7 +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( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@ -741,19 +776,76 @@ class _TLRecords extends StatelessWidget {
) )
), ),
child: ListTile( child: ListTile(
leading: Text("${data[index].endContext.firstWhere((element) => element.userId == userID).points} : ${data[index].endContext.firstWhere((element) => element.userId != userID).points}", leading: Text("${data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins} : ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins}",
style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)),
title: Text("vs. ${data[index].endContext.firstWhere((element) => element.userId != userID).username}"), title: Text("vs. ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).username}"),
subtitle: Text(timestamp(data[index].timestamp), style: const TextStyle(color: Colors.grey)), subtitle: Text(timestamp(data[index].ts), style: const TextStyle(color: Colors.grey)),
trailing: TrailingStats( trailing: TrailingStats(
data[index].endContext.firstWhere((element) => element.userId == userID).secondary, data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.apm,
data[index].endContext.firstWhere((element) => element.userId == userID).tertiary, data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.pps,
data[index].endContext.firstWhere((element) => element.userId == userID).extra, data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.vs,
data[index].endContext.firstWhere((element) => element.userId != userID).secondary, data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.apm,
data[index].endContext.firstWhere((element) => element.userId != userID).tertiary, data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.pps,
data[index].endContext.firstWhere((element) => element.userId != userID).extra data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.vs,
), ),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), onTap: () => 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{ class _History extends StatelessWidget{
final List<DropdownMenuItem<List<_HistoryChartSpot>>> chartsData; final List<List<DropdownMenuItem<List<_HistoryChartSpot>>>> chartsData;
final String userID; final String userID;
final Function update; final Function update;
final Function changePlayer; final Function changePlayer;
@ -784,8 +876,7 @@ class _History extends StatelessWidget{
)); ));
} }
bool bigScreen = MediaQuery.of(context).size.width > 768; bool bigScreen = MediaQuery.of(context).size.width > 768;
//List<_HistoryChartSpot> selectedGraph = _gamesPlayedInsteadOfDateAndTime ? chartsDataGamesPlayed[_chartsIndex].value! : chartsData[_chartsIndex].value!; List<_HistoryChartSpot> selectedGraph = chartsData[_season][_chartsIndex].value!;
List<_HistoryChartSpot> selectedGraph = chartsData[_chartsIndex].value!;
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: Column( child: Column(
@ -795,6 +886,20 @@ class _History extends StatelessWidget{
spacing: 20, spacing: 20,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ 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( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -814,10 +919,10 @@ class _History extends StatelessWidget{
children: [ children: [
const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))),
DropdownButton( DropdownButton(
items: chartsData, items: chartsData[_season],
value: chartsData[_chartsIndex].value, value: chartsData[_season][_chartsIndex].value,
onChanged: (value) { onChanged: (value) {
_chartsIndex = chartsData.indexWhere((element) => element.value == value); _chartsIndex = chartsData[_season].indexWhere((element) => element.value == value);
update(); update();
} }
), ),
@ -838,13 +943,13 @@ class _History extends StatelessWidget{
IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) 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()) 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[_chartsIndex].value!.length <= 1) Center(child: Column( else if (chartsData[_season][_chartsIndex].value!.length <= 1) Center(child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)),
if (wasActiveInTL) Text(t.errors.actionSuggestion), if (wasActiveInTL && _season == 0) Text(t.errors.actionSuggestion),
if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) 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) { Widget build(BuildContext context) {
late MapEntry closestAverageBlitz; late MapEntry closestAverageBlitz;
late bool blitzBetterThanClosestAverage; late bool blitzBetterThanClosestAverage;
bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.endContext.score > blitzAverages[rank]! : null; bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null;
late MapEntry closestAverageSprint; late MapEntry closestAverageSprint;
late bool sprintBetterThanClosestAverage; late bool sprintBetterThanClosestAverage;
bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.endContext.finalTime < sprintAverages[rank]! : null; bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && sprint != null) ? sprint!.stats.finalTime < sprintAverages[rank]! : null;
if (sprint != null) { if (sprint != null) {
closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.endContext.finalTime).abs() < (b -sprint!.endContext.finalTime).abs() ? a : b)); closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.stats.finalTime).abs() < (b -sprint!.stats.finalTime).abs() ? a : b));
sprintBetterThanClosestAverage = sprint!.endContext.finalTime < closestAverageSprint.value; sprintBetterThanClosestAverage = sprint!.stats.finalTime < closestAverageSprint.value;
} }
if (blitz != null){ if (blitz != null){
closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.endContext.score).abs() < (b -blitz!.endContext.score).abs() ? a : b)); closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.stats.score).abs() < (b -blitz!.stats.score).abs() ? a : b));
blitzBetterThanClosestAverage = blitz!.endContext.score > closestAverageBlitz.value; blitzBetterThanClosestAverage = blitz!.stats.score > closestAverageBlitz.value;
} }
return SingleChildScrollView(child: Padding( return SingleChildScrollView(child: Padding(
padding: const EdgeInsets.only(top: 20.0), padding: const EdgeInsets.only(top: 20.0),
@ -1047,23 +1152,23 @@ class _TwoRecordsThingy extends StatelessWidget {
children: [ children: [
Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
RichText(text: TextSpan( RichText(text: TextSpan(
text: sprint != null ? get40lTime(sprint!.endContext.finalTime.inMicroseconds) : "---", text: sprint != null ? get40lTime(sprint!.stats.finalTime.inMicroseconds) : "---",
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey),
//children: [TextSpan(text: get40lTime(record!.endContext.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] //children: [TextSpan(text: get40lTime(record!.stats.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
), ),
), ),
if (sprint != null) RichText(text: TextSpan( if (sprint != null) RichText(text: TextSpan(
text: "", text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [ children: [
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( if (rank != null && rank != "z" && 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 color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
)) ))
else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)), )),
if (sprint!.rank != null) TextSpan(text: "${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank!))), TextSpan(text: "${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank))),
if (sprint!.rank != null) const TextSpan(text: ""), const TextSpan(text: ""),
TextSpan(text: timestamp(sprint!.timestamp)), TextSpan(text: timestamp(sprint!.timestamp)),
] ]
), ),
@ -1076,14 +1181,14 @@ class _TwoRecordsThingy extends StatelessWidget {
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
spacing: 20, spacing: 20,
children: [ children: [
StatCellNum(playerStat: sprint!.endContext.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: sprint!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: sprint!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: sprint!.stats.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
], ],
), ),
if (sprint != null) FinesseThingy(sprint?.endContext.finesse, sprint?.endContext.finessePercentage), if (sprint != null) FinesseThingy(sprint?.stats.finesse, sprint?.stats.finessePercentage),
if (sprint != null) LineclearsThingy(sprint!.endContext.clears, sprint!.endContext.lines, sprint!.endContext.holds, sprint!.endContext.tSpins), if (sprint != null) LineclearsThingy(sprint!.stats.clears, sprint!.stats.lines, sprint!.stats.holds, sprint!.stats.tSpins),
if (sprint != null) Text("${sprint!.endContext.inputs} KP • ${f2.format(sprint!.endContext.kps)} KPS"), if (sprint != null) Text("${sprint!.stats.inputs} KP • ${f2.format(sprint!.stats.kps)} KPS"),
if (sprint != null) Wrap( if (sprint != null) Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
@ -1101,10 +1206,10 @@ class _TwoRecordsThingy extends StatelessWidget {
for (int i = 1; i < sprintStream.records.length; i++) ListTile( for (int i = 1; i < sprintStream.records.length; i++) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))),
leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ),
title: Text(get40lTime(sprintStream.records[i].endContext.finalTime.inMicroseconds), title: Text(get40lTime(sprintStream.records[i].stats.finalTime.inMicroseconds),
style: const TextStyle(fontSize: 18)), style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(sprintStream.records[i].endContext) trailing: SpTrailingStats(sprintStream.records[i], sprintStream.records[i].gamemode)
) )
], ],
), ),
@ -1128,7 +1233,7 @@ class _TwoRecordsThingy extends StatelessWidget {
text: "", text: "",
style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white),
children: [ children: [
TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.endContext.score) : "---"), TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.stats.score) : "---"),
//WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48)) //WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48))
] ]
), ),
@ -1139,15 +1244,15 @@ class _TwoRecordsThingy extends StatelessWidget {
text: "", text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [ children: [
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( if (rank != null && rank != "z" && 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 color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
)) ))
else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)), )),
TextSpan(text: timestamp(blitz!.timestamp)), TextSpan(text: timestamp(blitz!.timestamp)),
if (blitz!.rank != null) const TextSpan(text: ""), const TextSpan(text: ""),
if (blitz!.rank != null) TextSpan(text: "${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank!))), TextSpan(text: "${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank))),
] ]
), ),
), ),
@ -1162,14 +1267,14 @@ class _TwoRecordsThingy extends StatelessWidget {
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20, spacing: 20,
children: [ children: [
StatCellNum(playerStat: blitz!.endContext.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: blitz!.stats.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: blitz!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true) StatCellNum(playerStat: blitz!.stats.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true)
], ],
), ),
if (blitz != null) FinesseThingy(blitz?.endContext.finesse, blitz?.endContext.finessePercentage), if (blitz != null) FinesseThingy(blitz?.stats.finesse, blitz?.stats.finessePercentage),
if (blitz != null) LineclearsThingy(blitz!.endContext.clears, blitz!.endContext.lines, blitz!.endContext.holds, blitz!.endContext.tSpins), if (blitz != null) LineclearsThingy(blitz!.stats.clears, blitz!.stats.lines, blitz!.stats.holds, blitz!.stats.tSpins),
if (blitz != null) Text("${blitz!.endContext.piecesPlaced} P • ${blitz!.endContext.inputs} KP • ${f2.format(blitz!.endContext.kpp)} KPP • ${f2.format(blitz!.endContext.kps)} KPS"), if (blitz != null) Text("${blitz!.stats.piecesPlaced} P • ${blitz!.stats.inputs} KP • ${f2.format(blitz!.stats.kpp)} KPP • ${f2.format(blitz!.stats.kps)} KPS"),
if (blitz != null) Wrap( if (blitz != null) Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start, crossAxisAlignment: WrapCrossAlignment.start,
@ -1187,10 +1292,10 @@ class _TwoRecordsThingy extends StatelessWidget {
for (int i = 1; i < blitzStream.records.length; i++) ListTile( for (int i = 1; i < blitzStream.records.length; i++) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))),
leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ),
title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].endContext.score)} points", title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].stats.score)} points",
style: const TextStyle(fontSize: 18)), style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(blitzStream.records[i].endContext) trailing: SpTrailingStats(blitzStream.records[i], blitzStream.records[i].gamemode)
) )
], ],
), ),
@ -1217,7 +1322,6 @@ class _RecentSingleplayersThingy extends StatelessWidget {
child: RecentSingleplayerGames(recent: recent, hideTitle: true) child: RecentSingleplayerGames(recent: recent, hideTitle: true)
); );
} }
} }
class _OtherThingy extends StatelessWidget { class _OtherThingy extends StatelessWidget {
@ -1277,7 +1381,9 @@ class _OtherThingy extends StatelessWidget {
Map<String, String> gametypes = { Map<String, String> gametypes = {
"40l": t.sprint, "40l": t.sprint,
"blitz": t.blitz, "blitz": t.blitz,
"5mblast": "5,000,000 Blast" "5mblast": "5,000,000 Blast",
"zenith": "Quick Play",
"zenithex": "Quick Play Expert",
}; };
// Individuly handle each entry type // Individuly handle each entry type
@ -1306,7 +1412,16 @@ class _OtherThingy extends StatelessWidget {
children: [ children: [
TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.personalbestMiddle), TextSpan(text: t.newsParts.personalbestMiddle),
TextSpan(text: news.data["gametype"] == "blitz" ? NumberFormat.decimalPattern().format(news.data["result"]) : get40lTime((news.data["result"]*1000).floor()), style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: switch (news.data["gametype"]){
"blitz" => NumberFormat.decimalPattern().format(news.data["result"]),
"40l" => get40lTime((news.data["result"]*1000).floor()),
"5mblast" => get40lTime((news.data["result"]*1000).floor()),
"zenith" => "${f2.format(news.data["result"])} m.",
"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.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.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)), 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))
],),
)
], ],
), ),
), ),

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/tl_match_view.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
late String oldWindowTitle; late String oldWindowTitle;
@ -73,12 +72,6 @@ class MatchesState extends State<MatchesView> {
})); }));
}, },
), ),
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)))], : [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))],
); );

View File

@ -360,7 +360,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), Image.asset("res/tetrio_tl_alpha_ranks/${they[index].rank}.png", height: bigScreen ? 48 : 16),
], ],
), ),
@ -379,6 +379,8 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
child: ListView( child: ListView(
children: [ 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]["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]["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]["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), _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<RankView> with SingleTickerProviderStateMixin {
Text(t.averageValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Text(t.averageValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
Expanded( Expanded(
child: ListView(children: [ 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].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].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), _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<RankView> with SingleTickerProviderStateMixin {
child: ListView( child: ListView(
children: [ 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]["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]["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]["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), _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: [ children: [
Text(f.format(value), Text(f.format(value),
style: const TextStyle(fontSize: 22, height: 0.9)), 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 onTap: id.isNotEmpty

View File

@ -1,9 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.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:window_manager/window_manager.dart';
import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/main.dart' show teto;
@ -17,14 +19,9 @@ class RankAveragesView extends StatefulWidget {
late String oldWindowTitle; late String oldWindowTitle;
class RanksAverages extends State<RankAveragesView> { class RanksAverages extends State<RankAveragesView> {
Map<String, List<dynamic>> averages = {};
@override @override
void initState() { void initState() {
teto.fetchTLLeaderboard().then((value){
averages = value.averages;
setState(() {});
});
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value); windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.rankAveragesViewTitle}"); windowManager.setTitle("Tetra Stats: ${t.rankAveragesViewTitle}");
@ -46,29 +43,133 @@ class RanksAverages extends State<RankAveragesView> {
), ),
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: SafeArea( body: SafeArea(
child: averages.isEmpty ? const Center(child: Text('Fetching...')) : ListView.builder( child: FutureBuilder<CutoffsTetrio?>(future: teto.fetchCutoffsTetrio(), builder: (context, snapshot){
itemCount: averages.length, switch (snapshot.connectionState) {
itemBuilder: (context, index){ case ConnectionState.none:
List<String> keys = averages.keys.toList(); case ConnectionState.waiting:
return ListTile( case ConnectionState.active:
leading: Image.asset("res/tetrio_tl_alpha_ranks/${keys[index]}.png", height: 48), return const Center(child: CircularProgressIndicator(color: Colors.white));
title: Text(t.players(n: averages[keys[index]]?[1]["players"]), style: const TextStyle(fontFamily: "Eurostile Round Extended")), case ConnectionState.done:
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", if (snapshot.hasData){
style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey, fontSize: 13)), return Container(
trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: const TextStyle(fontSize: 28, fontFamily: "Eurostile Round")), alignment: Alignment.center,
onTap: (){ child: SingleChildScrollView(
if (averages[keys[index]]?[1]["players"] > 0) { scrollDirection: Axis.horizontal,
Navigator.push( child: Container(
context, alignment: Alignment.center,
MaterialPageRoute( width: 900,
builder: (context) => RankView(rank: averages[keys[index]]!), 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");
}
})
),
); );
} }
} }

View File

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

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.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/tl_thingy.dart';
import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart';
import 'package:window_manager/window_manager.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(); final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
class StateView extends StatefulWidget { class StateView extends StatefulWidget {
final TetrioPlayer state; final TetraLeague state;
const StateView({super.key, required this.state}); const StateView({super.key, required this.state});
@override @override
@ -28,7 +29,7 @@ class StateState extends State<StateView> {
_scrollController = ScrollController(); _scrollController = ScrollController();
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value); 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(); super.initState();
} }
@ -48,16 +49,13 @@ class StateState extends State<StateView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = Translations.of(context); final t = Translations.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(t.stateViewTitle(nickname: widget.state.username.toUpperCase(), date: dateFormat.format(widget.state.state))), title: Text("State from ${timestamp(widget.state.timestamp)}"),
), ),
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: SafeArea( body: SafeArea(
child: NestedScrollView( child: TLThingy(tl: widget.state, userID: widget.state.id, states: [])
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 [],))));
} }
} }

View File

@ -1,18 +1,19 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show teto; 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/mathes_view.dart';
import 'package:tetra_stats/views/state_view.dart'; import 'package:tetra_stats/views/state_view.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class StatesView extends StatefulWidget { class StatesView extends StatefulWidget {
final List<TetrioPlayer> states; final String nickname;
const StatesView({super.key, required this.states}); final String id;
const StatesView({required this.nickname, required this.id, super.key});
@override @override
State<StatefulWidget> createState() => StatesState(); State<StatefulWidget> createState() => StatesState();
@ -25,7 +26,7 @@ class StatesState extends State<StatesView> {
void initState() { void initState() {
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value); 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(); super.initState();
} }
@ -41,45 +42,78 @@ class StatesState extends State<StatesView> {
final t = Translations.of(context); final t = Translations.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( 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: [ actions: [
IconButton( IconButton(
onPressed: (){ onPressed: (){
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MatchesView(userID: widget.states.first.userId, username: widget.states.first.username), builder: (context) => MatchesView(userID: widget.id, username: widget.nickname),
), ),
); );
}, icon: const Icon(Icons.list), tooltip: t.viewAllMatches) }, icon: const Icon(Icons.list), tooltip: t.viewAllMatches)
], ],
), ),
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: SafeArea( body: SafeArea(
child: ListView.builder( child: FutureBuilder<List<TetraLeague>>(future: teto.getStates(widget.id), builder: (context, snapshot) {
itemCount: widget.states.length, switch (snapshot.connectionState) {
itemBuilder: (context, index) { case ConnectionState.none:
return ListTile( case ConnectionState.waiting:
title: Text(timestamp(widget.states[index].state)), case ConnectionState.active:
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))), return const Center(child: CircularProgressIndicator(color: Colors.white));
trailing: IconButton( case ConnectionState.done:
icon: const Icon(Icons.delete_forever), if (snapshot.hasData) {
onPressed: () { return ListView.builder(
DateTime nn = widget.states[index].state; itemCount: snapshot.data!.length,
teto.deleteState(widget.states[index]).then((value) => setState(() { prototypeItem: ListTile(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn))))); title: Text(""),
})); subtitle: Text("", style: TextStyle(color: Colors.grey)),
}, trailing: IconButton(icon: const Icon(Icons.delete_forever), onPressed: (){}),
), ),
onTap: () { itemBuilder: (context, index) {
Navigator.push( return ListTile(
context, title: Text(timestamp(snapshot.data![index].timestamp)),
MaterialPageRoute( subtitle: Text(
builder: (context) => StateView(state: widget.states[index]), 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));
}
)));}
} }
}

View File

@ -4,13 +4,13 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.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/main_view.dart';
import 'package:tetra_stats/views/rank_averages_view.dart'; import 'package:tetra_stats/views/rank_averages_view.dart';
import 'package:tetra_stats/views/ranks_averages_view.dart'; import 'package:tetra_stats/views/ranks_averages_view.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
final TetrioService _teto = TetrioService();
List<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; List<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
Stats _sortBy = Stats.tr; Stats _sortBy = Stats.tr;
bool reversed = false; bool reversed = false;
@ -64,148 +64,155 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
), ),
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: SafeArea( body: SafeArea(
child: FutureBuilder( child: FutureBuilder(
future: _teto.fetchTLLeaderboard(), future: teto.fetchTLLeaderboard(),
builder: (context, snapshot) { builder: (context, snapshot) {
switch (snapshot.connectionState) { switch (snapshot.connectionState) {
case ConnectionState.none: case ConnectionState.none:
case ConnectionState.waiting: case ConnectionState.waiting:
case ConnectionState.active: case ConnectionState.active:
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
case ConnectionState.done: case ConnectionState.done:
final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); if (snapshot.hasData){
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers!.length)}"); final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country);
bool bigScreen = MediaQuery.of(context).size.width > 768; if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers != null ? allPlayers.length : 0)}");
return NestedScrollView( bool bigScreen = MediaQuery.of(context).size.width > 768;
headerSliverBuilder: (context, value) { return NestedScrollView(
String howManyPlayers(int numberOfPlayers) => Intl.plural( headerSliverBuilder: (context, value) {
numberOfPlayers, return [
zero: t.lbViewZeroEntrys, SliverToBoxAdapter(
one: t.lbViewOneEntry, child: Padding(
other: t.lbViewManyEntrys(numberOfPlayers: t.players(n: numberOfPlayers)), padding: const EdgeInsets.only(left: 16),
name: 'howManyPeople', child: Wrap(
args: [numberOfPlayers], direction: Axis.horizontal,
desc: 'Description of how many people are seen in a place.', alignment: WrapAlignment.spaceBetween,
examples: const {'numberOfPeople': 3}, children: [
); Text(
return [ "${t.players(n: allPlayers.length)}${t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))}",
SliverToBoxAdapter( style: const TextStyle(color: Colors.white, fontSize: 25),
child: Padding( ),
padding: const EdgeInsets.only(left: 16), TextButton(onPressed: (){
child: Wrap( Navigator.push(
direction: Axis.horizontal, context,
alignment: WrapAlignment.spaceBetween, MaterialPageRoute(
children: [ builder: (context) => RankView(rank: snapshot.data!.getAverageOfRank("")),
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..."),
), ),
itemBuilder: (context, index) { );
return ListTile( }, child: Text(t.everyoneAverages,
leading: Text( style: const TextStyle(fontSize: 25)))
(index+1).toString(), ],)
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9) )),
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, Row(
trailing: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.baseline,
children: [ textBaseline: TextBaseline.alphabetic,
Text("${f2.format(allPlayers[index].rating)} TR", style: const TextStyle(fontSize: 28)), children: [
Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36), Text("${t.country}: ",
], style: const TextStyle(color: Colors.white, fontSize: 25)),
), DropdownButton(items: _itemCountries, value: _country, onChanged: ((value) {
onTap: () { _country = value;
Navigator.push( setState(() {});
context, }),),
MaterialPageRoute( ],
builder: (context) => MainView(player: allPlayers[index].userId), ),
maintainState: false, ],
), ),
); ),),
}, 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");
}
})),
); );
} }
} }

View File

@ -2,13 +2,11 @@
import 'dart:io'; import 'dart:io';
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; 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/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/list_tile_trailing_stats.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/widgets/vs_graphs.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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -30,7 +28,7 @@ Duration framesToTime(int frames){
} }
class TlMatchResultView extends StatefulWidget { class TlMatchResultView extends StatefulWidget {
final TetraLeagueAlphaRecord record; final BetaRecord record;
final String initPlayerId; final String initPlayerId;
const TlMatchResultView({super.key, required this.record, required this.initPlayerId}); const TlMatchResultView({super.key, required this.record, required this.initPlayerId});
@ -40,15 +38,65 @@ class TlMatchResultView extends StatefulWidget {
class TlMatchResultState extends State<TlMatchResultView> { class TlMatchResultState extends State<TlMatchResultView> {
late Future<ReplayData?> replayData; late Future<ReplayData?> replayData;
late Duration time;
late String readableTime;
late String reason;
Duration totalTime = const Duration();
List<Duration> roundLengths = [];
List<BetaLeagueStats> timeWeightedStats = [];
late bool initPlayerWon;
@override @override
void initState(){ void initState(){
rounds = [DropdownMenuItem(value: -1, child: Text(t.match))]; 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)))]); rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]);
replayData = teto.analyzeReplay(widget.record.replayId, widget.record.replayAvalable); 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<double> apmMultipliedByWeights = [0, 0];
List<double> ppsMultipliedByWeights= [0, 0];
List<double> 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){ if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value); 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(); super.initState();
} }
@ -62,45 +110,17 @@ class TlMatchResultState extends State<TlMatchResultView> {
Widget buildComparison(double width, bool showMobileSelector){ Widget buildComparison(double width, bool showMobileSelector){
bool bigScreen = width >= 768; 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( return SizedBox(
width: width, width: width,
child: FutureBuilder(future: replayData, builder: (context, snapshot){ child: NestedScrollView(
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(
headerSliverBuilder: (context, value) { headerSliverBuilder: (context, value) {
return [ return [
SliverToBoxAdapter( SliverToBoxAdapter(
@ -117,15 +137,15 @@ class TlMatchResultState extends State<TlMatchResultView> {
colors: const [Colors.green, Colors.transparent], colors: const [Colors.green, Colors.transparent],
begin: Alignment.bottomCenter, begin: Alignment.bottomCenter,
end: Alignment.topCenter, 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( child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(children: [ 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", fontFamily: "Eurostile Round Extended",
fontSize: 28) : const TextStyle()), 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", fontFamily: "Eurostile Round Extended",
fontSize: 42)) fontSize: 42))
]), ]),
@ -143,15 +163,15 @@ class TlMatchResultState extends State<TlMatchResultView> {
colors: const [Colors.red, Colors.transparent], colors: const [Colors.red, Colors.transparent],
begin: Alignment.bottomCenter, begin: Alignment.bottomCenter,
end: Alignment.topCenter, 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( child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(children: [ 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", fontFamily: "Eurostile Round Extended",
fontSize: 28) : const TextStyle()), 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", fontFamily: "Eurostile Round Extended",
fontSize: 42)) fontSize: 42))
]), ]),
@ -179,10 +199,10 @@ class TlMatchResultState extends State<TlMatchResultView> {
), ),
), ),
), ),
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)), 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( const SliverToBoxAdapter(
child: Divider(), child: Divider(),
) )
@ -194,106 +214,37 @@ class TlMatchResultState extends State<TlMatchResultView> {
children: [ children: [
CompareThingy( CompareThingy(
label: "APM", label: "APM",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : greenSide: roundSelector == -2 ? timeWeightedStats[0].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.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : redSide: roundSelector == -2 ? timeWeightedStats[1].apm :
roundSelector == -1 ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector], roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
fractionDigits: 2, fractionDigits: 2,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "PPS", label: "PPS",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps: greenSide: roundSelector == -2 ? timeWeightedStats[0].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.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : redSide: roundSelector == -2 ? timeWeightedStats[1].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 == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
fractionDigits: 2, fractionDigits: 2,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "VS", label: "VS",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : greenSide: roundSelector == -2 ? timeWeightedStats[0].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.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : redSide: roundSelector == -2 ? timeWeightedStats[1].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 == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs,
fractionDigits: 2, fractionDigits: 2,
higherIsBetter: true, higherIsBetter: true,
), ),
if (snapshot.hasData) Column(children: [ 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,
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs, redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageSent,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs, label: "Sent", higherIsBetter: true),
label: "Inputs", 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,
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced, redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageReceived,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced, label: "Received", higherIsBetter: true), const Divider(),
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(),
Column( Column(
children: [ children: [
Padding( Padding(
@ -305,180 +256,179 @@ class TlMatchResultState extends State<TlMatchResultView> {
), ),
CompareThingy( CompareThingy(
label: "APP", label: "APP",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.app : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.app : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "VS/APM", label: "VS/APM",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.vsapm : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.vsapm : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "DS/S", label: "DS/S",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dss : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dss : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "DS/P", label: "DS/P",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dsp : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dsp : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "APP + DS/P", label: "APP + DS/P",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.appdsp : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.appdsp : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "),
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.cheese : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.cheese : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 2,
higherIsBetter: false, higherIsBetter: false,
), ),
CompareThingy( CompareThingy(
label: "Gb Eff.", label: "Gb Eff.",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.gbe : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.gbe : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "wAPP", label: "wAPP",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.nyaapp : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.nyaapp : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "Area", label: "Area",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.area : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.area : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 2,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: t.statCellNum.estOfTRShort, label: t.statCellNum.estOfTRShort,
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].estTr.esttr : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].estTr.esttr : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 2,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "Opener", label: "Opener",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.opener : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.opener : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "Plonk", label: "Plonk",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.plonk : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.plonk : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "Stride", label: "Stride",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.stride : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.stride : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
CompareThingy( CompareThingy(
label: "Inf. DS", label: "Inf. DS",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.infds : greenSide: roundSelector == -2 ? timeWeightedStats[0].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, 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 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.infds : redSide: roundSelector == -2 ? timeWeightedStats[1].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, 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, fractionDigits: 3,
higherIsBetter: true, higherIsBetter: true,
), ),
VsGraphs( 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 ? 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 && 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 ? 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 && 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 ? 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 && 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 ? 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 && 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 ? 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 && 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 ? 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 && 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 ? 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 && 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 ? 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 && 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 ? 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 && 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[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) const Divider(),
if (widget.record.ownId != widget.record.replayId) Column( // if (widget.record.ownId != widget.record.replayId) Column(
children: [ // children: [
Padding( // Padding(
padding: const EdgeInsets.only(bottom: 16), // padding: const EdgeInsets.only(bottom: 16),
child: Text("Handling", // child: Text("Handling",
style: TextStyle( // style: TextStyle(
fontFamily: "Eurostile Round Extended", // fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)), // fontSize: bigScreen ? 42 : 28)),
), // ),
CompareThingy( // CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das,
redSide: 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", // label: "DAS", fractionDigits: 1, postfix: "F",
higherIsBetter: false), // higherIsBetter: false),
CompareThingy( // CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr,
redSide: 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", // label: "ARR", fractionDigits: 1, postfix: "F",
higherIsBetter: false), // higherIsBetter: false),
CompareThingy( // CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf,
redSide: 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", // label: "SDF", prefix: "x",
higherIsBetter: true), // higherIsBetter: true),
CompareBoolThingy( // CompareBoolThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, // greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock,
redSide: 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", // label: "Safe HD",
trueIsBetter: true) // trueIsBetter: true)
], // ],
) // )
], ],
) )
); ])),
}),
); );
} }
@ -494,76 +444,34 @@ class TlMatchResultState extends State<TlMatchResultView> {
Wrap( Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
children: [ children: [
FutureBuilder(future: replayData, builder: (context, snapshot) { Column(
switch(snapshot.connectionState){ crossAxisAlignment: CrossAxisAlignment.start,
case ConnectionState.none: children: [
case ConnectionState.waiting: Text(t.matchLength),
case ConnectionState.active: RichText(
return const CircularProgressIndicator(); text: !totalTime.isNegative ? TextSpan(
case ConnectionState.done: text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}",
if (!snapshot.hasError){ style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white),
var time = framesToTime(snapshot.data!.totalLength); children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(totalTime.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
return Column( ) : const TextSpan(
crossAxisAlignment: CrossAxisAlignment.start, text: "-:--",
children: [ style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.grey),
Text(t.matchLength), children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
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), if (widget.record.id != widget.record.replayID) Column(
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(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text(t.numberOfRounds), Text(t.numberOfRounds),
RichText( RichText(
text: TextSpan( text: TextSpan(
text: widget.record.endContext.first.secondaryTracking.isNotEmpty ? widget.record.endContext.first.secondaryTracking.length.toString() : "---", text: widget.record.results.rounds.length.toString(),
style: TextStyle( style: const TextStyle(
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.w500, 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<TlMatchResultView> {
OverflowBar( OverflowBar(
alignment: MainAxisAlignment.spaceEvenly, alignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[ children: <Widget>[
TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null, TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
onPressed: () { onPressed: () {
roundSelector = -1; roundSelector = -1;
setState(() {}); setState(() {});
}, child: Text(t.matchStats)), }, 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 ? () { onPressed: timeWeightedStatsAvaliable ? () {
roundSelector = -2; roundSelector = -2;
setState(() {}); setState(() {});
} : null, child: Text(t.timeWeightedmatchStats)) , } : 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) { itemBuilder: (BuildContext context, int index) {
return FutureBuilder(future: replayData, builder: (context, snapshot) { var accentColor = widget.record.results.rounds[index][0].id == widget.initPlayerId ? Colors.green : Colors.red;
switch(snapshot.connectionState){ var bgColor = roundSelector == index ? Colors.grey.shade900 : Colors.transparent;
case ConnectionState.none: var time = roundLengths[index];
case ConnectionState.waiting: return Container(
case ConnectionState.active: decoration: BoxDecoration(
return const LinearProgressIndicator(); gradient: LinearGradient(
case ConnectionState.done: stops: const [0, 0.05],
if (!snapshot.hasError){ colors: [accentColor, bgColor]
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; child: ListTile(
return Container( leading:RichText(
decoration: BoxDecoration( text: !time.isNegative ? TextSpan(
gradient: LinearGradient( text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}",
stops: const [0, 0.05], style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.white),
colors: [accentColor, bgColor] 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: "-:--",
child: ListTile( style: TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.grey),
leading:RichText( children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
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), title: Text(widget.record.results.rounds[index][0].username, textAlign: TextAlign.center),
children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] 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,
title: Text(snapshot.data!.roundWinners[index][1], textAlign: TextAlign.center), widget.record.results.rounds[index].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
trailing: TrailingStats( widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[index], widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[index], widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.vs
widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[index], ),
widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[index], onTap:(){
widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[index], roundSelector = index;
widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[index] setState(() {});
), },
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(() {});
},
),
);
}
}
}
);
}) })
), ),
), ),
@ -707,10 +569,10 @@ class TlMatchResultState extends State<TlMatchResultView> {
final t = Translations.of(context); final t = Translations.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( 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: [ actions: [
PopupMenuButton( PopupMenuButton(
enabled: widget.record.replayAvalable, enabled: widget.record.gamemode == "league",
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem( PopupMenuItem(
value: 1, value: 1,
@ -724,10 +586,10 @@ class TlMatchResultState extends State<TlMatchResultView> {
onSelected: (value) async { onSelected: (value) async {
switch (value) { switch (value) {
case 1: 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; break;
case 2: 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; break;
default: default:
} }

View File

@ -78,7 +78,7 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
case ConnectionState.active: case ConnectionState.active:
return const Center(child: CircularProgressIndicator(color: Colors.white)); return const Center(child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.done: case ConnectionState.done:
final allPlayers = (snapshot.data != null) ? snapshot.data as Map<String, List<TetrioPlayer>> : <String, List<TetrioPlayer>>{}; final allPlayers = (snapshot.data != null) ? snapshot.data as Map<String, String> : <String, String>{};
List<String> keys = allPlayers.keys.toList(); List<String> keys = allPlayers.keys.toList();
return NestedScrollView( return NestedScrollView(
headerSliverBuilder: (context, value) { headerSliverBuilder: (context, value) {
@ -105,29 +105,29 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
]; ];
}, },
body: ListView.builder( body: ListView.builder(
itemCount: allPlayers.length, itemCount: allPlayers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return ListTile( print(index);
title: Text(t.trackedPlayersEntry(nickname: allPlayers[keys[index]]!.last.username, numberOfStates: allPlayers[keys[index]]!.length)), return ListTile(
subtitle: Text(t.trackedPlayersDescription(firstStateDate: timestamp(allPlayers[keys[index]]!.first.state), lastStateDate: timestamp(allPlayers[keys[index]]!.last.state))), title: Text(allPlayers[keys[index]]??"No nickname (huh?)"),
trailing: IconButton( subtitle: Text(keys[index], style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
icon: const Icon(Icons.delete_forever), trailing: IconButton(
onPressed: () { icon: const Icon(Icons.delete_forever),
String nn = allPlayers[keys[index]]!.last.username; onPressed: () {
setState(() {teto.deletePlayer(keys[index]);}); setState(() {teto.deletePlayer(keys[index]);});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: nn)))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: allPlayers[keys[index]]??"No nickname (huh?)"))));
},
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StatesView(states: allPlayers[keys[index]]!),
),
);
}, },
); ),
})); onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StatesView(nickname: allPlayers[keys[index]]!, id: keys[index]),
),
);
},
);
}));
} }
})), })),
); );

View File

@ -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),
),
)
),
);
}
}

View File

@ -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 'dart:math';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
@ -196,7 +198,7 @@ class MyRadarChartPainter extends RadarChartPainter{
} }
class MyRadarChartLeaf extends RadarChartLeaf{ class MyRadarChartLeaf extends RadarChartLeaf{
MyRadarChartLeaf({required super.data, required super.targetData}); const MyRadarChartLeaf({super.key, required super.data, required super.targetData});
@override @override
RenderRadarChart createRenderObject(BuildContext context) => MyRenderRadarChart( RenderRadarChart createRenderObject(BuildContext context) => MyRenderRadarChart(

View File

@ -7,8 +7,9 @@ class LineclearsThingy extends StatelessWidget{
final int lines; final int lines;
final int holds; final int holds;
final int tSpins; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -21,6 +22,7 @@ class LineclearsThingy extends StatelessWidget{
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(t.numOfGameActions.lineClears(n: lines), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), 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("Quads"), Text(clears.quads.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Triples"), Text(clears.triples.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())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Doubles"), Text(clears.doubles.toString())]),
@ -36,10 +38,14 @@ class LineclearsThingy extends StatelessWidget{
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(t.numOfGameActions.tspinsTotal(n: tSpins), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), 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 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 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 singles"), Text(clears.tSpinSingles.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins zeros"), Text(clears.tSpinZeros.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 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 singles"), Text(clears.tSpinMiniSingles.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]),

View File

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

View File

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

View File

@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
class SpTrailingStats extends StatelessWidget{ 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -14,12 +16,28 @@ class SpTrailingStats extends StatelessWidget{
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text("${endContext.piecesPlaced} P, ${f2.format(endContext.pps)} PPS", style: style, textAlign: TextAlign.right), Text(switch(gamemode){
Text("${intf.format(endContext.finessePercentage*100)}% F, ${endContext.finesse?.faults} FF", style: style, textAlign: TextAlign.right), "40l" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS",
Text(switch(endContext.gameType){ "blitz" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS",
"40l" => "${f2.format(endContext.kps)} KPS, ${f2.format(endContext.kpp)} KPP", "5mblast" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS",
"blitz" => "${intf.format(endContext.spp)} SPP, lvl ${endContext.level}", "zenith" => "${f2.format(record.aggregateStats.apm)} APM, ${f2.format(record.aggregateStats.pps)} PPS",
"5mblast" => "${intf.format(endContext.spp)} SPP, ${endContext.lines} L", "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" String() => "huh"
}, style: style, textAlign: TextAlign.right) }, style: style, textAlign: TextAlign.right)
], ],

View File

@ -11,7 +11,7 @@ class StatCellNum extends StatelessWidget {
required this.playerStat, required this.playerStat,
required this.playerStatLabel, required this.playerStatLabel,
required this.isScreenBig, required this.isScreenBig,
this.smallDecimal = true, this.smallDecimal = false,
this.alertWidgets, this.alertWidgets,
this.fractionDigits, this.fractionDigits,
this.oldPlayerStat, this.oldPlayerStat,
@ -52,7 +52,7 @@ class StatCellNum extends StatelessWidget {
RichText( RichText(
text: TextSpan(text: splited[0], text: TextSpan(text: splited[0],
children: [ 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( style: TextStyle(
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
@ -88,25 +88,25 @@ class StatCellNum extends StatelessWidget {
: TextButton( : TextButton(
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) => AlertDialog( builder: (BuildContext context) => AlertDialog(
title: Text(alertTitle??playerStatLabel.replaceAll(RegExp(r'\n'), " "), title: Text(alertTitle??playerStatLabel.replaceAll(RegExp(r'\n'), " "),
style: const TextStyle( style: const TextStyle(
fontFamily: "Eurostile Round Extended")), fontFamily: "Eurostile Round Extended")),
content: SingleChildScrollView( content: SingleChildScrollView(
child: ListBody(children: alertWidgets!), child: ListBody(children: alertWidgets!),
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text(okText??"OK"), child: Text(okText??"OK"),
onPressed: () {Navigator.of(context).pop();} onPressed: () {Navigator.of(context).pop();}
) )
], ],
) )
); );
}, },
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero)), padding: WidgetStateProperty.all(EdgeInsets.zero)),
child: Text( child: Text(
playerStatLabel, playerStatLabel,
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@ -9,9 +9,7 @@ import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
class TLProgress extends StatelessWidget{ class TLProgress extends StatelessWidget{
final TetraLeagueAlpha tlData; final TetraLeague tlData;
final String? nextRank;
final String? previousRank;
final double? nextRankTRcutoff; final double? nextRankTRcutoff;
final double? previousRankTRcutoff; final double? previousRankTRcutoff;
final double? nextRankGlickoCutoff; final double? nextRankGlickoCutoff;
@ -19,7 +17,7 @@ class TLProgress extends StatelessWidget{
final double? nextRankTRcutoffTarget; final double? nextRankTRcutoffTarget;
final double? previousRankTRcutoffTarget; 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(){ double getBarPosition(){
return min(max(0, 1 - (tlData.standing - tlData.nextAt)/(tlData.prevAt - tlData.nextAt)), 1); return min(max(0, 1 - (tlData.standing - tlData.nextAt)/(tlData.prevAt - tlData.nextAt)), 1);
@ -31,65 +29,57 @@ class TLProgress extends StatelessWidget{
@override @override
Widget build(BuildContext context) { 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!; final glickoForWin = rate(tlData.glicko!, tlData.rd!, 0.06, [[tlData.glicko!, tlData.rd!, 1]], {})[0]-tlData.glicko!;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox( Row(
width: MediaQuery.of(context).size.width, children: [
height: 48, RichText(
child: Stack( textAlign: TextAlign.left,
alignment: AlignmentDirectional.bottomCenter, text: TextSpan(
fit: StackFit.expand, style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12),
children: [ children: [
Positioned(left: 0, if (tlData.prevAt > 0) TextSpan(text: "${f0.format(tlData.prevAt)}"),
child: RichText( if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"),
textAlign: TextAlign.left, if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.tr)}) TR"),
text: TextSpan( if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"),
style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), 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))
children: [ ]
if (tlData.prevAt > 0) TextSpan(text: "${f0.format(tlData.prevAt)}"), )
if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"),
if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"),
if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"),
if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null))
]
)
),
), ),
Positioned(right: 0, const Spacer(),
child: RichText( RichText(
textAlign: TextAlign.right, textAlign: TextAlign.right,
text: TextSpan( text: TextSpan(
style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12),
children: [ children: [
if (tlData.nextAt > 0) TextSpan(text: "${f0.format(tlData.nextAt)}"), if (tlData.nextAt > 0) TextSpan(text: "${f0.format(tlData.nextAt)}"),
if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"),
if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.tr)}) TR"),
if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"),
if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x+" && tlData.rank != "z") || tlData.percentileRank != "x+"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x+")) ? Colors.greenAccent : null))
] ]
) )
),
) )
],), ],
), ),
SfLinearGauge( SfLinearGauge(
minimum: 0, minimum: 0,
maximum: 1, maximum: 1,
interval: 1, interval: 1,
ranges: [ 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), 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 (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) if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside)
], ],
markerPointers: [ markerPointers: [
LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), 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.rating)! : getBarPosition(), child: Text("${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: const TextStyle(fontSize: 14),)) 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, isMirrored: true,
showTicks: true, showTicks: true,

View File

@ -1,4 +1,3 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -11,8 +10,8 @@ var fDiff = NumberFormat("+#,###.####;-#,###.####");
class TLRatingThingy extends StatelessWidget{ class TLRatingThingy extends StatelessWidget{
final String userID; final String userID;
final TetraLeagueAlpha tlData; final TetraLeague tlData;
final TetraLeagueAlpha? oldTl; final TetraLeague? oldTl;
final double? topTR; final double? topTR;
final DateTime? lastMatchPlayed; final DateTime? lastMatchPlayed;
@ -23,13 +22,13 @@ class TLRatingThingy extends StatelessWidget{
bool oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; bool oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true;
bool bigScreen = MediaQuery.of(context).size.width >= 768; bool bigScreen = MediaQuery.of(context).size.width >= 768;
String decimalSeparator = f4.symbols.DECIMAL_SEP; String decimalSeparator = f4.symbols.DECIMAL_SEP;
List<String> formatedTR = f4.format(tlData.rating).split(decimalSeparator); List<String> formatedTR = f4.format(tlData.tr).split(decimalSeparator);
List<String> formatedGlicko = f4.format(tlData.glicko).split(decimalSeparator); List<String> formatedGlicko = tlData.glicko != null ? f4.format(tlData.glicko).split(decimalSeparator) : ["---","--"];
List<String> formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); List<String> formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator);
DateTime now = DateTime.now(); //DateTime now = DateTime.now();
bool beforeS1end = now.isBefore(seasonEnd); //bool beforeS1end = now.isBefore(seasonEnd);
int daysLeft = seasonEnd.difference(now).inDays; //int daysLeft = seasonEnd.difference(now).inDays;
int safeRD = min(100, (100 + ((tlData.rd! >= 100 && tlData.decaying) ? 7 : max(0, 7 - (lastMatchPlayed != null ? now.difference(lastMatchPlayed!).inDays : 7))) - daysLeft).toInt()); //int safeRD = min(100, (100 + ((tlData.rd! >= 100 && tlData.decaying) ? 7 : max(0, 7 - (lastMatchPlayed != null ? now.difference(lastMatchPlayed!).inDays : 7))) - daysLeft).toInt());
return Wrap( return Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
alignment: WrapAlignment.spaceAround, alignment: WrapAlignment.spaceAround,
@ -44,7 +43,7 @@ class TLRatingThingy extends StatelessWidget{
RichText( RichText(
text: TextSpan( text: TextSpan(
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white), 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 => [ 1 => [
TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (formatedGlicko.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedGlicko[1]), 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]), 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: " 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( if (oldTl != null) Text(
switch(prefs.getInt("ratingMode")){ switch(prefs.getInt("ratingMode")){
1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko",
2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", 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, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: tlData.rating - oldTl!.rating < 0 ? color: tlData.tr - oldTl!.tr < 0 ?
Colors.red : Colors.red :
Colors.green Colors.green
), ),
), ),
Column( if (tlData.gamesPlayed > 9) Column(
children: [ children: [
RichText( RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -84,14 +83,14 @@ class TLRatingThingy extends StatelessWidget{
text: TextSpan( text: TextSpan(
style: DefaultTextStyle.of(context).style, style: DefaultTextStyle.of(context).style,
children: [ 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") const TextSpan(text: ""),
if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"), if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"),
if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"), 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), TextSpan(text: f2.format(tlData.rd!), style: tlData.decaying ? TextStyle(color: tlData.rd! > 98 ? Colors.red : Colors.yellow) : null),
if (tlData.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: tlData.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), if (tlData.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: tlData.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic),
if (beforeS1end) tlData.rd! <= safeRD ? TextSpan(text: " (Safe)", style: TextStyle(color: Colors.greenAccent)) : TextSpan(text: " (> ${safeRD} RD !!!)", style: TextStyle(color: Colors.redAccent)) //if (beforeS1end) tlData.rd! <= safeRD ? TextSpan(text: " (Safe)", style: TextStyle(color: Colors.greenAccent)) : TextSpan(text: " (> ${safeRD} RD !!!)", style: TextStyle(color: Colors.redAccent))
], ],
), ),
), ),

View File

@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart';
import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/colors_functions.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart';
@ -19,15 +20,15 @@ import 'package:tetra_stats/widgets/tl_rating_thingy.dart';
var intFDiff = NumberFormat("+#,###.000;-#,###.000"); var intFDiff = NumberFormat("+#,###.000;-#,###.000");
class TLThingy extends StatefulWidget { class TLThingy extends StatefulWidget {
final TetraLeagueAlpha tl; final TetraLeague tl;
final String userID; final String userID;
final List<TetrioPlayer> states; final List<TetraLeague> states;
final bool showTitle; final bool showTitle;
final bool bot; final bool bot;
final bool guest; final bool guest;
final double? topTR; final double? topTR;
final PlayerLeaderboardPosition? lbPositions; final PlayerLeaderboardPosition? lbPositions;
final TetraLeagueAlpha? averages; final TetraLeague? averages;
final double? thatRankCutoff; final double? thatRankCutoff;
final double? thatRankCutoffGlicko; final double? thatRankCutoffGlicko;
final double? thatRankTarget; final double? thatRankTarget;
@ -43,33 +44,22 @@ class TLThingy extends StatefulWidget {
class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin { class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
late bool oskKagariGimmick; late bool oskKagariGimmick;
late TetraLeagueAlpha? oldTl; late TetraLeague? oldTl;
late TetraLeagueAlpha currentTl; late TetraLeague currentTl;
late RangeValues _currentRangeValues; late RangeValues _currentRangeValues;
late List<TetrioPlayer> sortedStates; late List<TetraLeague> sortedStates;
late Timer _countdownTimer;
Duration seasonLeft = seasonEnd.difference(DateTime.now());
@override @override
void initState() { void initState() {
_currentRangeValues = const RangeValues(0, 1); _currentRangeValues = const RangeValues(0, 1);
sortedStates = widget.states.reversed.toList(); sortedStates = widget.states.reversed.toList();
oldTl = sortedStates.elementAtOrNull(1)?.tlSeason1; oldTl = sortedStates.elementAtOrNull(1);
currentTl = widget.tl; currentTl = widget.tl;
super.initState(); super.initState();
_countdownTimer = Timer.periodic(
Durations.extralong4,
(Timer timer) {
setState(() {
seasonLeft = seasonEnd.difference(DateTime.now());
});
},
);
} }
@override @override
void dispose() { void dispose() {
_countdownTimer.cancel();
super.dispose(); super.dispose();
} }
@ -90,8 +80,8 @@ class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
return Column( return Column(
children: [ children: [
if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft))) //if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft)))
else Text(t.seasonEnded), //else Text(t.seasonEnded),
if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)), if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)),
textAlign: TextAlign.center,), textAlign: TextAlign.center,),
if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(),
@ -105,37 +95,26 @@ class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
if (values.start.round() == 0){ if (values.start.round() == 0){
currentTl = widget.tl; currentTl = widget.tl;
}else{ }else{
currentTl = sortedStates[values.start.round()-1].tlSeason1; currentTl = sortedStates[values.start.round()-1]!;
} }
if (values.end.round() == 0){ if (values.end.round() == 0){
oldTl = widget.tl; oldTl = widget.tl;
}else{ }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( if (currentTl.gamesPlayed > 9) TLProgress(
tlData: currentTl, tlData: currentTl,
previousRankTRcutoff: widget.thatRankCutoff, previousRankTRcutoff: widget.thatRankCutoff,
previousGlickoCutoff: widget.thatRankCutoffGlicko, previousGlickoCutoff: widget.thatRankCutoffGlicko,
previousRank: widget.tl.prevRank,
previousRankTRcutoffTarget: widget.thatRankTarget, previousRankTRcutoffTarget: widget.thatRankTarget,
nextRankTRcutoff: widget.nextRankCutoff, nextRankTRcutoff: widget.nextRankCutoff,
nextRankGlickoCutoff: widget.nextRankCutoffGlicko, nextRankGlickoCutoff: widget.nextRankCutoffGlicko,
nextRankTRcutoffTarget: widget.nextRankTarget, 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(
padding: const EdgeInsets.fromLTRB(8, 16, 8, 48), padding: const EdgeInsets.fromLTRB(8, 16, 8, 48),
child: Wrap( child: Wrap(

View File

@ -182,7 +182,6 @@ class UserThingy extends StatelessWidget {
],), ],),
onPressed: () { onPressed: () {
teto.addPlayerToTrack(player).then((value) => setState()); teto.addPlayerToTrack(player).then((value) => setState());
teto.storeState(player);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked)));
}, },
), ),
@ -213,7 +212,7 @@ class UserThingy extends StatelessWidget {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( 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, crossAxisAlignment: WrapCrossAlignment.start,
clipBehavior: Clip.hardEdge, // hard WHAT??? clipBehavior: Clip.hardEdge, // hard WHAT???
children: [ children: [
StatCellNum( if (!player.level.isNegative && !player.level.isNaN) StatCellNum(
playerStat: player.level, playerStat: player.level,
playerStatLabel: t.statCellNum.xpLevel, playerStatLabel: t.statCellNum.xpLevel,
isScreenBig: bigScreen, isScreenBig: bigScreen,

View File

@ -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<ZenithThingy> createState() => _ZenithThingyState();
}
class _ZenithThingyState extends State<ZenithThingy> {
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),
)
],
)
);
});
}
}

View File

@ -21,18 +21,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.10" version: "3.6.1"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.5.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -101,10 +101,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: coverage name: coverage
sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" sha256: "576aaab8b1abdd452e0f656c3e73da9ead9d7880e15bdc494189d9c1a1baf0db"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.7.2" version: "1.9.0"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -133,18 +133,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "1.0.8"
dev_build: dev_build:
dependency: transitive dependency: transitive
description: description:
name: dev_build name: dev_build
sha256: e5d575f3de4b0e5f004e065e1e2d98fa012d634b61b5855216b5698ed7f1e443 sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.16.4+3" version: "1.0.0+11"
equatable: equatable:
dependency: transitive dependency: transitive
description: description:
@ -197,18 +197,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_android name: file_selector_android
sha256: "1cd66575f063b689e041aec836905ba7be18d76c9f0634d0d75daec825f67095" sha256: d1e8655c1a4850a900a0cfaed55fdd273881d53a4bb78e4736dc170a0b17db78
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0+7" version: "0.5.1+5"
file_selector_ios: file_selector_ios:
dependency: transitive dependency: transitive
description: description:
name: file_selector_ios name: file_selector_ios
sha256: b015154e6d9fddbc4d08916794df170b44531798c8dd709a026df162d07ad81d sha256: "38ebf91ecbcfa89a9639a0854ccaed8ab370c75678938eebca7d34184296f0bb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1+8" version: "0.5.3"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -221,10 +221,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_macos name: file_selector_macos
sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+3" version: "0.9.4"
file_selector_platform_interface: file_selector_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -245,10 +245,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_windows name: file_selector_windows
sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+1" version: "0.9.3+2"
fl_chart: fl_chart:
dependency: "direct main" dependency: "direct main"
description: description:
@ -266,10 +266,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_colorpicker name: flutter_colorpicker
sha256: "458a6ed8ea480eb16ff892aedb4b7092b2804affd7e046591fb03127e8d8ef8b" sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.3" version: "1.1.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -282,10 +282,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -295,18 +295,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_markdown name: flutter_markdown
sha256: "87e11b9df25a42e2db315b8b7a51fae8e66f57a4b2f50ec4b822d0fa155e6b52" sha256: "04c4722cc36ec5af38acc38ece70d22d3c2123c61305d555750a091517bbe504"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.22" version: "0.6.23"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.17" version: "2.0.21"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -329,10 +329,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: frontend_server_client name: frontend_server_client
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "4.0.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -345,10 +345,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "13.2.1" version: "13.2.5"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -377,18 +377,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.7" version: "4.2.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.18.1" version: "0.19.0"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -401,10 +401,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: js name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.7" version: "0.7.1"
json2yaml: json2yaml:
dependency: transitive dependency: transitive
description: description:
@ -417,34 +417,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.1" version: "4.9.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.0" version: "10.0.4"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.3"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.1"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -489,10 +489,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.0" version: "1.12.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -553,26 +553,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.2" version: "2.2.9"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -593,10 +593,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -609,10 +609,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.4" version: "3.1.5"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -621,14 +621,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
version: "3.7.4"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -641,10 +633,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: process_run name: process_run
sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e" sha256: c917dfb5f7afad4c7485bc00a4df038621248fce046105020cea276d1a87c820
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.14.2" version: "1.1.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -665,42 +657,42 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.2" version: "2.2.3"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.5.0"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.1"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
@ -713,10 +705,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -758,18 +750,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: slang name: slang
sha256: "5e08ac915ac27a3508863f37734280d30c3713d56746cd2e4a5da77413da4b95" sha256: f68f6d6709890f85efabfb0318e9d694be2ebdd333e57fe5cb50eee449e4e3ab
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.30.1" version: "3.31.1"
slang_flutter: slang_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: slang_flutter name: slang_flutter
sha256: "9ee040b0d364d3a4d692e4af536acff6ef513870689403494ebc6d59b0dccea6" sha256: f8400292be49c11697d94af58d7f7d054c91af759f41ffe71e4e5413871ffc62
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.30.0" version: "3.31.0"
source_map_stack_trace: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -798,10 +790,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqflite name: sqflite
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.3+1"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
@ -830,18 +822,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlite3 name: sqlite3
sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.2"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.20" version: "0.5.24"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -910,26 +902,26 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: test name: test
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.24.9" version: "1.25.2"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.1" version: "0.7.0"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.9" version: "0.6.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -942,26 +934,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.5" version: "6.3.0"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.0" version: "6.3.8"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.5" version: "6.3.1"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -974,10 +966,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.2.0"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -998,10 +990,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
vector_graphics: vector_graphics:
dependency: transitive dependency: transitive
description: description:
@ -1038,10 +1030,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "13.0.0" version: "14.2.1"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -1078,18 +1070,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.3.0" version: "5.5.3"
window_manager: window_manager:
dependency: "direct main" dependency: "direct main"
description: description:
name: window_manager name: window_manager
sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.8" version: "0.3.9"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -1115,5 +1107,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.3.0 <4.0.0" dart: ">=3.4.0 <4.0.0"
flutter: ">=3.19.0" flutter: ">=3.22.0"

View File

@ -2,7 +2,7 @@ name: tetra_stats
description: Track your and other player stats in TETR.IO description: Track your and other player stats in TETR.IO
publish_to: 'none' publish_to: 'none'
version: 1.6.2+22 version: 1.6.9+35
environment: environment:
sdk: '>=3.0.0' sdk: '>=3.0.0'
@ -30,7 +30,7 @@ dependencies:
fl_chart: ^0.66.0 fl_chart: ^0.66.0
package_info_plus: ^5.0.1 package_info_plus: ^5.0.1
shared_preferences: ^2.1.1 shared_preferences: ^2.1.1
intl: ^0.18.0 intl: ^0.19.0
syncfusion_flutter_gauges: ^24.1.41 syncfusion_flutter_gauges: ^24.1.41
file_selector: ^1.0.1 file_selector: ^1.0.1
file_picker: ^6.1.1 file_picker: ^6.1.1

View File

@ -87,6 +87,9 @@
"verdictBetter": "better", "verdictBetter": "better",
"verdictWorse": "worse", "verdictWorse": "worse",
"smooth": "Smooth", "smooth": "Smooth",
"postSeason": "Off-season",
"seasonStarts": "Season starts in:",
"nanow": "Not avaliable for now...",
"seasonEnds": "Season ends in ${countdown}", "seasonEnds": "Season ends in ${countdown}",
"seasonEnded": "Season has ended", "seasonEnded": "Season has ended",
"gamesUntilRanked": "${left} games until being ranked", "gamesUntilRanked": "${left} games until being ranked",
@ -101,6 +104,17 @@
"neverPlayedTL": "That user never played Tetra League", "neverPlayedTL": "That user never played Tetra League",
"botTL": "Bots are not allowed to play Tetra League", "botTL": "Bots are not allowed to play Tetra League",
"anonTL": "Guests 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", "exportDB": "Export local database",
"exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.", "exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.",
"desktopExportAlertTitle": "Desktop export", "desktopExportAlertTitle": "Desktop export",
@ -141,7 +155,7 @@
"stateViewTitle": "${nickname} account on ${date}", "stateViewTitle": "${nickname} account on ${date}",
"statesViewTitle": "${number} states of ${nickname} account", "statesViewTitle": "${number} states of ${nickname} account",
"matchesViewTitle": "${nickname} TL matches", "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!", "stateRemoved": "${date} state was removed from database!",
"matchRemoved": "${date} match was removed from database!", "matchRemoved": "${date} match was removed from database!",
"viewAllMatches": "View all matches", "viewAllMatches": "View all matches",
@ -255,7 +269,7 @@
"lbpcShort": "№ in local LB", "lbpcShort": "№ in local LB",
"gamesPlayed": "Games\nplayed", "gamesPlayed": "Games\nplayed",
"gamesWonTL": "Games\nWon", "gamesWonTL": "Games\nWon",
"winrate": "Winrate\nprecentage", "winrate": "Winrate",
"level": "Level", "level": "Level",
"score": "Score", "score": "Score",
"spp": "Score\nPer Piece", "spp": "Score\nPer Piece",

View File

@ -87,6 +87,9 @@
"verdictBetter": "Лучше", "verdictBetter": "Лучше",
"verdictWorse": "Хуже", "verdictWorse": "Хуже",
"smooth": "Гладкий", "smooth": "Гладкий",
"postSeason": "Внесезонье",
"seasonStarts": "Сезон начнётся через:",
"nanow": "Пока недоступно...",
"seasonEnds": "Сезон закончится через ${countdown}", "seasonEnds": "Сезон закончится через ${countdown}",
"seasonEnded": "Сезон закончился", "seasonEnded": "Сезон закончился",
"gamesUntilRanked": "${left} матчей до получения рейтинга", "gamesUntilRanked": "${left} матчей до получения рейтинга",
@ -101,6 +104,17 @@
"neverPlayedTL": "Этот игрок никогда не играл в Тетра Лигу", "neverPlayedTL": "Этот игрок никогда не играл в Тетра Лигу",
"botTL": "Ботам нельзя играть в Тетра Лигу", "botTL": "Ботам нельзя играть в Тетра Лигу",
"anonTL": "Гостям нельзя играть в Тетра Лигу", "anonTL": "Гостям нельзя играть в Тетра Лигу",
"quickPlay": "Быстрая Игра",
"expert": "Эксперт",
"withMods": "С модами",
"withModsPlural":{
"zero": "с $n модами",
"one": "с $n модом",
"two": "с $n модами",
"few": "с $n модами",
"many": "с $n модами",
"other": "с $n модами"
},
"exportDB": "Экспортировать локальную базу данных", "exportDB": "Экспортировать локальную базу данных",
"exportDBDescription": "Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.", "exportDBDescription": "Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.",
"desktopExportAlertTitle": "Экспорт на десктопе", "desktopExportAlertTitle": "Экспорт на десктопе",
@ -141,7 +155,7 @@
"stateViewTitle": "Аккаунт ${nickname} ${date}", "stateViewTitle": "Аккаунт ${nickname} ${date}",
"statesViewTitle": "${number} состояний аккаунта ${nickname}", "statesViewTitle": "${number} состояний аккаунта ${nickname}",
"matchesViewTitle": "Матчи аккаунта ${nickname}", "matchesViewTitle": "Матчи аккаунта ${nickname}",
"statesViewEntry": "${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD", "statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно",
"stateRemoved": "Состояние от ${date} было удалено из локальной базы данных!", "stateRemoved": "Состояние от ${date} было удалено из локальной базы данных!",
"matchRemoved": "Матч от ${date} был удален из локальной базы данных!", "matchRemoved": "Матч от ${date} был удален из локальной базы данных!",
"viewAllMatches": "Все матчи", "viewAllMatches": "Все матчи",

111
res/icons/40l.svg Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="360mm"
height="150mm"
viewBox="0 0 360 150"
version="1.1"
id="svg95853"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="40l.svg">
<defs
id="defs95847" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="469.93678"
inkscape:cy="-94.189212"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1676"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata95850">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,3.0000027)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 0,87 H 60.000002 V 57 H 30.000001 V -3.0000027 H 0 Z"
id="path100330-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.7962963;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347"
width="120"
height="30"
x="27"
y="-90"
transform="rotate(90)" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 120,-3 h 60 v 30 h -30 v 60 h -30 z"
id="path100330-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:0.6712963;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 120,147 V 87 h 30 v 30 h 60 v 30 z"
id="path100330-0-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.83333331;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-6"
width="120"
height="30.000002"
x="-117"
y="180"
transform="rotate(-90)" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9"
width="120"
height="30.000002"
x="-117"
y="240"
transform="rotate(-90)" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.8287037;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9-9"
width="120"
height="30.000002"
x="240"
y="117" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
res/icons/allspin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

114
res/icons/blitz.svg Normal file
View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="390mm"
height="150.00002mm"
viewBox="0 0 390 150.00002"
version="1.1"
id="svg95853"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="blitz.svg">
<defs
id="defs95847" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="560.53089"
inkscape:cy="314.22402"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1676"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata95850">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(1e-6,3.0000076)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -1e-6,-3.000002 H 59.999998 V 26.999999 H 29.999999 v 60 h -30 z"
id="path100330-0-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9"
width="120"
height="30.000002"
x="-116.99999"
y="120"
transform="rotate(-90)" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.81944442;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9-9"
width="120"
height="30.000002"
x="120"
y="116.99999" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:0.7962963;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 89.999986,26.999997 -29.999988,2e-6 -10e-6,30 h -30 v 29.999999 l 30.000001,10e-7 V 117 h 30.000007 z"
id="path100330-7-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:0.6712963;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -1e-6,147 V 86.999999 h 30 V 117 h 60.000003 v 30 z"
id="path100330-0-8-4"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 360,-3.0000057 V 56.999997 H 330 V 26.999994 H 270 V -3.0000057 Z"
id="path100330-0-8-49"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:0.81018519;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 270,56.999997 V 117 h 29.99999 V 86.999995 H 360 V 56.999997 Z"
id="path100330-7-8-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.64351851;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9-9-0"
width="120"
height="30.000002"
x="270"
y="117" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
res/icons/doublehole.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
res/icons/expert.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
res/icons/gravity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
res/icons/invisible.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

110
res/icons/league.svg Normal file
View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="420mm"
height="150mm"
viewBox="0 0 420 150"
version="1.1"
id="svg95853"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="league.svg">
<defs
id="defs95847" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.25"
inkscape:cx="259.85326"
inkscape:cy="119.35595"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1676"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata95850">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(1e-6,3.0000076)">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.82407409;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9"
width="120"
height="30.000002"
x="-3.0000076"
y="-119.99999"
transform="rotate(90)" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 300.00001,-2.9999986 V 57.000001 H 330 v -30 h 60 V -2.9999986 Z"
id="path100330-7-8-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -1e-6,56.999992 V -3.000006 h 29.99998 v 29.999998 h 60.00001 v 30 z"
id="path100330-7-8-9-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9-1"
width="120"
height="30.000002"
x="-2.9999986"
y="-180"
transform="rotate(90)" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.81018519;stroke:none;stroke-width:1.88264012;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect100347-9-1-2"
width="120"
height="30.000002"
x="-270"
y="-147"
transform="scale(-1)" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:0.8287037;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 300.00001,146.99999 h 60 V 117 h -30 V 57.000001 h -30 z"
id="path100330-7-8-9-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:0.63425928;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 420,146.99998 H 358 V 117 h 32 V 56.999985 h 30 z"
id="path100330-7-8-9-2-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
res/icons/messy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

BIN
res/icons/nohold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

111
res/icons/qp.svg Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="390mm"
height="150.00002mm"
viewBox="0 0 390 150.00002"
version="1.1"
id="svg95853"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="qp.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs95847" />
<sodipodi:namedview
id="base"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.32485136"
inkscape:cx="763.42609"
inkscape:cy="427.88801"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1732"
inkscape:window-height="1052"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="0"
inkscape:deskcolor="#505050" />
<metadata
id="metadata95850">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,3.000003)">
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 0,86.999997 h 60 v -30 H 30 V -3.0000027 H 0 Z"
id="path100330-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:0.796078;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 120,-3.0000027 V 56.999996 H 90 V 26.999997 H 30 V -3.0000027 Z"
id="path100330-7-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 390,-3.000003 V 57 H 360 V 26.999997 h -60 v -30 z"
id="path100330-0-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:0.796078;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 300,117 V 57 h 30 v 30.000003 h 60 V 117 Z"
id="path100330-0-8-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:0.671296;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 120,56.999996 H 59.999997 v 30 H 90.000002 V 147.00001 H 120 Z"
id="path100330-0-8-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:0.670588;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 270,86.999998 h -60 v -30 h 30 V -3.0000027 h 30 z"
id="path100330-7-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 150,-3.0000027 V 56.999998 h 30 v -30 h 60 V -3.0000027 Z"
id="path100330-7-3-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:0.796078;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 150,56.999998 h 60 v 30 h -30 v 60.000012 h -30 z"
id="path100330-0-8-2-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
res/icons/volatile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@ -1,186 +1,186 @@
import 'dart:io'; // import 'dart:io';
import 'dart:ui'; // import 'dart:ui';
import 'package:flutter/foundation.dart'; // import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; // import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; // import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
import 'package:test/test.dart'; // import 'package:test/test.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; // import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/services/crud_exceptions.dart'; // import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/services/tetrio_crud.dart'; // import 'package:tetra_stats/services/tetrio_crud.dart';
void main() { // void main() {
WidgetsFlutterBinding.ensureInitialized(); // WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized(); // DartPluginRegistrant.ensureInitialized();
late TetrioService teto; // late TetrioService teto;
setUp(() { // setUp(() {
if (kIsWeb) { // if (kIsWeb) {
sqfliteFfiInit(); // sqfliteFfiInit();
databaseFactory = databaseFactoryFfiWeb; // databaseFactory = databaseFactoryFfiWeb;
} else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { // } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
sqfliteFfiInit(); // sqfliteFfiInit();
databaseFactory = databaseFactoryFfi; // databaseFactory = databaseFactoryFfi;
} // }
teto = TetrioService(); // teto = TetrioService();
}); // });
test("Initialize TetrioServise", () async { // test("Initialize TetrioServise", () async {
teto.open(); // teto.open();
}); // a fucking MissingPluginException how does that even happening? // }); // a fucking MissingPluginException how does that even happening?
// i guess i will be unable to test iteractions with DB // // i guess i will be unable to test iteractions with DB
group("Test fetchPlayer with different players", () { // 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. // // 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 { // test("dan63047 (user who have activity in tetra league)", () async {
TetrioPlayer dan63047 = await teto.fetchPlayer("6098518e3d5155e6ec429cdc"); // TetrioPlayer dan63047 = await teto.fetchPlayer("6098518e3d5155e6ec429cdc");
expect(dan63047.userId, "6098518e3d5155e6ec429cdc"); // expect(dan63047.userId, "6098518e3d5155e6ec429cdc");
expect(dan63047.registrationTime != null, true); // expect(dan63047.registrationTime != null, true);
expect(dan63047.avatarRevision != null, true); // expect(dan63047.avatarRevision != null, true);
expect(dan63047.connections != null, true); // expect(dan63047.connections != null, true);
expect(dan63047.role, "user"); // expect(dan63047.role, "user");
expect(dan63047.distinguishment, null); // imagine if that one fails one day lol // expect(dan63047.distinguishment, null); // imagine if that one fails one day lol
expect(dan63047.tlSeason1.glicko != null, true); // expect(dan63047.tlSeason1.glicko != null, true);
//expect(dan63047.tlSeason1.rank != "z", true); lol // //expect(dan63047.tlSeason1.rank != "z", true); lol
expect(dan63047.tlSeason1.percentileRank != "z", true); // expect(dan63047.tlSeason1.percentileRank != "z", true);
expect(dan63047.tlSeason1.rating > -1, true); // expect(dan63047.tlSeason1.tr > -1, true);
expect(dan63047.tlSeason1.gamesPlayed > 9, true); // expect(dan63047.tlSeason1.gamesPlayed > 9, true);
expect(dan63047.tlSeason1.gamesWon > 0, true); // expect(dan63047.tlSeason1.gamesWon > 0, true);
//expect(dan63047.tlSeason1.standing, -1); // //expect(dan63047.tlSeason1.standing, -1);
//expect(dan63047.tlSeason1.standingLocal, -1); // //expect(dan63047.tlSeason1.standingLocal, -1);
expect(dan63047.tlSeason1.apm != null, true); // expect(dan63047.tlSeason1.apm != null, true);
expect(dan63047.tlSeason1.pps != null, true); // expect(dan63047.tlSeason1.pps != null, true);
expect(dan63047.tlSeason1.vs != null, true); // expect(dan63047.tlSeason1.vs != null, true);
expect(dan63047.tlSeason1.nerdStats != null, true); // expect(dan63047.tlSeason1.nerdStats != null, true);
expect(dan63047.tlSeason1.estTr != null, true); // expect(dan63047.tlSeason1.estTr != null, true);
expect(dan63047.tlSeason1.esttracc != null, true); // expect(dan63047.tlSeason1.esttracc != null, true);
expect(dan63047.tlSeason1.playstyle != null, true); // expect(dan63047.tlSeason1.playstyle != null, true);
}); // });
test("osk (sysop who have activity in tetra league)", () async { // test("osk (sysop who have activity in tetra league)", () async {
TetrioPlayer osk = await teto.fetchPlayer("5e32fc85ab319c2ab1beb07c"); // TetrioPlayer osk = await teto.fetchPlayer("5e32fc85ab319c2ab1beb07c");
expect(osk.userId, "5e32fc85ab319c2ab1beb07c"); // expect(osk.userId, "5e32fc85ab319c2ab1beb07c");
expect(osk.registrationTime, null); // expect(osk.registrationTime, null);
expect(osk.country, "XM"); // expect(osk.country, "XM");
expect(osk.avatarRevision != null, true); // expect(osk.avatarRevision != null, true);
expect(osk.bannerRevision != null, true); // expect(osk.bannerRevision != null, true);
expect(osk.connections != null, true); // expect(osk.connections != null, true);
expect(osk.verified, true); // expect(osk.verified, true);
expect(osk.role, "sysop"); // expect(osk.role, "sysop");
expect(osk.distinguishment != null, true); // expect(osk.distinguishment != null, true);
expect(osk.tlSeason1.glicko != null, true); // expect(osk.tlSeason1.glicko != null, true);
expect(osk.tlSeason1.glicko != null, true); // expect(osk.tlSeason1.glicko != null, true);
expect(osk.tlSeason1.rank == "z", true); // expect(osk.tlSeason1.rank == "z", true);
expect(osk.tlSeason1.percentileRank != "z", true); // expect(osk.tlSeason1.percentileRank != "z", true);
expect(osk.tlSeason1.rating > -1, true); // expect(osk.tlSeason1.tr > -1, true);
expect(osk.tlSeason1.gamesPlayed > 9, true); // expect(osk.tlSeason1.gamesPlayed > 9, true);
expect(osk.tlSeason1.gamesWon > 0, true); // expect(osk.tlSeason1.gamesWon > 0, true);
expect(osk.tlSeason1.standing, -1); // expect(osk.tlSeason1.standing, -1);
expect(osk.tlSeason1.standingLocal, -1); // expect(osk.tlSeason1.standingLocal, -1);
expect(osk.tlSeason1.apm != null, true); // expect(osk.tlSeason1.apm != null, true);
expect(osk.tlSeason1.pps != null, true); // expect(osk.tlSeason1.pps != null, true);
expect(osk.tlSeason1.vs != null, true); // expect(osk.tlSeason1.vs != null, true);
expect(osk.tlSeason1.nerdStats != null, true); // expect(osk.tlSeason1.nerdStats != null, true);
expect(osk.tlSeason1.estTr != null, true); // expect(osk.tlSeason1.estTr != null, true);
expect(osk.tlSeason1.esttracc != null, true); // expect(osk.tlSeason1.esttracc != null, true);
expect(osk.tlSeason1.playstyle != null, true); // expect(osk.tlSeason1.playstyle != null, true);
}); // });
test("kagari (sysop who have zero activity)", () async { // test("kagari (sysop who have zero activity)", () async {
TetrioPlayer kagari = await teto.fetchPlayer("5e331c3ce24a5a3e258f7a1b"); // TetrioPlayer kagari = await teto.fetchPlayer("5e331c3ce24a5a3e258f7a1b");
expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b"); // expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b");
expect(kagari.registrationTime, null); // expect(kagari.registrationTime, null);
expect(kagari.country, "XM"); // expect(kagari.country, "XM");
expect(kagari.xp, 0); // expect(kagari.xp, 0);
expect(kagari.gamesPlayed, -1); // expect(kagari.gamesPlayed, -1);
expect(kagari.gamesWon, -1); // expect(kagari.gamesWon, -1);
expect(kagari.gameTime, const Duration(seconds: -1)); // expect(kagari.gameTime, const Duration(seconds: -1));
expect(kagari.avatarRevision != null, true); // expect(kagari.avatarRevision != null, true);
expect(kagari.bannerRevision != null, true); // expect(kagari.bannerRevision != null, true);
expect(kagari.connections, null); // expect(kagari.connections, null);
expect(kagari.verified, true); // expect(kagari.verified, true);
expect(kagari.distinguishment != null, true); // expect(kagari.distinguishment != null, true);
expect(kagari.distinguishment!.detail, "kagarin"); // expect(kagari.distinguishment!.detail, "kagarin");
expect(kagari.friendCount, 1); // expect(kagari.friendCount, 1);
expect(kagari.tlSeason1.glicko, null); // expect(kagari.tlSeason1.glicko, null);
expect(kagari.tlSeason1.rank, "z"); // expect(kagari.tlSeason1.rank, "z");
expect(kagari.tlSeason1.percentileRank, "z"); // expect(kagari.tlSeason1.percentileRank, "z");
expect(kagari.tlSeason1.rating, -1); // expect(kagari.tlSeason1.tr, -1);
expect(kagari.tlSeason1.decaying, false); // expect(kagari.tlSeason1.decaying, false);
expect(kagari.tlSeason1.gamesPlayed, 0); // expect(kagari.tlSeason1.gamesPlayed, 0);
expect(kagari.tlSeason1.gamesWon, 0); // expect(kagari.tlSeason1.gamesWon, 0);
expect(kagari.tlSeason1.standing, -1); // expect(kagari.tlSeason1.standing, -1);
expect(kagari.tlSeason1.standingLocal, -1); // expect(kagari.tlSeason1.standingLocal, -1);
expect(kagari.tlSeason1.apm, null); // expect(kagari.tlSeason1.apm, null);
expect(kagari.tlSeason1.pps, null); // expect(kagari.tlSeason1.pps, null);
expect(kagari.tlSeason1.vs, null); // expect(kagari.tlSeason1.vs, null);
expect(kagari.tlSeason1.nerdStats, null); // expect(kagari.tlSeason1.nerdStats, null);
expect(kagari.tlSeason1.estTr, null); // expect(kagari.tlSeason1.estTr, null);
expect(kagari.tlSeason1.esttracc, null); // expect(kagari.tlSeason1.esttracc, null);
expect(kagari.tlSeason1.playstyle, null); // expect(kagari.tlSeason1.playstyle, null);
}); // });
test("furry (banned account)", () async { // test("furry (banned account)", () async {
TetrioPlayer furry = await teto.fetchPlayer("5eea0ff69a1ba76c20347086"); // TetrioPlayer furry = await teto.fetchPlayer("5eea0ff69a1ba76c20347086");
expect(furry.userId, "5eea0ff69a1ba76c20347086"); // expect(furry.userId, "5eea0ff69a1ba76c20347086");
expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z")); // expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z"));
expect(furry.role, "banned"); // expect(furry.role, "banned");
expect(furry.badges.isEmpty, true); // expect(furry.badges.isEmpty, true);
expect(furry.badstanding, false); // expect(furry.badstanding, false);
expect(furry.xp, 0); // expect(furry.xp, 0);
expect(furry.supporterTier, 0); // expect(furry.supporterTier, 0);
expect(furry.verified, false); // expect(furry.verified, false);
expect(furry.connections, null); // expect(furry.connections, null);
expect(furry.gamesPlayed, 0); // expect(furry.gamesPlayed, 0);
expect(furry.gamesWon, 0); // expect(furry.gamesWon, 0);
expect(furry.gameTime, Duration.zero); // expect(furry.gameTime, Duration.zero);
expect(furry.tlSeason1.glicko, null); // expect(furry.tlSeason1.glicko, null);
expect(furry.tlSeason1.rank, "z"); // expect(furry.tlSeason1.rank, "z");
expect(furry.tlSeason1.percentileRank, "z"); // expect(furry.tlSeason1.percentileRank, "z");
expect(furry.tlSeason1.rating, -1); // expect(furry.tlSeason1.tr, -1);
expect(furry.tlSeason1.decaying, false); // expect(furry.tlSeason1.decaying, false);
expect(furry.tlSeason1.gamesPlayed, 0); // expect(furry.tlSeason1.gamesPlayed, 0);
expect(furry.tlSeason1.gamesWon, 0); // expect(furry.tlSeason1.gamesWon, 0);
expect(furry.tlSeason1.standing, -1); // expect(furry.tlSeason1.standing, -1);
expect(furry.tlSeason1.standingLocal, -1); // expect(furry.tlSeason1.standingLocal, -1);
expect(furry.tlSeason1.apm, null); // expect(furry.tlSeason1.apm, null);
expect(furry.tlSeason1.pps, null); // expect(furry.tlSeason1.pps, null);
expect(furry.tlSeason1.vs, null); // expect(furry.tlSeason1.vs, null);
expect(furry.tlSeason1.nerdStats, null); // expect(furry.tlSeason1.nerdStats, null);
expect(furry.tlSeason1.estTr, null); // expect(furry.tlSeason1.estTr, null);
expect(furry.tlSeason1.esttracc, null); // expect(furry.tlSeason1.esttracc, null);
expect(furry.tlSeason1.playstyle, null); // expect(furry.tlSeason1.playstyle, null);
}); // });
test("oskwarefan (anon account)", () async { // test("oskwarefan (anon account)", () async {
TetrioPlayer oskwarefan = await teto.fetchPlayer("646cb8273e887a054d64febe"); // TetrioPlayer oskwarefan = await teto.fetchPlayer("646cb8273e887a054d64febe");
expect(oskwarefan.userId, "646cb8273e887a054d64febe"); // expect(oskwarefan.userId, "646cb8273e887a054d64febe");
expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z")); // expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z"));
expect(oskwarefan.role, "anon"); // expect(oskwarefan.role, "anon");
expect(oskwarefan.xp > 0, true); // expect(oskwarefan.xp > 0, true);
expect(oskwarefan.gamesPlayed > -1, true); // expect(oskwarefan.gamesPlayed > -1, true);
expect(oskwarefan.gamesWon > -1, true); // expect(oskwarefan.gamesWon > -1, true);
expect(oskwarefan.gameTime.isNegative, false); // expect(oskwarefan.gameTime.isNegative, false);
expect(oskwarefan.country, null); // expect(oskwarefan.country, null);
expect(oskwarefan.verified, false); // expect(oskwarefan.verified, false);
expect(oskwarefan.connections, null); // expect(oskwarefan.connections, null);
expect(oskwarefan.friendCount, 0); // expect(oskwarefan.friendCount, 0);
expect(oskwarefan.tlSeason1.glicko, null); // expect(oskwarefan.tlSeason1.glicko, null);
expect(oskwarefan.tlSeason1.rank, "z"); // expect(oskwarefan.tlSeason1.rank, "z");
expect(oskwarefan.tlSeason1.percentileRank, "z"); // expect(oskwarefan.tlSeason1.percentileRank, "z");
expect(oskwarefan.tlSeason1.rating, -1); // expect(oskwarefan.tlSeason1.tr, -1);
expect(oskwarefan.tlSeason1.decaying, true); // ??? why true? // expect(oskwarefan.tlSeason1.decaying, true); // ??? why true?
expect(oskwarefan.tlSeason1.gamesPlayed, 0); // expect(oskwarefan.tlSeason1.gamesPlayed, 0);
expect(oskwarefan.tlSeason1.gamesWon, 0); // expect(oskwarefan.tlSeason1.gamesWon, 0);
expect(oskwarefan.tlSeason1.standing, -1); // expect(oskwarefan.tlSeason1.standing, -1);
expect(oskwarefan.tlSeason1.standingLocal, -1); // expect(oskwarefan.tlSeason1.standingLocal, -1);
expect(oskwarefan.tlSeason1.apm, null); // expect(oskwarefan.tlSeason1.apm, null);
expect(oskwarefan.tlSeason1.pps, null); // expect(oskwarefan.tlSeason1.pps, null);
expect(oskwarefan.tlSeason1.vs, null); // expect(oskwarefan.tlSeason1.vs, null);
expect(oskwarefan.tlSeason1.nerdStats, null); // expect(oskwarefan.tlSeason1.nerdStats, null);
expect(oskwarefan.tlSeason1.estTr, null); // expect(oskwarefan.tlSeason1.estTr, null);
expect(oskwarefan.tlSeason1.esttracc, null); // expect(oskwarefan.tlSeason1.esttracc, null);
expect(oskwarefan.tlSeason1.playstyle, null); // expect(oskwarefan.tlSeason1.playstyle, null);
}); // });
test("not existing account", () async { // test("not existing account", () async {
var future = teto.fetchPlayer("hasdbashdbs"); // var future = teto.fetchPlayer("hasdbashdbs");
await expectLater(future, throwsA(isA<TetrioPlayerNotExist>())); // await expectLater(future, throwsA(isA<TetrioPlayerNotExist>()));
}); // });
}); // });
} // }