first time experience + some fixes...

This commit is contained in:
dan63047 2024-12-12 19:15:44 +03:00
parent 8ded0aeb34
commit 0dae5a73d6
11 changed files with 151 additions and 96 deletions

View File

@ -28,7 +28,7 @@ class SmallLeague{
rd = json['rd'];
tr = json['tr'];
rank = json['rank'];
placement = json['placement'];
placement = json['placement']??-1;
}
}

View File

@ -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<AppLocale, Translations> {
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.';

View File

@ -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: "/",

View File

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

View File

@ -882,7 +882,7 @@ class CompareState extends State<CompareView> {
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<AddNewColumnCard> 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<AddNewColumnCard> 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){

View File

@ -904,13 +904,26 @@ class _DestinationHomeState extends State<DestinationHome> 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<DestinationHome> 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(

View File

@ -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<FirstTimeView> with SingleTickerProviderStat
late Animation<double> _enterNicknameOpacity;
late Animation<double> _transform;
late Animation<Color?> _badNicknameAnim;
late Animation<double> _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<FirstTimeView> with SingleTickerProviderStat
void initState() {
_animController = AnimationController(
vsync: this,
// value: 0,
// lowerBound: 0.0,
// upperBound: 2.0,
duration: Durations.extralong2
);
_spinAnimation = Tween<double>(
@ -53,7 +55,7 @@ class _FirstTimeState extends State<FirstTimeView> with SingleTickerProviderStat
curve: const Interval(
0.5,
0.75,
curve: Easing.emphasizedAccelerate
curve: Curves.easeInCubic
),
));
_opacity = Tween<double>(
@ -77,24 +79,37 @@ class _FirstTimeState extends State<FirstTimeView> with SingleTickerProviderStat
parent: _animController,
curve: const Interval(
0.75,
1.0,
0.9,
curve: Curves.ease,
),
),
);
_transform = Tween<double>(
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<double>(
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<FirstTimeView> 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<FirstTimeView> 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<FirstTimeView> 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<FirstTimeView> 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
)
),
),

View File

@ -121,7 +121,7 @@ class _MainState extends State<MainView> 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), () {

View File

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

View File

@ -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": {

View File

@ -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": {