From fcab60f7ba9cd6cbc344a1e9ff56e74eb62b8f87 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 4 May 2024 23:04:48 +0300 Subject: [PATCH] i guess 1.5.1 is ready --- .../tetrio_multiplayer_replay.dart | 1 - lib/gen/strings.g.dart | 56 +++++- lib/services/tetrio_crud.dart | 56 +++++- lib/views/calc_view.dart | 2 +- lib/views/compare_view.dart | 8 +- lib/views/customization_view.dart | 2 - lib/views/main_view.dart | 172 +++++++++--------- lib/views/rank_averages_view.dart | 18 +- lib/views/ranks_averages_view.dart | 2 +- lib/views/settings_view.dart | 10 +- lib/views/tl_match_view.dart | 9 +- lib/widgets/graphs.dart | 107 +++++------ lib/widgets/tl_progress_bar.dart | 33 ++-- lib/widgets/tl_thingy.dart | 28 +-- lib/widgets/user_thingy.dart | 5 +- lib/widgets/vs_graphs.dart | 66 +++++++ pubspec.yaml | 2 +- res/i18n/strings.i18n.json | 10 + res/i18n/strings_ru.i18n.json | 10 + 19 files changed, 380 insertions(+), 217 deletions(-) diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index e453dd3..406675b 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,5 +1,4 @@ import 'dart:math'; -import 'package:vector_math/vector_math_64.dart'; import 'tetrio.dart'; diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 461928b..fab5778 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: 1122 (561 per locale) +/// Strings: 1138 (569 per locale) /// -/// Built on 2024-04-11 at 22:23 UTC +/// Built on 2024-05-04 at 19:13 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -218,6 +218,10 @@ class Translations implements BaseTranslations { String get verdictWorse => 'worse'; String get smooth => 'Smooth'; String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; + String numOfVictories({required Object wins}) => '~${wins} victories'; + String get promotionOnNextWin => 'Promotion on next win'; + String numOfdefeats({required Object losses}) => '~${losses} defeats'; + String get demotionOnNextLoss => 'Demotion on next loss'; String get nerdStats => 'Nerd Stats'; String get playersYouTrack => 'Players you track'; String get formula => 'Formula'; @@ -336,6 +340,7 @@ class Translations implements BaseTranslations { String currentAxis({required Object axis}) => '${axis} axis:'; String get p1nkl0bst3rAlert => 'That data was retrived from third party API maintained by p1nkl0bst3r'; String get notForWeb => 'Function is not available for web version'; + late final _StringsGraphsEn graphs = _StringsGraphsEn._(_root); late final _StringsStatCellNumEn statCellNum = _StringsStatCellNumEn._(_root); Map get playerRole => { 'user': 'User', @@ -635,6 +640,19 @@ class _StringsNewsPartsEn { String unknownNews({required Object type}) => 'Unknown news of type ${type}'; } +// Path: graphs +class _StringsGraphsEn { + _StringsGraphsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get attack => 'Attack'; + String get speed => 'Speed'; + String get defense => 'Defense'; + String get cheese => 'Cheese'; +} + // Path: statCellNum class _StringsStatCellNumEn { _StringsStatCellNumEn._(this._root); @@ -873,6 +891,10 @@ class _StringsRu implements Translations { @override String get verdictWorse => 'Хуже'; @override String get smooth => 'Гладкий'; @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; + @override String numOfVictories({required Object wins}) => '~${wins} побед'; + @override String get promotionOnNextWin => 'Повышение после следующей победы'; + @override String numOfdefeats({required Object losses}) => '~${losses} поражений'; + @override String get demotionOnNextLoss => 'Понижение после следующего поражения'; @override String get nerdStats => 'Для задротов'; @override String get playersYouTrack => 'Отслеживаемые игроки'; @override String get formula => 'Формула'; @@ -991,6 +1013,7 @@ class _StringsRu implements Translations { @override String currentAxis({required Object axis}) => 'Ось ${axis}:'; @override String get p1nkl0bst3rAlert => 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; @override String get notForWeb => 'Функция недоступна для веб версии'; + @override late final _StringsGraphsRu graphs = _StringsGraphsRu._(_root); @override late final _StringsStatCellNumRu statCellNum = _StringsStatCellNumRu._(_root); @override Map get playerRole => { 'user': 'Пользователь', @@ -1290,6 +1313,19 @@ class _StringsNewsPartsRu implements _StringsNewsPartsEn { @override String unknownNews({required Object type}) => 'Неизвестная новость типа ${type}'; } +// Path: graphs +class _StringsGraphsRu implements _StringsGraphsEn { + _StringsGraphsRu._(this._root); + + @override final _StringsRu _root; // ignore: unused_field + + // Translations + @override String get attack => 'Атака'; + @override String get speed => 'Скорость'; + @override String get defense => 'Защита'; + @override String get cheese => 'Сыр'; +} + // Path: statCellNum class _StringsStatCellNumRu implements _StringsStatCellNumEn { _StringsStatCellNumRu._(this._root); @@ -1520,6 +1556,10 @@ extension on Translations { case 'verdictWorse': return 'worse'; case 'smooth': return 'Smooth'; case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; + case 'numOfVictories': return ({required Object wins}) => '~${wins} victories'; + case 'promotionOnNextWin': return 'Promotion on next win'; + case 'numOfdefeats': return ({required Object losses}) => '~${losses} defeats'; + case 'demotionOnNextLoss': return 'Demotion on next loss'; case 'nerdStats': return 'Nerd Stats'; case 'playersYouTrack': return 'Players you track'; case 'formula': return 'Formula'; @@ -1638,6 +1678,10 @@ extension on Translations { case 'currentAxis': return ({required Object axis}) => '${axis} axis:'; case 'p1nkl0bst3rAlert': return 'That data was retrived from third party API maintained by p1nkl0bst3r'; case 'notForWeb': return 'Function is not available for web version'; + case 'graphs.attack': return 'Attack'; + case 'graphs.speed': return 'Speed'; + case 'graphs.defense': return 'Defense'; + case 'graphs.cheese': return 'Cheese'; case 'statCellNum.xpLevel': return 'XP Level'; case 'statCellNum.xpProgress': return 'Progress to next level'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Progress from 0 XP to level ${n}'; @@ -2101,6 +2145,10 @@ extension on _StringsRu { case 'verdictWorse': return 'Хуже'; case 'smooth': return 'Гладкий'; case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; + case 'numOfVictories': return ({required Object wins}) => '~${wins} побед'; + case 'promotionOnNextWin': return 'Повышение после следующей победы'; + case 'numOfdefeats': return ({required Object losses}) => '~${losses} поражений'; + case 'demotionOnNextLoss': return 'Понижение после следующего поражения'; case 'nerdStats': return 'Для задротов'; case 'playersYouTrack': return 'Отслеживаемые игроки'; case 'formula': return 'Формула'; @@ -2219,6 +2267,10 @@ extension on _StringsRu { case 'currentAxis': return ({required Object axis}) => 'Ось ${axis}:'; case 'p1nkl0bst3rAlert': return 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; case 'notForWeb': return 'Функция недоступна для веб версии'; + case 'graphs.attack': return 'Атака'; + case 'graphs.speed': return 'Скорость'; + case 'graphs.defense': return 'Защита'; + case 'graphs.cheese': return 'Сыр'; case 'statCellNum.xpLevel': return 'Уровень\nопыта'; case 'statCellNum.xpProgress': return 'Прогресс до следующего уровня'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Прогресс от 0 XP до ${n} уровня'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index d4fd561..8c118ae 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -70,6 +70,7 @@ class TetrioService extends DB { // I'm trying to send as less requests, as possible, so i'm caching the results of those requests. // Usually those maps looks like this: {"cached_until_unix_milliseconds": Object} + // TODO: Make a proper caching system final Map _playersCache = {}; final Map> _recordsCache = {}; final Map _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]} @@ -78,6 +79,7 @@ class TetrioService extends DB { final Map> _newsCache = {}; final Map> _topTRcache = {}; final Map>> _cutoffsCache = {}; + final Map _topOneFromLB = {}; final Map _tlStreamsCache = {}; /// Thing, that sends every request to the API endpoints final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); @@ -314,7 +316,7 @@ class TetrioService extends DB { Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "PeakTR"}); + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLCutoffs"}); } else { url = Uri.https('api.p1nkl0bst3r.xyz', 'rankcutoff', {"users": null}); } @@ -359,6 +361,58 @@ class TetrioService extends DB { } } + Future fetchTopOneFromTheLeaderboard() async { + try{ + var cached = _topOneFromLB.entries.first; + if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired + developer.log("fetchTopOneFromTheLeaderboard: Leader retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); + return cached.value; + }else{ // if cache expired + _topTRcache.remove(cached.key); + developer.log("fetchTopOneFromTheLeaderboard: Leader expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); + } + }catch(e){ // actually going to obtain + developer.log("fetchTopOneFromTheLeaderboard: Trying to retrieve leader", name: "services/tetrio_crud"); + } + + Uri url; + 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"}); + } + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + var rawJson = jsonDecode(response.body); + return TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["users"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"])); + case 404: + throw TetrioPlayerNotExist(); + // 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: + throw P1nkl0bst3rInternalProblem(); + default: + developer.log("fetchTopOneFromTheLeaderboard: Failed to fetch top one", 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 + } + } + /// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data. Future> fetchAndsaveTLHistory(String id) async { diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart index 5688d26..001d87d 100644 --- a/lib/views/calc_view.dart +++ b/lib/views/calc_view.dart @@ -109,7 +109,7 @@ class CalcState extends State { ], ), ), - Divider(), + const Divider(), if (nerdStats == null) Text(t.calcViewNoValues) else Column(children: [ _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 58c893c..3129ea8 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -257,7 +257,7 @@ class CompareState extends State { backgroundColor: Colors.black, body: SingleChildScrollView( controller: _scrollController, - physics: AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 768), @@ -317,7 +317,7 @@ class CompareState extends State { ], ), ), - Divider(), + const Divider(), if (!listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])) Column( children: [ if (theGreenSide[0] != null && theRedSide[0] != null && theGreenSide[0]!.role != "banned" && theRedSide[0]!.role != "banned") @@ -820,7 +820,7 @@ class CompareThingy extends StatelessWidget { colors: const [Colors.green, Colors.transparent], begin: Alignment.centerLeft, end: Alignment.centerRight, - transform: GradientRotation(0.6), + transform: const GradientRotation(0.6), stops: [ 0.0, higherIsBetter @@ -881,7 +881,7 @@ class CompareThingy extends StatelessWidget { colors: const [Colors.red, Colors.transparent], begin: Alignment.centerRight, end: Alignment.centerLeft, - transform: GradientRotation(-0.6), + transform: const GradientRotation(-0.6), stops: [ 0.0, higherIsBetter diff --git a/lib/views/customization_view.dart b/lib/views/customization_view.dart index e7c22a4..4d85148 100644 --- a/lib/views/customization_view.dart +++ b/lib/views/customization_view.dart @@ -1,10 +1,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart'; import 'package:window_manager/window_manager.dart'; late String oldWindowTitle; diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 7be92d5..4408eea 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,6 +1,5 @@ // ignore_for_file: type_literal_in_constant_pattern -import 'dart:ffi'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -8,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; -import 'dart:math'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; @@ -177,18 +175,21 @@ class _MainState extends State with TickerProviderStateMixin { late TetraLeagueAlphaStream tlStream; late Map records; late List news; + late TetrioPlayerFromLeaderboard? topOne; late double? topTR; requests = await Future.wait([ // all at once teto.fetchTLStream(_searchFor), teto.fetchRecords(_searchFor), teto.fetchNews(_searchFor), - prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>[]), + prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>>[]), + (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR ]); tlStream = requests[0] as TetraLeagueAlphaStream; records = requests[1] as Map; news = requests[2] as List; - topTR = requests.elementAtOrNull(4) as double?; // No TR - no Top TR + topOne = requests[4] as TetrioPlayerFromLeaderboard?; + topTR = requests.elementAtOrNull(5) as double?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); if (prefs.getBool("showPositions") == true){ @@ -200,15 +201,14 @@ class _MainState extends State with TickerProviderStateMixin { if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } } - Map cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : requests[3][0]; - Map cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : requests[3][1]; + Map? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as List>).elementAtOrNull(0); + Map? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as List>).elementAtOrNull(1); + 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 = cutoffs[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; - nextRankGlickoCutoff = cutoffsGlicko[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; - nextRankCutoff = nextRankCutoff??25000; - nextRankGlickoCutoff = nextRankGlickoCutoff??double.infinity; + 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 (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; @@ -487,6 +487,12 @@ class _MainState extends State with TickerProviderStateMixin { topTR: snapshot.data![7], 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, + 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, averages: rankAverages, lbPositions: meAmongEveryone ), @@ -543,7 +549,7 @@ class _MainState extends State with TickerProviderStateMixin { Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), if (subText != null) Padding( padding: const EdgeInsets.only(top: 8.0), - child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18)), + child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), ), ], ) @@ -781,71 +787,67 @@ class _History extends StatelessWidget{ List<_HistoryChartSpot> selectedGraph = chartsData[_chartsIndex].value!; return SingleChildScrollView( scrollDirection: Axis.vertical, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - primary: true, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: const [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], - value: _gamesPlayedInsteadOfDateAndTime, - onChanged: (value) { - _gamesPlayedInsteadOfDateAndTime = value!; - update(); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: chartsData, - value: chartsData[_chartsIndex].value, - onChanged: (value) { - _chartsIndex = chartsData.indexWhere((element) => element.value == value); - update(); - } - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: const [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], + value: _gamesPlayedInsteadOfDateAndTime, + onChanged: (value) { + _gamesPlayedInsteadOfDateAndTime = value!; + update(); + } + ), ], - ), - if (selectedGraph.length > 300) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox(value: _smooth, - checkColor: Colors.black, - onChanged: ((value) { - _smooth = value!; - update(); - })), - Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: 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( - 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)) - ], - )) - ], - ), - ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: chartsData, + value: chartsData[_chartsIndex].value, + onChanged: (value) { + _chartsIndex = chartsData.indexWhere((element) => element.value == value); + update(); + } + ), + ], + ), + if (selectedGraph.length > 300) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox(value: _smooth, + checkColor: Colors.black, + onChanged: ((value) { + _smooth = value!; + update(); + })), + Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) + ], + ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) + ], + ), + if(chartsData[_chartsIndex].value!.length > 1) _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( + 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)) + ], + )) + ], + ), ); } } @@ -901,10 +903,10 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { padding: const EdgeInsets.only(bottom: 8.0), child: Text( "${f4.format(data.stat)} ${widget.yAxisTitle}", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), ), ), - Text(_gamesPlayedInsteadOfDateAndTime ? "${f0.format(data.gamesPlayed)} games played" : _dateFormat.format(data.timestamp)) + Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : _dateFormat.format(data.timestamp)) ], ), ); @@ -943,8 +945,8 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { child: SfCartesianChart( tooltipBehavior: _tooltipBehavior, zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? NumericAxis() : DateTimeAxis(), - primaryYAxis: NumericAxis( + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( rangePadding: ChartRangePadding.additional, ), series: [ @@ -1079,7 +1081,7 @@ class _TwoRecordsThingy extends StatelessWidget { ), 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) Text("${sprint!.endContext!.inputs} KP • ${f2.format(sprint!.endContext!.kps)} KPS") ] ), Column( @@ -1140,7 +1142,7 @@ class _TwoRecordsThingy extends StatelessWidget { ), 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) Text("${blitz!.endContext!.piecesPlaced} P • ${blitz!.endContext!.inputs} KP • ${f2.format(blitz!.endContext!.kpp)} KPP • ${f2.format(blitz!.endContext!.kps)} KPS") ], ), ]), @@ -1256,8 +1258,8 @@ class _RecordThingy extends StatelessWidget { ), FinesseThingy(record?.endContext?.finesse, record?.endContext?.finessePercentage), LineclearsThingy(record!.endContext!.clears, record!.endContext!.lines, record!.endContext!.holds, record!.endContext!.tSpins), - if (record!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KpS"), - if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KpP • ${f2.format(record!.endContext!.kps)} KpS") + if (record!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KPS"), + if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KPP • ${f2.format(record!.endContext!.kps)} KPS") ] ), ), diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index ced944e..b55fae0 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -7,7 +6,6 @@ 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/views/main_view.dart' show MainView; -import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:window_manager/window_manager.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; @@ -81,7 +79,7 @@ class RankState extends State with SingleTickerProviderStateMixin { padding: const EdgeInsets.only(bottom: 8.0), child: Text( "${data.nickname} (${data.rank.toUpperCase()})", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20), + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20), ), ), Text('${_f4.format(data.x)} ${chartsShortTitles[_chartsX]}\n${_f4.format(data.y)} ${chartsShortTitles[_chartsY]}') @@ -241,7 +239,7 @@ class RankState extends State with SingleTickerProviderStateMixin { ), ], ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: Icon(Icons.refresh), alignment: Alignment.center,) + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) ], ), if (widget.rank[1]["entries"].length > 1) @@ -320,7 +318,9 @@ class RankState extends State with SingleTickerProviderStateMixin { checkColor: Colors.black, onChanged: ((value) { _reversed = value!; - setState(() {}); + setState(() { + they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); + }); }), ), ), @@ -337,7 +337,9 @@ class RankState extends State with SingleTickerProviderStateMixin { value: _country, onChanged: ((value) { _country = value; - setState(() {}); + setState(() { + they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); + }); }), ), ], @@ -352,7 +354,9 @@ class RankState extends State with SingleTickerProviderStateMixin { bool bigScreen = MediaQuery.of(context).size.width > 768; return ListTile( title: Text(they[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - subtitle: Text(_sortBy == Stats.tr ? "${_f2.format(they[index].apm)} APM, ${_f2.format(they[index].pps)} PPS, ${_f2.format(they[index].vs)} VS, ${_f2.format(they[index].nerdStats.app)} APP, ${_f2.format(they[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(they[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}"), + subtitle: Text( + _sortBy == Stats.tr ? "${_f2.format(they[index].apm)} APM, ${_f2.format(they[index].pps)} PPS, ${_f2.format(they[index].vs)} VS, ${_f2.format(they[index].nerdStats.app)} APP, ${_f2.format(they[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(they[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", + style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index b82d8f3..e45e512 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -55,7 +55,7 @@ class RanksAverages extends State { 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: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), + style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), onTap: (){ if (averages[keys[index]]?[1]["players"] > 0) { diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index d9ae7f7..92670d5 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -97,7 +97,7 @@ class SettingsState extends State { children: [ ListTile( title: Text(t.exportDB), - subtitle: Text(t.exportDBDescription), + subtitle: Text(t.exportDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), onTap: () { if (kIsWeb){ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); @@ -151,7 +151,7 @@ class SettingsState extends State { ), ListTile( title: Text(t.importDB), - subtitle: Text(t.importDBDescription), + subtitle: Text(t.importDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), onTap: () { if (kIsWeb){ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); @@ -262,13 +262,13 @@ class SettingsState extends State { ), ), ListTile(title: Text(t.customization), - subtitle: Text(t.customizationDescription), + subtitle: Text(t.customizationDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: const Icon(Icons.arrow_right), onTap: () { context.go("/customization"); },), ListTile(title: Text(t.lbStats), - subtitle: Text(t.lbStatsDescription), + subtitle: Text(t.lbStatsDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), trailing: Switch(value: showPositions, onChanged: (bool value){ prefs.setBool("showPositions", value); setState(() { @@ -280,7 +280,7 @@ class SettingsState extends State { onTap: (){ launchInBrowser(Uri.https("github.com", "dan63047/TetraStats")); }, - title: Text(t.aboutApp), + title: Text(t.aboutApp, style: TextStyle(fontWeight: FontWeight.w500),), subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)), trailing: const Icon(Icons.arrow_right) ), diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 048f882..9dc15b9 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -1,9 +1,6 @@ // ignore_for_file: use_build_context_synchronously import 'dart:io'; -import 'dart:math'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/widgets.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy; @@ -234,12 +231,12 @@ class TlMatchResultState extends State { 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,), + 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,), + 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), @@ -250,7 +247,7 @@ class TlMatchResultState extends State { 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,), + 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), diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index 990db42..f8dd910 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.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'; class Graphs extends StatelessWidget{ @@ -108,59 +109,6 @@ class Graphs extends StatelessWidget{ ), ), ), - Padding( // sq graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: RadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), - titleTextStyle: const TextStyle(height: 1.1), - radarTouchData: RadarTouchData(), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: 'Attack\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: 'Speed\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: 'Defense\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: 'Cheese\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - dataEntries: [ - RadarEntry(value: attack), - RadarEntry(value: speed), - RadarEntry(value: defense), - RadarEntry(value: cheese), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 1.2), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ) - ], - ) - ) - ) - ), Padding( // psq graph padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), child: SizedBox( @@ -216,6 +164,59 @@ class Graphs extends StatelessWidget{ ), ), ), + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: RadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: '${t.graphs.attack}\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: '${t.graphs.speed}\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: '${t.graphs.defense}\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: '${t.graphs.cheese}\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + dataEntries: [ + RadarEntry(value: attack), + RadarEntry(value: speed), + RadarEntry(value: defense), + RadarEntry(value: cheese), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1.2), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ) + ) + ) + ) ], ); } diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index d3c549c..a2f6dbd 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -31,6 +31,7 @@ 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(); 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), @@ -48,13 +49,13 @@ class TLProgress extends StatelessWidget{ child: RichText( textAlign: TextAlign.left, text: TextSpan( - style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + 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) TextSpan(text: "\n"), - if (previousRankTRcutoff != null) TextSpan(text: "${intf.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"), - if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) TextSpan(text: "\n"), - if (previousGlickoCutoff != null) TextSpan(text: "~${f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)} defeats") + if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"), + if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"), + if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null)) ] ) ), @@ -63,13 +64,13 @@ class TLProgress extends StatelessWidget{ child: RichText( textAlign: TextAlign.right, text: TextSpan( - style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + 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) TextSpan(text: "\n"), - if (nextRankTRcutoff != null) TextSpan(text: "${intf.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), - if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) TextSpan(text: "\n"), - if (nextRankGlickoCutoff != null) TextSpan(text: "~${f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)} victories") + if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), + if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), + if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || (nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || (nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5) ? Colors.greenAccent : null)) ] ) ), @@ -84,22 +85,14 @@ class TLProgress extends StatelessWidget{ if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.rating)!, color: Colors.cyanAccent, position: LinearElementPosition.cross) else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Colors.cyanAccent, position: LinearElementPosition.cross), if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent, position: LinearElementPosition.inside), - if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside) + if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside) ], markerPointers: [ 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: TextStyle(fontSize: 14),)) + 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),)) ], isMirrored: true, showTicks: true, - // onGenerateLabels: () => [ - // LinearAxisLabel(text: "${tlData.prevAt > 0 ? "№ ${f0.format(tlData.prevAt)}" : ""}\n ${intf.format(previousRankTRcutoff)} TR", value: 0), - // LinearAxisLabel(text: "${tlData.nextAt > 0 ? "№ ${f0.format(tlData.nextAt)}" : ""}\n ${intf.format(nextRankTRcutoff)} TR", value: 1), - // ], - // labelFormatterCallback: (value) { - // if (value == "0") return "${f0.format(previousRankPosition)}\n 26,700 TR"; - // else return f0.format(nextRankPosition); - // }, showLabels: false ) ] diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 3c02508..a70dc81 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -49,11 +49,7 @@ class _TLThingyState extends State { _currentRangeValues = const RangeValues(0, 1); sortedStates = widget.states.reversed.toList(); oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; - try{ - oldTl = sortedStates[1].tlSeason1; - }on RangeError{ - oldTl = null; - } + oldTl = sortedStates.elementAtOrNull(1)?.tlSeason1; currentTl = widget.tl; super.initState(); } @@ -142,27 +138,7 @@ class _TLThingyState extends State { ), ], ), - if (currentTl.gamesPlayed > 9) - // if (currentTl.gamesPlayed >= 10 && currentTl.rd! < 100 && currentTl.nextAt >=0 && currentTl.prevAt >= 0) - // Padding( - // padding: const EdgeInsets.all(8.0), - // child: SfLinearGauge( - // minimum: currentTl.nextAt.toDouble(), - // maximum: currentTl.prevAt.toDouble(), - // interval: currentTl.prevAt.toDouble() - currentTl.nextAt.toDouble(), - // ranges: [ - // LinearGaugeRange(startValue: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() : currentTl.prevAt.toDouble(), endValue: currentTl.prevAt.toDouble(), color: Colors.cyanAccent, position: LinearElementPosition.cross,), - // //LinearGaugeRange(startValue: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() + 500.00 : currentTl.prevAt.toDouble(), endValue: currentTl.prevAt.toDouble(), color: Colors.amber, position: LinearElementPosition.inside,) - // ], - // markerPointers: [LinearShapePointer(value: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() : currentTl.prevAt.toDouble(), position: LinearElementPosition.inside, shapeType: LinearShapePointerType.triangle, color: Colors.white, height: 20), - // LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: currentTl.standing.toDouble() <= currentTl.prevAt.toDouble() ? currentTl.standing.toDouble() : currentTl.prevAt.toDouble(), child: Text(NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(currentTl.standing)))], - // isAxisInversed: true, - // isMirrored: true, - // showTicks: true, - // showLabels: true - // ), - // ), - TLProgress( + if (currentTl.gamesPlayed > 9) TLProgress( tlData: currentTl, previousRankTRcutoff: widget.thatRankCutoff, previousGlickoCutoff: widget.thatRankCutoffGlicko, diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index a900051..e197181 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; @@ -52,7 +53,7 @@ class UserThingy extends StatelessWidget { alignment: Alignment.topCenter, children: [ if (player.bannerRevision != null) - Image.network("https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", + Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", fit: BoxFit.cover, height: bannerHeight, errorBuilder: (context, error, stackTrace) { @@ -90,7 +91,7 @@ class UserThingy extends StatelessWidget { child: player.role == "banned" ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) : player.avatarRevision != null - ? Image.network("https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + ? Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", // TODO: osk banner can cause memory leak fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace); diff --git a/lib/widgets/vs_graphs.dart b/lib/widgets/vs_graphs.dart index d40e809..2ac5eea 100644 --- a/lib/widgets/vs_graphs.dart +++ b/lib/widgets/vs_graphs.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; class VsGraphs extends StatelessWidget{ final double greenAPM; @@ -205,6 +206,71 @@ class VsGraphs extends StatelessWidget{ ), ), ), + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: RadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: t.graphs.attack, angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: t.graphs.speed, angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: t.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: t.graphs.cheese, angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: const Color.fromARGB(115, 76, 175, 79), + borderColor: Colors.green, + dataEntries: [ + RadarEntry(value: greenAPM / 60 * 0.4), + RadarEntry(value: greenPPS / 3.75), + RadarEntry(value: greenNerdStats.dss * 1.15), + RadarEntry(value: greenNerdStats.cheese / 110), + ], + ), + RadarDataSet( + fillColor: const Color.fromARGB(115, 244, 67, 54), + borderColor: Colors.red, + dataEntries: [ + RadarEntry(value: redAPM / 60 * 0.4), + RadarEntry(value: redPPS / 3.75), + RadarEntry(value: redNerdStats.dss * 1.15), + RadarEntry(value: redNerdStats.cheese / 110), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1.2), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ) + ) + ) + ) ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index 79ace8e..0c10373 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.5.0+16 +version: 1.5.1+17 environment: sdk: '>=3.0.0' diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index d01705a..ef7ddef 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -83,6 +83,10 @@ "verdictWorse": "worse", "smooth": "Smooth", "gamesUntilRanked": "${left} games until being ranked", + "numOfVictories": "~${wins} victories", + "promotionOnNextWin": "Promotion on next win", + "numOfdefeats": "~${losses} defeats", + "demotionOnNextLoss": "Demotion on next loss", "nerdStats": "Nerd Stats", "playersYouTrack": "Players you track", "formula": "Formula", @@ -201,6 +205,12 @@ "currentAxis": "$axis axis:", "p1nkl0bst3rAlert": "That data was retrived from third party API maintained by p1nkl0bst3r", "notForWeb": "Function is not available for web version", + "graphs": { + "attack": "Attack", + "speed": "Speed", + "defense": "Defense", + "cheese": "Cheese" + }, "statCellNum":{ "xpLevel": "XP Level", "xpProgress": "Progress to next level", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 3a46278..eb81f15 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -83,6 +83,10 @@ "verdictWorse": "Хуже", "smooth": "Гладкий", "gamesUntilRanked": "${left} матчей до получения рейтинга", + "numOfVictories": "~${wins} побед", + "promotionOnNextWin": "Повышение после следующей победы", + "numOfdefeats": "~${losses} поражений", + "demotionOnNextLoss": "Понижение после следующего поражения", "nerdStats": "Для задротов", "playersYouTrack": "Отслеживаемые игроки", "formula": "Формула", @@ -201,6 +205,12 @@ "currentAxis": "Ось $axis:", "p1nkl0bst3rAlert": "Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r", "notForWeb": "Функция недоступна для веб версии", + "graphs": { + "attack": "Атака", + "speed": "Скорость", + "defense": "Защита", + "cheese": "Сыр" + }, "statCellNum": { "xpLevel": "Уровень\nопыта", "xpProgress": "Прогресс до следующего уровня",