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
with:
channel: 'stable'
flutter-version: '3.16.5'
flutter-version: '3.22.3'
- name: Install project dependencies
run: flutter pub get
- name: Build artifacts
@ -40,51 +40,28 @@ jobs:
tag: Auto-${{ github.run_number }}
body: Builded with GitHub Action workflow
token: ${{ secrets.TOKEN }}
# build-and-release-linux:
# name: Build Linux App
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - uses: subosito/flutter-action@v1
# - uses: ashutoshvarma/setup-ninja@master
# with:
# channel: 'stable'
# flutter-version: '3.16.5'
# - name: Install project dependencies
# run: flutter pub get
# - name: Build artifacts
# run: flutter build linux --release
# - name: Archive Release
# uses: thedoctor0/zip-release@master
# with:
# type: 'zip'
# filename: TetraStats-${{github.ref_name}}-windows.zip
# directory: build/linux/x64/runner/Release/bundle
# - name: Push to Releases
# uses: ncipollo/release-action@v1
# with:
# prerelease: true
# allowUpdates: true
# replacesArtifacts: false
# discussionCategory: autobuilded-releases
# artifacts: "build/linux/x64/runner/Release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
# tag: Auto-${{ github.run_number }}
# body: Builded with GitHub Action workflow
# token: ${{ secrets.TOKEN }}
build-and-release-android:
name: Build Android App
build-and-release-linux:
name: Build Linux App
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-java@v1
with:
java-version: '12.x'
- uses: actions/checkout@v2
- uses: ashutoshvarma/setup-ninja@master
- uses: subosito/flutter-action@v1
with:
flutter-version: '3.16.5'
- run: flutter pub get
# - run: flutter test // lmao. Tests? Who needs it?
- run: flutter build apk --split-per-abi
channel: 'stable'
flutter-version: '3.22.3'
- name: Install project dependencies
run: |
flutter pub get
sudo apt-get install -y ninja-build libgtk-3-dev
- name: Build artifacts
run: flutter build linux --release
- name: Archive Release
uses: thedoctor0/zip-release@master
with:
type: 'zip'
filename: TetraStats-${{github.ref_name}}-linux.zip
directory: build/linux/x64/release/bundle
- name: Push to Releases
uses: ncipollo/release-action@v1
with:
@ -92,7 +69,32 @@ jobs:
allowUpdates: true
replacesArtifacts: false
discussionCategory: autobuilded-releases
artifacts: "build/app/outputs/flutter-apk/*"
artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
tag: Auto-${{ github.run_number }}
body: Builded with GitHub Action workflow
token: ${{ secrets.TOKEN }}
# 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

