diff --git a/lib/data_objects/record_extras.dart b/lib/data_objects/record_extras.dart index f6a215d..7003b86 100644 --- a/lib/data_objects/record_extras.dart +++ b/lib/data_objects/record_extras.dart @@ -28,7 +28,7 @@ class SmallLeague{ rd = json['rd']; tr = json['tr']; rank = json['rank']; - placement = json['placement']; + placement = json['placement']??-1; } } diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index a251a80..8adb1dc 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1520 (760 per locale) +/// Strings: 1526 (763 per locale) /// -/// Built on 2024-12-11 at 15:09 UTC +/// Built on 2024-12-12 at 15:53 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -204,6 +204,7 @@ class Translations implements BaseTranslations { String comparingWith({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; String get compare => 'Compare'; String get comparison => 'Comparison'; + String get enterUsername => 'Enter username or \$avgX (where X is rank)'; String get general => 'General'; String get badges => 'Badges'; String obtainDate({required Object date}) => 'Obtained ${date}'; @@ -991,7 +992,9 @@ class _StringsFirstTimeViewEn { String get description => 'Service, that allows you to keep track of various statistics for TETR.IO'; String get nicknameQuestion => 'What\'s your nickname?'; String get inpuntHint => 'Type it here... (3-16 symbols)'; - String get emptyInputError => 'Can\'t submit empty string'; + String get emptyInputError => 'Can\'t submit an empty string'; + String niceToSeeYou({required Object n}) => 'Nice to see you, ${n}'; + String get letsTakeALook => 'Let\'s take a look at your stats...'; String get skip => 'Skip'; } @@ -1739,6 +1742,7 @@ class _StringsRuRu implements Translations { @override String comparingWith({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; @override String get compare => 'Сравнить'; @override String get comparison => 'Сравнение'; + @override String get enterUsername => 'Введите ник или \$avgX (где X это ранг)'; @override String get general => 'Основное'; @override String get badges => 'Значки'; @override String obtainDate({required Object date}) => 'Получен ${date}'; @@ -2527,6 +2531,8 @@ class _StringsFirstTimeViewRuRu implements _StringsFirstTimeViewEn { @override String get nicknameQuestion => 'Введите свой ник'; @override String get inpuntHint => '(3-16 символов)'; @override String get emptyInputError => 'Строка пуста'; + @override String niceToSeeYou({required Object n}) => 'Приятно познакомиться, ${n}'; + @override String get letsTakeALook => 'Давайте же посмотрим на ваши статы...'; @override String get skip => 'Пропустить'; } @@ -3261,6 +3267,7 @@ extension on Translations { case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; case 'compare': return 'Compare'; case 'comparison': return 'Comparison'; + case 'enterUsername': return 'Enter username or \$avgX (where X is rank)'; case 'general': return 'General'; case 'badges': return 'Badges'; case 'obtainDate': return ({required Object date}) => 'Obtained ${date}'; @@ -3534,7 +3541,9 @@ extension on Translations { case 'firstTimeView.description': return 'Service, that allows you to keep track of various statistics for TETR.IO'; case 'firstTimeView.nicknameQuestion': return 'What\'s your nickname?'; case 'firstTimeView.inpuntHint': return 'Type it here... (3-16 symbols)'; - case 'firstTimeView.emptyInputError': return 'Can\'t submit empty string'; + case 'firstTimeView.emptyInputError': return 'Can\'t submit an empty string'; + case 'firstTimeView.niceToSeeYou': return ({required Object n}) => 'Nice to see you, ${n}'; + case 'firstTimeView.letsTakeALook': return 'Let\'s take a look at your stats...'; case 'firstTimeView.skip': return 'Skip'; case 'aboutView.title': return 'About Tetra Stats'; case 'aboutView.about': return 'Tetra Stats is a service, that works with TETR.IO Tetra Channel API, providing data from it and calculating some addtitional metrics, based on this data. Service allows user to track their progress in Tetra League with "Track" function, which records every Tetra League change into local database (not automatically, you have to visit service from time to time), so these changes could be looked through graphs.\n\nBeanserver blaster is a part of a Tetra Stats, that decoupled into a serverside script. It provides full Tetra League leaderboard, allowing Tetra Stats to sort leaderboard by any metric and build scatter chart, that allows user to analyse Tetra League trends. It also provides history of Tetra League ranks cutoffs, which can be viewed by user via graph as well.\n\nThere is a plans to add replay analysis and tournaments history, so stay tuned!\n\nService is not associated with TETR.IO or osk in any capacity.'; @@ -4070,6 +4079,7 @@ extension on _StringsRuRu { case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; case 'compare': return 'Сравнить'; case 'comparison': return 'Сравнение'; + case 'enterUsername': return 'Введите ник или \$avgX (где X это ранг)'; case 'general': return 'Основное'; case 'badges': return 'Значки'; case 'obtainDate': return ({required Object date}) => 'Получен ${date}'; @@ -4344,6 +4354,8 @@ extension on _StringsRuRu { case 'firstTimeView.nicknameQuestion': return 'Введите свой ник'; case 'firstTimeView.inpuntHint': return '(3-16 символов)'; case 'firstTimeView.emptyInputError': return 'Строка пуста'; + case 'firstTimeView.niceToSeeYou': return ({required Object n}) => 'Приятно познакомиться, ${n}'; + case 'firstTimeView.letsTakeALook': return 'Давайте же посмотрим на ваши статы...'; case 'firstTimeView.skip': return 'Пропустить'; case 'aboutView.title': return 'О Tetra Stats'; case 'aboutView.about': return 'Tetra Stats — это сервис, который работает с TETR.IO Tetra Channel API, показывает данные оттуда и считает дополнительную статистику, основанную на этих данных. Сервис позволяет отслеживать прогресс в Тетра Лиге с помощью функции "Отслеживать", которая записывает каждое изменение в Лиге в локальную базу данных (не автоматически, вы должны вручную посещать свой профиль), что позволяет потом просматривать изменения с помощью графиков.\n\nBeanserver blaster — серверная часть Tetra Stats. Она собирает полную таблицу игроков Тетра Лиги, благодаря чему сортировать эту таблицу по любой метрике и строить точечную диаграмму, что позволяет анализировать тренды Лиги. Также она предоставляет историю требований рангов, которую тоже можно посмотреть на графике.\n\nВ будущем планируется добавить анализ повторов и историю турниров, так что оставайтесь на связи.\n\nСервис ни коим образом не ассоциируется с TETR.IO или osk.'; diff --git a/lib/main.dart b/lib/main.dart index d3c1f26..bef51b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -97,8 +97,7 @@ void main() async { teto = TetrioService(); router = GoRouter( - //initialLocation: prefs.getBool("notFirstTime") == true ? "/" : "/hihello", - initialLocation: "/", + initialLocation: prefs.getBool("notFirstTime") == true ? "/" : "/hihello", routes: [ GoRoute( path: "/", diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 4daf927..f0c0938 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; -import 'dart:ffi'; import 'dart:io'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -674,6 +673,7 @@ class TetrioService extends DB { /// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data. Future> fetchAndsaveTLHistory(String id, int season) async { + // TODO: find le way to get season 2 history Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index 1d282cc..576fd72 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -882,7 +882,7 @@ class CompareState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - for (int l = 0; l < formattedValues[1][k].length; l++) Container(decoration: (rawValues[0].length > 1 && rawValues[1][k][l] != null && best[1][l] == rawValues[1][k][l]) ? BoxDecoration(boxShadow: [BoxShadow(color: Colors.cyanAccent.withAlpha(96), spreadRadius: 0, blurRadius: 4)]) : null, child: formattedValues[1][k][l]), + for (int l = 0; l < formattedValues[1][k].length; l++) Container(decoration: (rawValues[1].length > 1 && rawValues[1][k][l] != null && best[1][l] == rawValues[1][k][l]) ? BoxDecoration(boxShadow: [BoxShadow(color: Colors.cyanAccent.withAlpha(96), spreadRadius: 0, blurRadius: 4)]) : null, child: formattedValues[1][k][l]), ], ), ), @@ -1010,6 +1010,8 @@ class AddNewColumnCard extends StatefulWidget{ } class _AddNewColumnCardState extends State with SingleTickerProviderStateMixin { + // TODO: make spinner while awaiting for data + // TODO: show error if failed to retrieve data late AnimationController _animController; late Animation _anim; @@ -1049,7 +1051,7 @@ class _AddNewColumnCardState extends State with SingleTickerPr transform: Matrix4.translationValues(0, 100-(_anim.value as double)*100, 0), child: Column( children: [ - Text("Enter username:"), + Text(t.enterUsername), TextField( autofocus: true, onSubmitted: (value){ diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 9e4aa42..636ec0e 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -904,13 +904,26 @@ class _DestinationHomeState extends State with SingleTickerProv }else{ toSee = states[currentRangeValues.start.round()-1]; } - if (currentRangeValues.end.round() == 0){ - toCompare = states.length >= 2 ? states.elementAtOrNull(states.length-2) : null; + if (currentRangeValues.end.round() == 1){ + toCompare = states.length >= 2 ? states.elementAtOrNull(2) : null; }else{ toCompare = states[currentRangeValues.end.round()-1]; } return Column( children: [ + if (toCompare != null) Card( + child: RangeSlider(values: currentRangeValues, max: states.length.toDouble(), + labels: RangeLabels( + currentRangeValues.start.round().toString(), + currentRangeValues.end.round().toString(), + ), + onChanged: (RangeValues values) { + setState(() { + currentRangeValues = values; + }); + }, + ), + ), Card( //surfaceTintColor: rankColors[data.rank], child: Padding( @@ -927,19 +940,6 @@ class _DestinationHomeState extends State with SingleTickerProv ), ), ), - if (toCompare != null) Card( - child: RangeSlider(values: currentRangeValues, max: states.length.toDouble(), - labels: RangeLabels( - currentRangeValues.start.round().toString(), - currentRangeValues.end.round().toString(), - ), - onChanged: (RangeValues values) { - setState(() { - currentRangeValues = values; - }); - }, - ), - ), TetraLeagueThingy(league: toSee, toCompare: toCompare, cutoffs: cutoffs, averages: averages, lbPos: lbPos, width: width), // Center( // child: Card( diff --git a/lib/views/first_time_view.dart b/lib/views/first_time_view.dart index a5a2e4d..67173d7 100644 --- a/lib/views/first_time_view.dart +++ b/lib/views/first_time_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:tetra_stats/data_objects/tetrio_player.dart'; @@ -19,7 +21,10 @@ class _FirstTimeState extends State with SingleTickerProviderStat late Animation _enterNicknameOpacity; late Animation _transform; late Animation _badNicknameAnim; + late Animation _fadeOutOpacity; late TextEditingController _controller; + String title = t.firstTimeView.welcome; + String subtitle = t.firstTimeView.description; String helperText = ""; String nickname = ""; double helperTextOpacity = 0; @@ -29,9 +34,6 @@ class _FirstTimeState extends State with SingleTickerProviderStat void initState() { _animController = AnimationController( vsync: this, - // value: 0, - // lowerBound: 0.0, - // upperBound: 2.0, duration: Durations.extralong2 ); _spinAnimation = Tween( @@ -53,7 +55,7 @@ class _FirstTimeState extends State with SingleTickerProviderStat curve: const Interval( 0.5, 0.75, - curve: Easing.emphasizedAccelerate + curve: Curves.easeInCubic ), )); _opacity = Tween( @@ -77,24 +79,37 @@ class _FirstTimeState extends State with SingleTickerProviderStat parent: _animController, curve: const Interval( 0.75, - 1.0, + 0.9, curve: Curves.ease, ), ), ); _transform = Tween( begin: 0.0, - end: 40.0 + end: 150.0 ).animate( CurvedAnimation( parent: _animController, curve: const Interval( 0.75, - 1.0, + 0.9, curve: Curves.easeInOut, ), ), ); + _fadeOutOpacity = Tween( + begin: 1.0, + end: 0.0 + ).animate( + CurvedAnimation( + parent: _animController, + curve: const Interval( + 0.9, + 1.0, + curve: Curves.ease, + ), + ), + ); _controller = TextEditingController(); super.initState(); } @@ -121,17 +136,23 @@ class _FirstTimeState extends State with SingleTickerProviderStat TetrioPlayer player = await teto.fetchPlayer(n); nickname = player.username; await prefs.setString('playerID', player.userId); + if(!(await teto.isPlayerTracking(player.userId))) await teto.addPlayerToTrack(player); } await prefs.setString('player', nickname); + await prefs.setBool("notFirstTime", true); helperText = ""; - _animController.forward(); + _animController.animateTo(0.9); setState((){ userSet = true; + title = "Nice to see you, ${nickname}"; + subtitle = "Let's take a look at your stats..."; }); + Timer(Duration(seconds: 2), () => _animController.animateTo(1.0, duration: Duration(seconds: 1))); + Timer(Duration(seconds: 3), () => context.replace("/")); return true; } catch (e) { _animController.value = 0.5; - _animController.animateTo(1.0, duration: Durations.long1); + _animController.animateTo(0.75, duration: Duration(seconds: 1)); setState((){ helperText = t.settingsDestination.noSuchAccount; }); @@ -139,7 +160,7 @@ class _FirstTimeState extends State with SingleTickerProviderStat } } else { _animController.value = 0.5; - _animController.animateTo(1.0, duration: Durations.long1); + _animController.animateTo(0.75, duration: Durations.long1); setState((){ helperText = t.firstTimeView.emptyInputError; }); @@ -147,6 +168,76 @@ class _FirstTimeState extends State with SingleTickerProviderStat } } + Widget _buildAnimation(BuildContext context, Widget? child) { + return Center( + child: Container( + transform: Matrix4.translationValues(0, _transform.value, 0), + child: Opacity( + opacity: _fadeOutOpacity.value, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: RotationTransition( + turns: _spinAnimation, + child: Image.asset("res/icons/app.png", height: 128, opacity: _opacity) + ), + ), + Text(title, style: Theme.of(context).textTheme.titleLarge), + Text(subtitle, style: TextStyle(color: Colors.grey)), + Opacity( + opacity: _enterNicknameOpacity.value, + child: Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.firstTimeView.nicknameQuestion, style: Theme.of(context).textTheme.titleSmall), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: SizedBox(width: 400.0, child: Focus( + onFocusChange: (value) { + setState((){if (value) helperTextOpacity = 0;}); + }, + child: TextField( + controller: _controller, + maxLength: 16, + textAlign: TextAlign.center, + enabled: !userSet, + decoration: InputDecoration( + hintText: t.firstTimeView.inpuntHint, + helper: Opacity( + opacity: helperTextOpacity, + child: Text(helperText, style: TextStyle(fontFamily: "Eurostile Round", color: _badNicknameAnim.value, height: 0.5)) + ), + counter: const Offstage() + ), + onSubmitted: (value) => _setDefaultNickname(value), + ), + )), + ), + ElevatedButton.icon(onPressed: !userSet ? () => _setDefaultNickname(_controller.value.text) : null, icon: Icon(Icons.subdirectory_arrow_left), label: Text(t.actions.submit)) + ], + ), + ), + ), + ), + ), + Spacer(flex: 2), + TextButton(onPressed: (){ context.replace("/"); }, child: Text(t.firstTimeView.skip)) + ], + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -164,64 +255,9 @@ class _FirstTimeState extends State with SingleTickerProviderStat child: Opacity(opacity: value, child: child), ); }, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Spacer(), - Padding( - padding: const EdgeInsets.only(bottom: 24.0), - child: RotationTransition( - turns: _spinAnimation, - child: Image.asset("res/icons/app.png", height: 128, opacity: _opacity) - ), - ), - Text(t.firstTimeView.welcome, style: Theme.of(context).textTheme.titleLarge), - Text(t.firstTimeView.description, style: TextStyle(color: Colors.grey)), - Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.firstTimeView.nicknameQuestion, style: Theme.of(context).textTheme.titleSmall), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: SizedBox(width: 400.0, child: Focus( - onFocusChange: (value) { - setState((){if (value) helperTextOpacity = 0;}); - }, - child: TextField( - controller: _controller, - maxLength: 16, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintText: t.firstTimeView.inpuntHint, - helper: AnimatedOpacity( - opacity: helperTextOpacity, - duration: Durations.long1, - curve: Easing.standardDecelerate, - child: AnimatedDefaultTextStyle(child: Text(helperText), style: TextStyle(fontFamily: "Eurostile Round", color: _badNicknameAnim.value, height: 0.5), duration: Durations.long1) - ), - counter: const Offstage() - ), - onSubmitted: (value) => _setDefaultNickname(value), - ), - )), - ), - ElevatedButton.icon(onPressed: () => _setDefaultNickname(_controller.value.text), icon: Icon(Icons.subdirectory_arrow_left), label: Text(t.actions.submit)) - ], - ), - ), - ), - ), - Spacer(flex: 2), - TextButton(onPressed: (){ context.replace("/"); }, child: Text(t.firstTimeView.skip)) - ], - ), + child: AnimatedBuilder( + animation: _animController, + builder: _buildAnimation ) ), ), diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 2bff835..42adc3b 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -121,7 +121,7 @@ class _MainState extends State with TickerProviderStateMixin { void initState() { teto.open(); controller = ScrollController(); - changePlayer(_searchFor); + changePlayer(prefs.getString('playerID')??_searchFor); if (prefs.getBool("updateInBG") == true) { _backgroundUpdate = Timer(Duration(minutes: 5), () { diff --git a/lib/widgets/beta_league_entry_thingy.dart b/lib/widgets/beta_league_entry_thingy.dart index e012d84..8b989eb 100644 --- a/lib/widgets/beta_league_entry_thingy.dart +++ b/lib/widgets/beta_league_entry_thingy.dart @@ -50,7 +50,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{ } Color deltaColor(double? delta){ - if (delta == null || delta.isNaN) return Colors.grey; + if (delta == null || delta.isNaN || ["nocontest", "nullified"].contains(record.extras.result)) return Colors.grey; if (delta.isNegative) return Colors.redAccent; else return Colors.greenAccent; } diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 533b546..02c2e1e 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -72,6 +72,7 @@ "comparingWith": "Data from ${newDate} comparing with ${oldDate}", "compare": "Compare", "comparison": "Comparison", + "enterUsername": "Enter username or \\$avgX (where X is rank)", "general": "General", "badges": "Badges", "obtainDate": "Obtained ${date}", @@ -359,7 +360,9 @@ "description": "Service, that allows you to keep track of various statistics for TETR.IO", "nicknameQuestion": "What's your nickname?", "inpuntHint": "Type it here... (3-16 symbols)", - "emptyInputError": "Can't submit empty string", + "emptyInputError": "Can't submit an empty string", + "niceToSeeYou": "Nice to see you, $n", + "letsTakeALook": "Let's take a look at your stats...", "skip": "Skip" }, "aboutView": { diff --git a/res/i18n/strings_ru-RU.i18n.json b/res/i18n/strings_ru-RU.i18n.json index 9adf45e..293b4ed 100644 --- a/res/i18n/strings_ru-RU.i18n.json +++ b/res/i18n/strings_ru-RU.i18n.json @@ -72,6 +72,7 @@ "comparingWith": "Данные от ${newDate} в сравнении с данными от ${oldDate}", "compare": "Сравнить", "comparison": "Сравнение", + "enterUsername": "Введите ник или \\$avgX (где X это ранг)", "general": "Основное", "badges": "Значки", "obtainDate": "Получен ${date}", @@ -360,6 +361,8 @@ "nicknameQuestion": "Введите свой ник", "inpuntHint": "(3-16 символов)", "emptyInputError": "Строка пуста", + "niceToSeeYou": "Приятно познакомиться, $n", + "letsTakeALook": "Давайте же посмотрим на ваши статы...", "skip": "Пропустить" }, "aboutView": {