@ -53,7 +53,7 @@ android {
applicationId "com.dan63.tetra_stats"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 19
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -134,7 +134,7 @@ class ReplayStats{
topSpike = 0;
tspins = 0;
roundLength = 0.0;
clears = Clears(singles: 0, doubles: 0, triples: 0, quads: 0, pentas: 0, allClears: 0, tSpinZeros: 0, tSpinSingles: 0, tSpinDoubles: 0, tSpinTriples: 0, tSpinPentas: 0, tSpinQuads: 0, tSpinMiniZeros: 0, tSpinMiniSingles: 0, tSpinMiniDoubles: 0);
clears = Clears(singles: 0, doubles: 0, triples: 0, quads: 0, pentas: 0, allClears: 0, tSpinZeros: 0, tSpinSingles: 0, tSpinDoubles: 0, tSpinTriples: 0, tSpinPentas: 0, tSpinQuads: 0, tSpinMiniZeros: 0, tSpinMiniSingles: 0, tSpinMiniDoubles: 0, tSpinMiniTriples: 0, tSpinMiniQuads: 0);
garbage = Garbage(sent: 0, recived: 0, attack: 0, cleared: 0);
finesse = Finesse(combo: 0, faults: 0, perfectPieces: 0);
}
@ -208,12 +208,12 @@ class ReplayData{
stats = [];
roundWinners = [];
int roundID = 0;
List<double> APMmultipliedByWeights = [0, 0];
List<double> PPSmultipliedByWeights = [0, 0];
List<double> VSmultipliedByWeights = [0, 0];
List<double> SPPmultipliedByWeights = [0, 0];
List<double> KPPmultipliedByWeights = [0, 0];
List<double> KPSmultipliedByWeights = [0, 0];
List<double> apmMultipliedByWeights = [0, 0];
List<double> ppsMultipliedByWeights = [0, 0];
List<double> vsMultipliedByWeights = [0, 0];
List<double> sppMultipliedByWeights = [0, 0];
List<double> kppMultipliedByWeights = [0, 0];
List<double> kpsMultipliedByWeights = [0, 0];
totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()];
for(var round in json['data']) {
int firstInEndContext = round['replays'][0]["events"].last['data']['export']['options']['username'].startsWith(endcontext[0].username) ? 0 : 1;
@ -221,30 +221,30 @@ class ReplayData{
int roundLength = max(round['replays'][0]['frames'], round['replays'][1]['frames']);
roundLengths.add(roundLength);
totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']);
APMmultipliedByWeights[0] += endcontext[0].secondaryTracking[roundID]*roundLength;
APMmultipliedByWeights[1] += endcontext[1].secondaryTracking[roundID]*roundLength;
PPSmultipliedByWeights[0] += endcontext[0].tertiaryTracking[roundID]*roundLength;
PPSmultipliedByWeights[1] += endcontext[1].tertiaryTracking[roundID]*roundLength;
VSmultipliedByWeights[0] += endcontext[0].extraTracking[roundID]*roundLength;
VSmultipliedByWeights[1] += endcontext[1].extraTracking[roundID]*roundLength;
apmMultipliedByWeights[0] += endcontext[0].secondaryTracking[roundID]*roundLength;
apmMultipliedByWeights[1] += endcontext[1].secondaryTracking[roundID]*roundLength;
ppsMultipliedByWeights[0] += endcontext[0].tertiaryTracking[roundID]*roundLength;
ppsMultipliedByWeights[1] += endcontext[1].tertiaryTracking[roundID]*roundLength;
vsMultipliedByWeights[0] += endcontext[0].extraTracking[roundID]*roundLength;
vsMultipliedByWeights[1] += endcontext[1].extraTracking[roundID]*roundLength;
int winner = round['board'].indexWhere((element) => element['success'] == true);
roundWinners.add([round['board'][winner]['id']??round['board'][winner]['user']['_id'], round['board'][winner]['username']??round['board'][winner]['user']['username']]);
ReplayStats playerOne = ReplayStats.fromJson(round['replays'][firstInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][secondInEndContext]['events']), round['replays'][firstInEndContext]['frames']); // (events contain recived attacks)
ReplayStats playerTwo = ReplayStats.fromJson(round['replays'][secondInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][firstInEndContext]['events']), round['replays'][secondInEndContext]['frames']);
SPPmultipliedByWeights[0] += playerOne.spp*roundLength;
SPPmultipliedByWeights[1] += playerTwo.spp*roundLength;
KPPmultipliedByWeights[0] += playerOne.kpp*roundLength;
KPPmultipliedByWeights[1] += playerTwo.kpp*roundLength;
KPSmultipliedByWeights[0] += playerOne.kps*roundLength;
KPSmultipliedByWeights[1] += playerTwo.kps*roundLength;
sppMultipliedByWeights[0] += playerOne.spp*roundLength;
sppMultipliedByWeights[1] += playerTwo.spp*roundLength;
kppMultipliedByWeights[0] += playerOne.kpp*roundLength;
kppMultipliedByWeights[1] += playerTwo.kpp*roundLength;
kpsMultipliedByWeights[0] += playerOne.kps*roundLength;
kpsMultipliedByWeights[1] += playerTwo.kps*roundLength;
stats.add([playerOne, playerTwo]);
totalStats[0] = totalStats[0] + playerOne;
totalStats[1] = totalStats[1] + playerTwo;
roundID ++;
}
timeWeightedStats = [
AggregateStats(APMmultipliedByWeights[0]/totalLength, PPSmultipliedByWeights[0]/totalLength, VSmultipliedByWeights[0]/totalLength, SPPmultipliedByWeights[0]/totalLength, KPPmultipliedByWeights[0]/totalLength, KPSmultipliedByWeights[0]/totalLength),
AggregateStats(APMmultipliedByWeights[1]/totalLength, PPSmultipliedByWeights[1]/totalLength, VSmultipliedByWeights[1]/totalLength, SPPmultipliedByWeights[1]/totalLength, KPPmultipliedByWeights[1]/totalLength, KPSmultipliedByWeights[1]/totalLength)
AggregateStats(apmMultipliedByWeights[0]/totalLength, ppsMultipliedByWeights[0]/totalLength, vsMultipliedByWeights[0]/totalLength, sppMultipliedByWeights[0]/totalLength, kppMultipliedByWeights[0]/totalLength, kpsMultipliedByWeights[0]/totalLength),
AggregateStats(apmMultipliedByWeights[1]/totalLength, ppsMultipliedByWeights[1]/totalLength, vsMultipliedByWeights[1]/totalLength, sppMultipliedByWeights[1]/totalLength, kppMultipliedByWeights[1]/totalLength, kpsMultipliedByWeights[1]/totalLength)
];
}

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 1186 (593 per locale)
/// Strings: 1210 (605 per locale)
///
/// Built on 2024-07-20 at 13:24 UTC
/// Built on 2024-09-04 at 20:41 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
@ -222,6 +222,9 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get verdictBetter => 'better';
String get verdictWorse => 'worse';
String get smooth => 'Smooth';
String get postSeason => 'Off-season';
String get seasonStarts => 'Season starts in:';
String get nanow => 'Not avaliable for now...';
String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}';
String get seasonEnded => 'Season has ended';
String gamesUntilRanked({required Object left}) => '${left} games until being ranked';
@ -236,6 +239,17 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get neverPlayedTL => 'That user never played Tetra League';
String get botTL => 'Bots are not allowed to play Tetra League';
String get anonTL => 'Guests are not allowed to play Tetra League';
String get quickPlay => 'Quick Play';
String get expert => 'Expert';
String get withMods => 'With mods';
String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
zero: 'with ${n} mods',
one: 'with ${n} mod',
two: 'with ${n} mods',
few: 'with ${n} mods',
many: 'with ${n} mods',
other: 'with ${n} mods',
);
String get exportDB => 'Export local database';
String get exportDBDescription => 'It contains states and Tetra League records of the tracked players and list of tracked players.';
String get desktopExportAlertTitle => 'Desktop export';
@ -276,7 +290,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}';
String statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
String matchesViewTitle({required Object nickname}) => '${nickname} TL matches';
String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD';
String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
String stateRemoved({required Object date}) => '${date} state was removed from database!';
String matchRemoved({required Object date}) => '${date} match was removed from database!';
String get viewAllMatches => 'View all matches';
@ -703,7 +717,7 @@ class _StringsStatCellNumEn {
String get lbpcShort => '№ in local LB';
String get gamesPlayed => 'Games\nplayed';
String get gamesWonTL => 'Games\nWon';
String get winrate => 'Winrate\nprecentage';
String get winrate => 'Winrate';
String get level => 'Level';
String get score => 'Score';
String get spp => 'Score\nPer Piece';
@ -919,6 +933,9 @@ class _StringsRu implements Translations {
@override String get verdictBetter => 'Лучше';
@override String get verdictWorse => 'Хуже';
@override String get smooth => 'Гладкий';
@override String get postSeason => 'Внесезонье';
@override String get seasonStarts => 'Сезон начнётся через:';
@override String get nanow => 'Пока недоступно...';
@override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}';
@override String get seasonEnded => 'Сезон закончился';
@override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга';
@ -933,6 +950,17 @@ class _StringsRu implements Translations {
@override String get neverPlayedTL => 'Этот игрок никогда не играл в Тетра Лигу';
@override String get botTL => 'Ботам нельзя играть в Тетра Лигу';
@override String get anonTL => 'Гостям нельзя играть в Тетра Лигу';
@override String get quickPlay => 'Быстрая Игра';
@override String get expert => 'Эксперт';
@override String get withMods => 'С модами';
@override String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n,
zero: 'с ${n} модами',
one: 'с ${n} модом',
two: 'с ${n} модами',
few: 'с ${n} модами',
many: 'с ${n} модами',
other: 'с ${n} модами',
);
@override String get exportDB => 'Экспортировать локальную базу данных';
@override String get exportDBDescription => 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.';
@override String get desktopExportAlertTitle => 'Экспорт на десктопе';
@ -973,7 +1001,7 @@ class _StringsRu implements Translations {
@override String stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
@override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
@override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${nickname}';
@override String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD';
@override String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
@override String stateRemoved({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!';
@override String matchRemoved({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!';
@override String get viewAllMatches => 'Все матчи';
@ -1608,6 +1636,9 @@ extension on Translations {
case 'verdictBetter': return 'better';
case 'verdictWorse': return 'worse';
case 'smooth': return 'Smooth';
case 'postSeason': return 'Off-season';
case 'seasonStarts': return 'Season starts in:';
case 'nanow': return 'Not avaliable for now...';
case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}';
case 'seasonEnded': return 'Season has ended';
case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked';
@ -1622,6 +1653,17 @@ extension on Translations {
case 'neverPlayedTL': return 'That user never played Tetra League';
case 'botTL': return 'Bots are not allowed to play Tetra League';
case 'anonTL': return 'Guests are not allowed to play Tetra League';
case 'quickPlay': return 'Quick Play';
case 'expert': return 'Expert';
case 'withMods': return 'With mods';
case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
zero: 'with ${n} mods',
one: 'with ${n} mod',
two: 'with ${n} mods',
few: 'with ${n} mods',
many: 'with ${n} mods',
other: 'with ${n} mods',
);
case 'exportDB': return 'Export local database';
case 'exportDBDescription': return 'It contains states and Tetra League records of the tracked players and list of tracked players.';
case 'desktopExportAlertTitle': return 'Desktop export';
@ -1662,7 +1704,7 @@ extension on Translations {
case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} account on ${date}';
case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches';
case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD';
case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
case 'stateRemoved': return ({required Object date}) => '${date} state was removed from database!';
case 'matchRemoved': return ({required Object date}) => '${date} match was removed from database!';
case 'viewAllMatches': return 'View all matches';
@ -1773,7 +1815,7 @@ extension on Translations {
case 'statCellNum.lbpcShort': return '№ in local LB';
case 'statCellNum.gamesPlayed': return 'Games\nplayed';
case 'statCellNum.gamesWonTL': return 'Games\nWon';
case 'statCellNum.winrate': return 'Winrate\nprecentage';
case 'statCellNum.winrate': return 'Winrate';
case 'statCellNum.level': return 'Level';
case 'statCellNum.score': return 'Score';
case 'statCellNum.spp': return 'Score\nPer Piece';
@ -2221,6 +2263,9 @@ extension on _StringsRu {
case 'verdictBetter': return 'Лучше';
case 'verdictWorse': return 'Хуже';
case 'smooth': return 'Гладкий';
case 'postSeason': return 'Внесезонье';
case 'seasonStarts': return 'Сезон начнётся через:';
case 'nanow': return 'Пока недоступно...';
case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}';
case 'seasonEnded': return 'Сезон закончился';
case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга';
@ -2235,6 +2280,17 @@ extension on _StringsRu {
case 'neverPlayedTL': return 'Этот игрок никогда не играл в Тетра Лигу';
case 'botTL': return 'Ботам нельзя играть в Тетра Лигу';
case 'anonTL': return 'Гостям нельзя играть в Тетра Лигу';
case 'quickPlay': return 'Быстрая Игра';
case 'expert': return 'Эксперт';
case 'withMods': return 'С модами';
case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n,
zero: 'с ${n} модами',
one: 'с ${n} модом',
two: 'с ${n} модами',
few: 'с ${n} модами',
many: 'с ${n} модами',
other: 'с ${n} модами',
);
case 'exportDB': return 'Экспортировать локальную базу данных';
case 'exportDBDescription': return 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.';
case 'desktopExportAlertTitle': return 'Экспорт на десктопе';
@ -2275,7 +2331,7 @@ extension on _StringsRu {
case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${nickname}';
case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD';
case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
case 'stateRemoved': return ({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!';
case 'matchRemoved': return ({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!';
case 'viewAllMatches': return 'Все матчи';

View File

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

View File

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

View File

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

View File

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

View File

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

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

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:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/tl_match_view.dart';
import 'package:window_manager/window_manager.dart';
late String oldWindowTitle;
@ -73,12 +72,6 @@ class MatchesState extends State<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)))],
);

View File

@ -360,7 +360,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("${_f2.format(they[index].rating)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null),
Text("${_f2.format(they[index].tr)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null),
Image.asset("res/tetrio_tl_alpha_ranks/${they[index].rank}.png", height: bigScreen ? 48 : 16),
],
),
@ -379,6 +379,8 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
child: ListView(
children: [
_ListEntry(value: widget.rank[1]["lowestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestTRid"], username: widget.rank[1]["lowestTRnick"], approximate: false, fractionDigits: 2),
_ListEntry(value: widget.rank[1]["lowestGlixare"], label: "Glixare", id: widget.rank[1]["lowestGlixareID"], username: widget.rank[1]["lowestGlixareNick"], approximate: false, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["lowestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["lowestS1trID"], username: widget.rank[1]["lowestS1trNick"], approximate: false, fractionDigits: 2),
_ListEntry(value: widget.rank[1]["lowestGlicko"], label: "Glicko", id: widget.rank[1]["lowestGlickoID"], username: widget.rank[1]["lowestGlickoNick"], approximate: false, fractionDigits: 2),
_ListEntry(value: widget.rank[1]["lowestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestRdID"], username: widget.rank[1]["lowestRdNick"], approximate: false, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["lowestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGamesPlayedID"], username: widget.rank[1]["lowestGamesPlayedNick"], approximate: false),
@ -412,7 +414,9 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
Text(t.averageValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
Expanded(
child: ListView(children: [
_ListEntry(value: widget.rank[0].rating, label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
_ListEntry(value: widget.rank[0].tr, label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
_ListEntry(value: widget.rank[0].gxe, label: "Glixare", id: "", username: "", approximate: false, fractionDigits: 3),
_ListEntry(value: widget.rank[0].s1tr, label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: "", username: "", approximate: false, fractionDigits: 2),
_ListEntry(value: widget.rank[0].glicko, label: "Glicko", id: "", username: "", approximate: true, fractionDigits: 2),
_ListEntry(value: widget.rank[0].rd, label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
_ListEntry(value: widget.rank[0].gamesPlayed, label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 0),
@ -446,6 +450,8 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
child: ListView(
children: [
_ListEntry(value: widget.rank[1]["highestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestTRid"], username: widget.rank[1]["highestTRnick"], approximate: false, fractionDigits: 2),
_ListEntry(value: widget.rank[1]["highestGlixare"], label: "Glixare", id: widget.rank[1]["highestGlixareID"], username: widget.rank[1]["highestGlixareNick"], approximate: false, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["highestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["highestS1trID"], username: widget.rank[1]["highestS1trNick"], approximate: false, fractionDigits: 2),
_ListEntry(value: widget.rank[1]["highestGlicko"], label: "Glicko", id: widget.rank[1]["highestGlickoID"], username: widget.rank[1]["highestGlickoNick"], approximate: false, fractionDigits: 2),
_ListEntry(value: widget.rank[1]["highestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestRdID"], username: widget.rank[1]["highestRdNick"], approximate: false, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["highestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGamesPlayedID"], username: widget.rank[1]["highestGamesPlayedNick"], approximate: false),
@ -517,7 +523,7 @@ class _ListEntry extends StatelessWidget {
children: [
Text(f.format(value),
style: const TextStyle(fontSize: 22, height: 0.9)),
if (id.isNotEmpty) Text(t.forPlayer(username: username))
if (id.isNotEmpty) Text(t.forPlayer(username: username), style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.w100),)
],
),
onTap: id.isNotEmpty

View File

@ -1,9 +1,11 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/views/rank_averages_view.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:window_manager/window_manager.dart';
import 'package:tetra_stats/main.dart' show teto;
@ -17,14 +19,9 @@ class RankAveragesView extends StatefulWidget {
late String oldWindowTitle;
class RanksAverages extends State<RankAveragesView> {
Map<String, List<dynamic>> averages = {};
@override
void initState() {
teto.fetchTLLeaderboard().then((value){
averages = value.averages;
setState(() {});
});
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.rankAveragesViewTitle}");
@ -46,27 +43,131 @@ class RanksAverages extends State<RankAveragesView> {
),
backgroundColor: Colors.black,
body: SafeArea(
child: averages.isEmpty ? const Center(child: Text('Fetching...')) : ListView.builder(
itemCount: averages.length,
itemBuilder: (context, index){
List<String> keys = averages.keys.toList();
return ListTile(
leading: Image.asset("res/tetrio_tl_alpha_ranks/${keys[index]}.png", height: 48),
title: Text(t.players(n: averages[keys[index]]?[1]["players"]), style: const TextStyle(fontFamily: "Eurostile Round Extended")),
subtitle: Text("${f2.format(averages[keys[index]]?[0].apm)} APM, ${f2.format(averages[keys[index]]?[0].pps)} PPS, ${f2.format(averages[keys[index]]?[0].vs)} VS, ${f2.format(averages[keys[index]]?[0].nerdStats.app)} APP, ${f2.format(averages[keys[index]]?[0].nerdStats.vsapm)} VS/APM",
style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey, fontSize: 13)),
trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: const TextStyle(fontSize: 28, fontFamily: "Eurostile Round")),
onTap: (){
if (averages[keys[index]]?[1]["players"] > 0) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RankView(rank: averages[keys[index]]!),
child: FutureBuilder<CutoffsTetrio?>(future: teto.fetchCutoffsTetrio(), builder: (context, snapshot){
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return const Center(child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.done:
if (snapshot.hasData){
return Container(
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(
alignment: Alignment.center,
width: 900,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: TableBorder.all(color: Colors.grey.shade900),
columnWidths: const {
0: FixedColumnWidth(48),
1: FixedColumnWidth(155),
2: FixedColumnWidth(150),
3: FixedColumnWidth(90),
4: FixedColumnWidth(130),
},
children: [
TableRow(
children: [
Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
const Padding(
padding: EdgeInsets.only(right: 8.0),
child: Text("TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
const Padding(
padding: EdgeInsets.only(right: 8.0),
child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
const Padding(
padding: EdgeInsets.only(right: 8.0),
child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
const Padding(
padding: EdgeInsets.only(right: 8.0),
child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
const Padding(
padding: EdgeInsets.only(right: 8.0),
child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
),
]
),
for (String rank in snapshot.data!.data.keys) TableRow(
decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])),
children: [
Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(f2.format(snapshot.data!.data[rank]!.apm), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(f2.format(snapshot.data!.data[rank]!.pps), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(f2.format(snapshot.data!.data[rank]!.vs), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text("${f3.format(snapshot.data!.data[rank]!.apm / (snapshot.data!.data[rank]!.pps * 60))} APP\n${f3.format(snapshot.data!.data[rank]!.vs / snapshot.data!.data[rank]!.apm)} VS/APM", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: RichText(
textAlign: TextAlign.right,
text: TextSpan(
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow),
children: [
TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)),
TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)),
TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null))
]
))
),
]
)
],
),
Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp)))
],
),
),
),
),
);
}
},
if (snapshot.hasError){
return Center(child:
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
if (snapshot.stackTrace != null) Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center),
),
],
)
);
}
return const Text("end of FutureBuilder");
}
})
),
);

View File

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

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/widgets/tl_thingy.dart';
import 'package:tetra_stats/widgets/user_thingy.dart';
import 'package:window_manager/window_manager.dart';
@ -11,7 +12,7 @@ import 'package:window_manager/window_manager.dart';
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
class StateView extends StatefulWidget {
final TetrioPlayer state;
final TetraLeague state;
const StateView({super.key, required this.state});
@override
@ -28,7 +29,7 @@ class StateState extends State<StateView> {
_scrollController = ScrollController();
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.stateViewTitle(nickname: widget.state.username.toUpperCase(), date: dateFormat.format(widget.state.state))}");
windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}");
}
super.initState();
}
@ -49,15 +50,12 @@ class StateState extends State<StateView> {
final t = Translations.of(context);
return Scaffold(
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,
body: SafeArea(
child: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (context, value) {
return [SliverToBoxAdapter(child: UserThingy(player: widget.state, showStateTimestamp: true, setState: _justUpdate))];
},
body: TLThingy(tl: widget.state.tlSeason1, userID: widget.state.userId, states: const [],))));
child: TLThingy(tl: widget.state, userID: widget.state.id, states: [])
)
);
}
}

View File

@ -1,18 +1,19 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/views/mathes_view.dart';
import 'package:tetra_stats/views/state_view.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:window_manager/window_manager.dart';
class StatesView extends StatefulWidget {
final List<TetrioPlayer> states;
const StatesView({super.key, required this.states});
final String nickname;
final String id;
const StatesView({required this.nickname, required this.id, super.key});
@override
State<StatefulWidget> createState() => StatesState();
@ -25,7 +26,7 @@ class StatesState extends State<StatesView> {
void initState() {
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.username.toUpperCase())}");
//windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}");
}
super.initState();
}
@ -41,14 +42,14 @@ class StatesState extends State<StatesView> {
final t = Translations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.username.toUpperCase())),
title: Text(t.statesViewTitle(number: "", nickname: widget.nickname)),
actions: [
IconButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MatchesView(userID: widget.states.first.userId, username: widget.states.first.username),
builder: (context) => MatchesView(userID: widget.id, username: widget.nickname),
),
);
}, icon: const Icon(Icons.list), tooltip: t.viewAllMatches)
@ -56,18 +57,33 @@ class StatesState extends State<StatesView> {
),
backgroundColor: Colors.black,
body: SafeArea(
child: ListView.builder(
itemCount: widget.states.length,
child: FutureBuilder<List<TetraLeague>>(future: teto.getStates(widget.id), builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return const Center(child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.done:
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
prototypeItem: ListTile(
title: Text(""),
subtitle: Text("", style: TextStyle(color: Colors.grey)),
trailing: IconButton(icon: const Icon(Icons.delete_forever), onPressed: (){}),
),
itemBuilder: (context, index) {
return ListTile(
title: Text(timestamp(widget.states[index].state)),
subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: NumberFormat.compact().format(widget.states[index].tlSeason1.rd))),
title: Text(timestamp(snapshot.data![index].timestamp)),
subtitle: Text(
t.statesViewEntry(level: f2.format(snapshot.data![index].tr), games: intf.format(snapshot.data![index].gamesPlayed), glicko: snapshot.data![index].glicko != null ? f2.format(snapshot.data![index].glicko) : "---", rd: snapshot.data![index].rd != null ? f2.format(snapshot.data![index].rd) : "--"),
style: TextStyle(color: Colors.grey),
),
trailing: IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () {
DateTime nn = widget.states[index].state;
teto.deleteState(widget.states[index]).then((value) => setState(() {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn)))));
teto.deleteState(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)))));
}));
},
),
@ -75,11 +91,29 @@ class StatesState extends State<StatesView> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StateView(state: widget.states[index]),
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:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/main.dart';
import 'package:tetra_stats/views/main_view.dart';
import 'package:tetra_stats/views/rank_averages_view.dart';
import 'package:tetra_stats/views/ranks_averages_view.dart';
import 'package:window_manager/window_manager.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
final TetrioService _teto = TetrioService();
List<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
Stats _sortBy = Stats.tr;
bool reversed = false;
@ -65,7 +65,7 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
backgroundColor: Colors.black,
body: SafeArea(
child: FutureBuilder(
future: _teto.fetchTLLeaderboard(),
future: teto.fetchTLLeaderboard(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
@ -73,21 +73,12 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
case ConnectionState.active:
return const Center(child: CircularProgressIndicator());
case ConnectionState.done:
if (snapshot.hasData){
final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country);
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers!.length)}");
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers != null ? allPlayers.length : 0)}");
bool bigScreen = MediaQuery.of(context).size.width > 768;
return NestedScrollView(
headerSliverBuilder: (context, value) {
String howManyPlayers(int numberOfPlayers) => Intl.plural(
numberOfPlayers,
zero: t.lbViewZeroEntrys,
one: t.lbViewOneEntry,
other: t.lbViewManyEntrys(numberOfPlayers: t.players(n: numberOfPlayers)),
name: 'howManyPeople',
args: [numberOfPlayers],
desc: 'Description of how many people are seen in a place.',
examples: const {'numberOfPeople': 3},
);
return [
SliverToBoxAdapter(
child: Padding(
@ -97,7 +88,7 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
alignment: WrapAlignment.spaceBetween,
children: [
Text(
howManyPlayers(allPlayers.length),
"${t.players(n: allPlayers.length)}${t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))}",
style: const TextStyle(color: Colors.white, fontSize: 25),
),
TextButton(onPressed: (){
@ -189,7 +180,7 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("${f2.format(allPlayers[index].rating)} TR", style: const TextStyle(fontSize: 28)),
Text("${f2.format(allPlayers[index].tr)} TR", style: const TextStyle(fontSize: 28)),
Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36),
],
),
@ -205,6 +196,22 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
);
}));
}
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 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy;
import 'package:tetra_stats/views/compare_view.dart' show CompareThingy;
import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/widgets/vs_graphs.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
@ -30,7 +28,7 @@ Duration framesToTime(int frames){
}
class TlMatchResultView extends StatefulWidget {
final TetraLeagueAlphaRecord record;
final BetaRecord record;
final String initPlayerId;
const TlMatchResultView({super.key, required this.record, required this.initPlayerId});
@ -40,15 +38,65 @@ class TlMatchResultView extends StatefulWidget {
class TlMatchResultState extends State<TlMatchResultView> {
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
void initState(){
rounds = [DropdownMenuItem(value: -1, child: Text(t.match))];
rounds.addAll([for (int i = 0; i < widget.record.endContext.first.secondaryTracking.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]);
replayData = teto.analyzeReplay(widget.record.replayId, widget.record.replayAvalable);
rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]);
if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch)));
greenSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id == widget.initPlayerId);
redSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id != widget.initPlayerId);
List<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){
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${timestamp(widget.record.timestamp)}");
windowManager.setTitle("Tetra Stats: ${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}");
}
super.initState();
}
@ -62,45 +110,17 @@ class TlMatchResultState extends State<TlMatchResultView> {
Widget buildComparison(double width, bool showMobileSelector){
bool bigScreen = width >= 768;
if (roundSelector.isNegative){
time = totalTime;
readableTime = !time.isNegative ? "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "${t.matchLength}: ---";
}else{
time = roundLengths[roundSelector];
int alive = widget.record.results.rounds[roundSelector].indexWhere((element) => element.alive);
readableTime = "${t.roundLength}: ${!time.isNegative ? "${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "---"}\n${t.winner}: ${alive == -1 ? "idk" : widget.record.results.rounds[roundSelector][alive].username}";
}
return SizedBox(
width: width,
child: FutureBuilder(future: replayData, builder: (context, snapshot){
late Duration time;
late String readableTime;
late String reason;
timeWeightedStatsAvaliable = true;
if (snapshot.connectionState != ConnectionState.done) return const LinearProgressIndicator();
if (!snapshot.hasError){
if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch)));
greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId);
redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId);
if (roundSelector.isNegative){
time = framesToTime(snapshot.data!.totalLength);
readableTime = "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}";
}else{
time = framesToTime(snapshot.data!.roundLengths[roundSelector]);
readableTime = "${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}";
}
}else{
switch (snapshot.error.runtimeType){
case ReplayNotAvalable:
reason = t.matchIsTooOld;
break;
case SzyNotFound:
reason = t.matchIsTooOld;
break;
case SzyForbidden:
reason = t.errors.replayRejected;
break;
case SzyTooManyRequests:
reason = t.errors.tooManyRequests;
break;
default:
reason = snapshot.error.toString();
break;
}
}
return NestedScrollView(
child: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverToBoxAdapter(
@ -117,15 +137,15 @@ class TlMatchResultState extends State<TlMatchResultView> {
colors: const [Colors.green, Colors.transparent],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0],
stops: [0.0, initPlayerWon ? 0.4 : 0.0],
)),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(children: [
Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: bigScreen ? const TextStyle(
Text(widget.record.results.leaderboard[greenSidePlayer].username, style: bigScreen ? const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28) : const TextStyle()),
Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle(
Text(widget.record.results.leaderboard[greenSidePlayer].wins.toString(), style: const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 42))
]),
@ -143,15 +163,15 @@ class TlMatchResultState extends State<TlMatchResultView> {
colors: const [Colors.red, Colors.transparent],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0],
stops: [0.0, !initPlayerWon ? 0.4 : 0.0],
)),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Column(children: [
Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: bigScreen ? const TextStyle(
Text(widget.record.results.leaderboard[redSidePlayer].username, style: bigScreen ? const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28) : const TextStyle()),
Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle(
Text(widget.record.results.leaderboard[redSidePlayer].wins.toString(), style: const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 42))
]),
@ -179,10 +199,10 @@ class TlMatchResultState extends State<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)),
),
if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(snapshot.hasError ? reason : readableTime, textAlign: TextAlign.center))),
if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(readableTime, textAlign: TextAlign.center))),
const SliverToBoxAdapter(
child: Divider(),
)
@ -194,106 +214,37 @@ class TlMatchResultState extends State<TlMatchResultView> {
children: [
CompareThingy(
label: "APM",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector],
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm :
roundSelector == -1 ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector],
greenSide: roundSelector == -2 ? timeWeightedStats[0].apm :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
redSide: roundSelector == -2 ? timeWeightedStats[1].apm :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: "PPS",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps:
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector],
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector],
greenSide: roundSelector == -2 ? timeWeightedStats[0].pps :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
redSide: roundSelector == -2 ? timeWeightedStats[1].pps :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: "VS",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector],
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector],
greenSide: roundSelector == -2 ? timeWeightedStats[0].vs :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
redSide: roundSelector == -2 ? timeWeightedStats[1].vs :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs,
fractionDigits: 2,
higherIsBetter: true,
),
if (snapshot.hasData) Column(children: [
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].inputs : snapshot.data!.stats[roundSelector][greenSidePlayer].inputs,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].inputs : snapshot.data!.stats[roundSelector][redSidePlayer].inputs,
label: "Inputs", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced,
label: "Pieces Placed", higherIsBetter: true),
CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kpp :
roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kpp : snapshot.data!.stats[roundSelector][greenSidePlayer].kpp,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kpp :
roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kpp : snapshot.data!.stats[roundSelector][redSidePlayer].kpp,
label: "KPP", higherIsBetter: false, fractionDigits: 2,),
CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].kps :
roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].kps : snapshot.data!.stats[roundSelector][greenSidePlayer].kps,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].kps :
roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].kps : snapshot.data!.stats[roundSelector][redSidePlayer].kps,
label: "KPS", higherIsBetter: true, fractionDigits: 2,),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared,
label: "Lines Cleared", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].score : snapshot.data!.stats[roundSelector][greenSidePlayer].score,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].score : snapshot.data!.stats[roundSelector][redSidePlayer].score,
label: "Score", higherIsBetter: true),
CompareThingy(greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].spp :
roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].spp : snapshot.data!.stats[roundSelector][greenSidePlayer].spp,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].spp :
roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].spp : snapshot.data!.stats[roundSelector][redSidePlayer].spp,
label: "SPP", higherIsBetter: true, fractionDigits: 2,),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100,
label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike,
label: "Best Spike", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo,
label: "Best Combo", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB,
label: "Best BtB", higherIsBetter: true),
const Divider(),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent,
if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageSent,
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageSent,
label: "Sent", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived,
label: "Received", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack,
label: "Attack", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared,
label: "Cleared", higherIsBetter: true),
const Divider(),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text("Line Clears", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.allClears,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.allClears : snapshot.data!.stats[roundSelector][redSidePlayer].clears.allClears,
label: "PC", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].tspins : snapshot.data!.stats[roundSelector][greenSidePlayer].tspins,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].tspins : snapshot.data!.stats[roundSelector][redSidePlayer].tspins,
label: "T-spins", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][greenSidePlayer].clears.quads,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].clears.quads : snapshot.data!.stats[roundSelector][redSidePlayer].clears.quads,
label: "Quads", higherIsBetter: true),
],),
],
),
const Divider(),
if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageReceived,
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageReceived,
label: "Received", higherIsBetter: true), const Divider(),
Column(
children: [
Padding(
@ -305,180 +256,179 @@ class TlMatchResultState extends State<TlMatchResultView> {
),
CompareThingy(
label: "APP",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.app :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].app,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.app :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.app : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].app,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.app :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.app,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.app :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.app,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "VS/APM",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.vsapm :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.vsapm :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.vsapm : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].vsapm,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.vsapm :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.vsapm,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.vsapm :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.vsapm,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "DS/S",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dss :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dss,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dss :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dss : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dss,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dss :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dss,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dss :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dss,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "DS/P",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.dsp :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].dsp,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.dsp :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.dsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].dsp,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dsp :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dsp,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dsp :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dsp,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "APP + DS/P",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.appdsp :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.appdsp :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.appdsp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].appdsp,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.appdsp :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.appdsp,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.appdsp :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.appdsp,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "),
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.cheese :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].cheese,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.cheese :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.cheese : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].cheese,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.cheese :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.cheese,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.cheese :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.cheese,
fractionDigits: 2,
higherIsBetter: false,
),
CompareThingy(
label: "Gb Eff.",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.gbe :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].gbe,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.gbe :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.gbe : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].gbe,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.gbe :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.gbe,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.gbe :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.gbe,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "wAPP",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.nyaapp :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.nyaapp :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.nyaapp : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].nyaapp,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.nyaapp :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.nyaapp,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.nyaapp :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.nyaapp,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "Area",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats.area :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector].area,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats.area :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats.area : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector].area,
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.area :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.area,
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.area :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.area,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: t.statCellNum.estOfTRShort,
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].estTr.esttr :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).estTrTracking[roundSelector].esttr,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].estTr.esttr :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTr.esttr : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).estTrTracking[roundSelector].esttr,
greenSide: roundSelector == -2 ? timeWeightedStats[0].estTr.esttr :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr,
redSide: roundSelector == -2 ? timeWeightedStats[1].estTr.esttr :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.estTr.esttr,
fractionDigits: 2,
higherIsBetter: true,
),
CompareThingy(
label: "Opener",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.opener :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].opener,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.opener :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.opener : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].opener,
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.opener :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.opener,
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.opener :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.opener,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "Plonk",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.plonk :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].plonk,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.plonk :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.plonk : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].plonk,
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.plonk :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk,
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.plonk :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "Stride",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.stride :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].stride,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.stride :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.stride : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].stride,
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.stride :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.stride,
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.stride :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.stride,
fractionDigits: 3,
higherIsBetter: true,
),
CompareThingy(
label: "Inf. DS",
greenSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle.infds :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector].infds,
redSide: (roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle.infds :
roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle.infds : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector].infds,
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.infds :
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.infds,
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.infds :
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.infds,
fractionDigits: 3,
higherIsBetter: true,
),
VsGraphs(
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).nerdStatsTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[greenSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).playstyleTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].apm : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].pps : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiary : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].vs : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extra : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].nerdStats : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStats : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).nerdStatsTracking[roundSelector],
(roundSelector == -2 && snapshot.hasData) ? snapshot.data!.timeWeightedStats[redSidePlayer].playstyle : roundSelector.isNegative ? widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyle : widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).playstyleTracking[roundSelector]
roundSelector == -2 ? timeWeightedStats[0].apm : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
roundSelector == -2 ? timeWeightedStats[0].pps : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
roundSelector == -2 ? timeWeightedStats[0].vs : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
roundSelector == -2 ? timeWeightedStats[0].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats,
roundSelector == -2 ? timeWeightedStats[0].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle,
roundSelector == -2 ? timeWeightedStats[1].apm : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
roundSelector == -2 ? timeWeightedStats[1].pps : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
roundSelector == -2 ? timeWeightedStats[1].vs : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs,
roundSelector == -2 ? timeWeightedStats[1].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats,
roundSelector == -2 ? timeWeightedStats[1].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle,
)
],
),
if (widget.record.ownId != widget.record.replayId) const Divider(),
if (widget.record.ownId != widget.record.replayId) Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text("Handling",
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),
),
CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das,
label: "DAS", fractionDigits: 1, postfix: "F",
higherIsBetter: false),
CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr,
label: "ARR", fractionDigits: 1, postfix: "F",
higherIsBetter: false),
CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf,
label: "SDF", prefix: "x",
higherIsBetter: true),
CompareBoolThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock,
label: "Safe HD",
trueIsBetter: true)
// if (widget.record.ownId != widget.record.replayId) const Divider(),
// if (widget.record.ownId != widget.record.replayId) Column(
// children: [
// Padding(
// padding: const EdgeInsets.only(bottom: 16),
// child: Text("Handling",
// style: TextStyle(
// fontFamily: "Eurostile Round Extended",
// fontSize: bigScreen ? 42 : 28)),
// ),
// CompareThingy(
// greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das,
// redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das,
// label: "DAS", fractionDigits: 1, postfix: "F",
// higherIsBetter: false),
// CompareThingy(
// greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr,
// redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr,
// label: "ARR", fractionDigits: 1, postfix: "F",
// higherIsBetter: false),
// CompareThingy(
// greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf,
// redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf,
// label: "SDF", prefix: "x",
// higherIsBetter: true),
// CompareBoolThingy(
// greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock,
// redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock,
// label: "Safe HD",
// trueIsBetter: true)
// ],
// )
],
)
],
)
);
}),
])),
);
}
@ -494,76 +444,34 @@ class TlMatchResultState extends State<TlMatchResultView> {
Wrap(
alignment: WrapAlignment.spaceBetween,
children: [
FutureBuilder(future: replayData, builder: (context, snapshot) {
switch(snapshot.connectionState){
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return const CircularProgressIndicator();
case ConnectionState.done:
if (!snapshot.hasError){
var time = framesToTime(snapshot.data!.totalLength);
return Column(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.matchLength),
RichText(
text: TextSpan(
text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}",
text: !totalTime.isNegative ? TextSpan(
text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}",
style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white),
children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(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(
children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(totalTime.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
) : const TextSpan(
text: "-:--",
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.grey),
children: [TextSpan(text: ".---", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
),
)
],);
}
}
},),
if (widget.record.ownId != widget.record.replayId) Column(
],),
if (widget.record.id != widget.record.replayID) Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(t.numberOfRounds),
RichText(
text: TextSpan(
text: widget.record.endContext.first.secondaryTracking.isNotEmpty ? widget.record.endContext.first.secondaryTracking.length.toString() : "---",
style: TextStyle(
text: widget.record.results.rounds.length.toString(),
style: const TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 28,
fontWeight: FontWeight.w500,
color: widget.record.endContext.first.secondaryTracking.isEmpty ? Colors.grey : Colors.white
color: Colors.white
),
),
)
@ -572,47 +480,29 @@ class TlMatchResultState extends State<TlMatchResultView> {
OverflowBar(
alignment: MainAxisAlignment.spaceEvenly,
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: () {
roundSelector = -1;
setState(() {});
}, child: Text(t.matchStats)),
TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.grey.shade900)) : null,
TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
onPressed: timeWeightedStatsAvaliable ? () {
roundSelector = -2;
setState(() {});
} : null, child: Text(t.timeWeightedmatchStats)) ,
//TextButton( child: const Text('Button 3'), onPressed: () {}),
],
)
]),
// Column(
// children: [
// ListTile(
// leading: Text("Round time"),
// title: Text("Winner", textAlign: TextAlign.center,),
// trailing: Text("Round stats"),
// )
// ],
// )
],
)
)
];
},
body: ListView.builder(itemCount: widget.record.endContext.first.secondaryTracking.length,
body: ListView.builder(itemCount: widget.record.results.rounds.length,
itemBuilder: (BuildContext context, int index) {
return FutureBuilder(future: replayData, builder: (context, snapshot) {
switch(snapshot.connectionState){
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return const LinearProgressIndicator();
case ConnectionState.done:
if (!snapshot.hasError){
var time = framesToTime(snapshot.data!.roundLengths[index]);
var accentColor = snapshot.data!.roundWinners[index][0] == widget.initPlayerId ? Colors.green : Colors.red;
var accentColor = widget.record.results.rounds[index][0].id == widget.initPlayerId ? Colors.green : Colors.red;
var bgColor = roundSelector == index ? Colors.grey.shade900 : Colors.transparent;
var time = roundLengths[index];
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
@ -622,48 +512,24 @@ class TlMatchResultState extends State<TlMatchResultView> {
),
child: ListTile(
leading:RichText(
text: TextSpan(
text: !time.isNegative ? TextSpan(
text: "${time.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(time.inSeconds%60)}",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 22, fontWeight: FontWeight.w500, color: Colors.white),
children: [TextSpan(text: ".${NumberFormat("000", LocaleSettings.currentLocale.languageCode).format(time.inMilliseconds%1000)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
),
),
title: Text(snapshot.data!.roundWinners[index][1], textAlign: TextAlign.center),
trailing: TrailingStats(
widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).secondaryTracking[index],
widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).tertiaryTracking[index],
widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).extraTracking[index],
widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).secondaryTracking[index],
widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).tertiaryTracking[index],
widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).extraTracking[index]
),
onTap:(){
roundSelector = index;
setState(() {});
},
),
);
}else{
return Container(
decoration: BoxDecoration(
color: roundSelector == index ? Colors.grey.shade900 : Colors.transparent
),
child: ListTile(
leading: RichText(
text: const TextSpan(
) : 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),
title: Text(widget.record.results.rounds[index][0].username, 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]
widget.record.results.rounds[index].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
widget.record.results.rounds[index].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
widget.record.results.rounds[index].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
widget.record.results.rounds[index].firstWhere((element) => element.id != widget.initPlayerId).stats.vs
),
onTap:(){
roundSelector = index;
@ -671,10 +537,6 @@ class TlMatchResultState extends State<TlMatchResultView> {
},
),
);
}
}
}
);
})
),
),
@ -707,10 +569,10 @@ class TlMatchResultState extends State<TlMatchResultView> {
final t = Translations.of(context);
return Scaffold(
appBar: AppBar(
title: Text("${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${timestamp(widget.record.timestamp)}"),
title: Text("${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}"),
actions: [
PopupMenuButton(
enabled: widget.record.replayAvalable,
enabled: widget.record.gamemode == "league",
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
value: 1,
@ -724,10 +586,10 @@ class TlMatchResultState extends State<TlMatchResultView> {
onSelected: (value) async {
switch (value) {
case 1:
await launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${widget.record.replayId}"));
await launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${widget.record.replayID}"));
break;
case 2:
await launchInBrowser(Uri.parse("https://tetr.io/#r:${widget.record.replayId}"));
await launchInBrowser(Uri.parse("https://tetr.io/#r:${widget.record.replayID}"));
break;
default:
}

View File

@ -78,7 +78,7 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
case ConnectionState.active:
return const Center(child: CircularProgressIndicator(color: Colors.white));
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();
return NestedScrollView(
headerSliverBuilder: (context, value) {
@ -107,22 +107,22 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
body: ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
print(index);
return ListTile(
title: Text(t.trackedPlayersEntry(nickname: allPlayers[keys[index]]!.last.username, numberOfStates: allPlayers[keys[index]]!.length)),
subtitle: Text(t.trackedPlayersDescription(firstStateDate: timestamp(allPlayers[keys[index]]!.first.state), lastStateDate: timestamp(allPlayers[keys[index]]!.last.state))),
title: Text(allPlayers[keys[index]]??"No nickname (huh?)"),
subtitle: Text(keys[index], style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
trailing: IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () {
String nn = allPlayers[keys[index]]!.last.username;
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]]!),
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 'package:fl_chart/fl_chart.dart';
@ -196,7 +198,7 @@ class MyRadarChartPainter extends RadarChartPainter{
}
class MyRadarChartLeaf extends RadarChartLeaf{
MyRadarChartLeaf({required super.data, required super.targetData});
const MyRadarChartLeaf({super.key, required super.data, required super.targetData});
@override
RenderRadarChart createRenderObject(BuildContext context) => MyRenderRadarChart(

View File

@ -7,8 +7,9 @@ class LineclearsThingy extends StatelessWidget{
final int lines;
final int holds;
final int tSpins;
final bool showMoreClears;
const LineclearsThingy(this.clears, this.lines, this.holds, this.tSpins, {super.key});
const LineclearsThingy(this.clears, this.lines, this.holds, this.tSpins, {super.key, this.showMoreClears = false});
@override
Widget build(BuildContext context) {
@ -21,6 +22,7 @@ class LineclearsThingy extends StatelessWidget{
mainAxisSize: MainAxisSize.min,
children: [
Text(t.numOfGameActions.lineClears(n: lines), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center),
if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Pentas"), Text(clears.pentas.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Quads"), Text(clears.quads.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Triples"), Text(clears.triples.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Doubles"), Text(clears.doubles.toString())]),
@ -36,10 +38,14 @@ class LineclearsThingy extends StatelessWidget{
mainAxisSize: MainAxisSize.min,
children: [
Text(t.numOfGameActions.tspinsTotal(n: tSpins), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center),
if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spin pentas"), Text(clears.tSpinPentas.toString())]),
if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spin quads"), Text(clears.tSpinQuads.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins triples"), Text(clears.tSpinTriples.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins doubles"), Text(clears.tSpinDoubles.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins singles"), Text(clears.tSpinSingles.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins zeros"), Text(clears.tSpinZeros.toString())]),
if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins quads"), Text(clears.tSpinMiniQuads.toString())]),
if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins triples"), Text(clears.tSpinMiniTriples.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins doubles"), Text(clears.tSpinMiniDoubles.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins singles"), Text(clears.tSpinMiniSingles.toString())]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]),

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ class StatCellNum extends StatelessWidget {
required this.playerStat,
required this.playerStatLabel,
required this.isScreenBig,
this.smallDecimal = true,
this.smallDecimal = false,
this.alertWidgets,
this.fractionDigits,
this.oldPlayerStat,
@ -52,7 +52,7 @@ class StatCellNum extends StatelessWidget {
RichText(
text: TextSpan(text: splited[0],
children: [
if ((fractionDigits??0) > 0) TextSpan(text: f.symbols.DECIMAL_SEP+splited[1], style: smallDecimal ? const TextStyle(fontFamily: "Eurostile Round", fontSize: 16) : null)
if ((fractionDigits??0) > 0 && splited.elementAtOrNull(1) != null) TextSpan(text: f.symbols.DECIMAL_SEP+splited[1], style: smallDecimal ? const TextStyle(fontFamily: "Eurostile Round", fontSize: 16) : null)
],
style: TextStyle(
fontFamily: "Eurostile Round Extended",
@ -106,7 +106,7 @@ class StatCellNum extends StatelessWidget {
);
},
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero)),
padding: WidgetStateProperty.all(EdgeInsets.zero)),
child: Text(
playerStatLabel,
textAlign: TextAlign.center,

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -87,6 +87,9 @@
"verdictBetter": "better",
"verdictWorse": "worse",
"smooth": "Smooth",
"postSeason": "Off-season",
"seasonStarts": "Season starts in:",
"nanow": "Not avaliable for now...",
"seasonEnds": "Season ends in ${countdown}",
"seasonEnded": "Season has ended",
"gamesUntilRanked": "${left} games until being ranked",
@ -101,6 +104,17 @@
"neverPlayedTL": "That user never played Tetra League",
"botTL": "Bots are not allowed to play Tetra League",
"anonTL": "Guests are not allowed to play Tetra League",
"quickPlay": "Quick Play",
"expert": "Expert",
"withMods": "With mods",
"withModsPlural":{
"zero": "with $n mods",
"one": "with $n mod",
"two": "with $n mods",
"few": "with $n mods",
"many": "with $n mods",
"other": "with $n mods"
},
"exportDB": "Export local database",
"exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.",
"desktopExportAlertTitle": "Desktop export",
@ -141,7 +155,7 @@
"stateViewTitle": "${nickname} account on ${date}",
"statesViewTitle": "${number} states of ${nickname} account",
"matchesViewTitle": "${nickname} TL matches",
"statesViewEntry": "Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD",
"statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно",
"stateRemoved": "${date} state was removed from database!",
"matchRemoved": "${date} match was removed from database!",
"viewAllMatches": "View all matches",
@ -255,7 +269,7 @@
"lbpcShort": "№ in local LB",
"gamesPlayed": "Games\nplayed",
"gamesWonTL": "Games\nWon",
"winrate": "Winrate\nprecentage",
"winrate": "Winrate",
"level": "Level",
"score": "Score",
"spp": "Score\nPer Piece",

View File

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

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