diff --git a/README.md b/README.md index 975959b..d7506ff 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Track your and other players stats in TETR.IO You can [download an app](https://github.com/dan63047/TetraStats/releases), or [use web version](https://ts.dan63.by). -![Screenshot of the app 1](https://imgur.com/CKGYyBg.png) +![Screenshot of the app 1](https://imgur.com/e8CYvj3.png) # Available functionality - Advanced stats for players @@ -15,6 +15,7 @@ You can [download an app](https://github.com/dan63047/TetraStats/releases), or [ - Comparison to players, rank averages, and player stats from the past - Stats Calculator - Player history in charts +- Tetra League matches history # Special thanks - **kerrmunism** — formulas diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 5e555b8..6df9f83 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -830,7 +830,14 @@ class EndContextMulti { required this.tertiaryTracking, required this.extra, required this.extraTracking, - required this.success}); + required this.success}){ + nerdStats = NerdStats(secondary, tertiary, extra); + nerdStatsTracking = [for (int i = 0; i < secondaryTracking.length; i++) NerdStats(secondaryTracking[i], tertiaryTracking[i], extraTracking[i])]; + estTr = EstTr(secondary, tertiary, extra, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); + estTrTracking = [for (int i = 0; i < secondaryTracking.length; i++) EstTr(secondaryTracking[i], tertiaryTracking[i], extraTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].dss, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe)]; + playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); + playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)]; + } EndContextMulti.fromJson(Map json) { userId = json['id'] ?? json['user']['_id']; diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 7b9f9dc..b28bfe7 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: 1008 (504 per locale) +/// Strings: 1018 (509 per locale) /// -/// Built on 2024-01-22 at 19:27 UTC +/// Built on 2024-02-08 at 20:30 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -165,8 +165,12 @@ class Translations implements BaseTranslations { late final _StringsNewsPartsEn newsParts = _StringsNewsPartsEn._(_root); String get openSearch => 'Search player'; String get closeSearch => 'Close search'; + String get searchHint => 'Nickname, ID or Discord userID (with "ds:" prefix)'; String get refresh => 'Refresh'; String get fetchAndsaveTLHistory => 'Get player history'; + String get fetchAndSaveOldTLmatches => 'Get Tetra League matches history'; + String fetchAndsaveTLHistoryResult({required Object number}) => '${number} states was found'; + String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} matches was found'; String get showStoredData => 'Show stored data'; String get statsCalc => 'Stats Calculator'; String get settings => 'Settings'; @@ -221,8 +225,8 @@ class Translations implements BaseTranslations { String get importCancelled => 'Operation was cancelled'; String get importSuccess => 'Import successful'; String get yourID => 'Your TETR.IO account'; - String get yourIDAlertTitle => 'Your TETR.IO account nickname or ID'; - String get yourIDText => 'Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed.'; + String get yourIDAlertTitle => 'Your nickname in TETR.IO'; + String get yourIDText => 'When app loads, it will retrieve data for this account'; String get language => 'Language'; String get aboutApp => 'About app'; String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; @@ -262,7 +266,7 @@ class Translations implements BaseTranslations { String get winChance => 'Win Chance'; String get byGlicko => 'By Glicko'; String get byEstTR => 'By Est. TR'; - String compareViewNoValues({required Object avgR}) => 'Please, enter username, user ID, APM-PPS-VS values (divider doesn\'t matter, only order matter) or ${avgR} (where R is rank) to both of fields'; + String compareViewNoValues({required Object avgR}) => 'Please, enter username, user ID, APM-PPS-VS values (divider doesn\'t matter, only order matter) or ${avgR} (where R is rank) to both fields'; String compareViewWrongValue({required Object value}) => 'Falied to assign ${value}'; String get mostRecentOne => 'Most recent one'; String get yes => 'Yes'; @@ -697,6 +701,7 @@ class _StringsErrorsEn { String connection({required Object code, required Object message}) => 'Some issue with connection: ${code} ${message}'; String get noSuchUser => 'No such user'; String get history => 'History for that player is missing'; + String get p1nkl0bst3rTLmatches => 'No Tetra League matches was found'; String get clientException => 'No internet connection'; String get forbidden => 'Your IP address is blocked.\nChange IP address or reach out to osk'; String get tooManyRequests => 'You have been rate limited. Try again later'; @@ -753,8 +758,12 @@ class _StringsRu implements Translations { @override late final _StringsNewsPartsRu newsParts = _StringsNewsPartsRu._(_root); @override String get openSearch => 'Искать игрока'; @override String get closeSearch => 'Закрыть поиск'; + @override String get searchHint => 'Ник, ID или ID в Discord (с префиксом "ds:")'; @override String get refresh => 'Обновить'; @override String get fetchAndsaveTLHistory => 'Получить историю игрока'; + @override String get fetchAndSaveOldTLmatches => 'Получить старые матчи Тетра Лиги'; + @override String fetchAndsaveTLHistoryResult({required Object number}) => '${number} состояний было найдено'; + @override String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} старых матчей было найдено'; @override String get showStoredData => 'Показать сохранённые данные'; @override String get statsCalc => 'Калькулятор статистики'; @override String get settings => 'Настройки'; @@ -809,8 +818,8 @@ class _StringsRu implements Translations { @override String get importCancelled => 'Операция была отменена'; @override String get importSuccess => 'Успешно импортировано'; @override String get yourID => 'Ваш аккаунт в TETR.IO'; - @override String get yourIDAlertTitle => 'Никнейм или ID вашего аккаунта в TETR.IO'; - @override String get yourIDText => 'Каждый раз, когда приложение запускается, приложение будет получать статистику этого игрока. Пожалуйста, отдайте предпочтение ID, так как никнейм можно изменить.'; + @override String get yourIDAlertTitle => 'Ваш ник в TETR.IO'; + @override String get yourIDText => 'При запуске приложения оно будет получать статистику этого игрока.'; @override String get language => 'Язык (Language)'; @override String get aboutApp => 'О приложении'; @override String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; @@ -1285,6 +1294,7 @@ class _StringsErrorsRu implements _StringsErrorsEn { @override String connection({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; @override String get noSuchUser => 'Нет такого пользователя'; @override String get history => 'История данного игрока отсутствует'; + @override String get p1nkl0bst3rTLmatches => 'Старых матчей Тетра Лиги не было найдено'; @override String get clientException => 'Нет соединения с интернетом'; @override String get forbidden => 'Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом'; @override String get tooManyRequests => 'Слишком много запросов. Попробуйте позже'; @@ -1333,8 +1343,12 @@ extension on Translations { case 'newsParts.unknownNews': return ({required Object type}) => 'Unknown news of type ${type}'; case 'openSearch': return 'Search player'; case 'closeSearch': return 'Close search'; + case 'searchHint': return 'Nickname, ID or Discord userID (with "ds:" prefix)'; case 'refresh': return 'Refresh'; case 'fetchAndsaveTLHistory': return 'Get player history'; + case 'fetchAndSaveOldTLmatches': return 'Get Tetra League matches history'; + case 'fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} states was found'; + case 'fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} matches was found'; case 'showStoredData': return 'Show stored data'; case 'statsCalc': return 'Stats Calculator'; case 'settings': return 'Settings'; @@ -1389,8 +1403,8 @@ extension on Translations { case 'importCancelled': return 'Operation was cancelled'; case 'importSuccess': return 'Import successful'; case 'yourID': return 'Your TETR.IO account'; - case 'yourIDAlertTitle': return 'Your TETR.IO account nickname or ID'; - case 'yourIDText': return 'Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed.'; + case 'yourIDAlertTitle': return 'Your nickname in TETR.IO'; + case 'yourIDText': return 'When app loads, it will retrieve data for this account'; case 'language': return 'Language'; case 'aboutApp': return 'About app'; case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; @@ -1430,7 +1444,7 @@ extension on Translations { case 'winChance': return 'Win Chance'; case 'byGlicko': return 'By Glicko'; case 'byEstTR': return 'By Est. TR'; - case 'compareViewNoValues': return ({required Object avgR}) => 'Please, enter username, user ID, APM-PPS-VS values (divider doesn\'t matter, only order matter) or ${avgR} (where R is rank) to both of fields'; + case 'compareViewNoValues': return ({required Object avgR}) => 'Please, enter username, user ID, APM-PPS-VS values (divider doesn\'t matter, only order matter) or ${avgR} (where R is rank) to both fields'; case 'compareViewWrongValue': return ({required Object value}) => 'Falied to assign ${value}'; case 'mostRecentOne': return 'Most recent one'; case 'yes': return 'Yes'; @@ -1540,6 +1554,7 @@ extension on Translations { case 'errors.connection': return ({required Object code, required Object message}) => 'Some issue with connection: ${code} ${message}'; case 'errors.noSuchUser': return 'No such user'; case 'errors.history': return 'History for that player is missing'; + case 'errors.p1nkl0bst3rTLmatches': return 'No Tetra League matches was found'; case 'errors.clientException': return 'No internet connection'; case 'errors.forbidden': return 'Your IP address is blocked.\nChange IP address or reach out to osk'; case 'errors.tooManyRequests': return 'You have been rate limited. Try again later'; @@ -1847,8 +1862,12 @@ extension on _StringsRu { case 'newsParts.unknownNews': return ({required Object type}) => 'Неизвестная новость типа ${type}'; case 'openSearch': return 'Искать игрока'; case 'closeSearch': return 'Закрыть поиск'; + case 'searchHint': return 'Ник, ID или ID в Discord (с префиксом "ds:")'; case 'refresh': return 'Обновить'; case 'fetchAndsaveTLHistory': return 'Получить историю игрока'; + case 'fetchAndSaveOldTLmatches': return 'Получить старые матчи Тетра Лиги'; + case 'fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} состояний было найдено'; + case 'fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} старых матчей было найдено'; case 'showStoredData': return 'Показать сохранённые данные'; case 'statsCalc': return 'Калькулятор статистики'; case 'settings': return 'Настройки'; @@ -1903,8 +1922,8 @@ extension on _StringsRu { case 'importCancelled': return 'Операция была отменена'; case 'importSuccess': return 'Успешно импортировано'; case 'yourID': return 'Ваш аккаунт в TETR.IO'; - case 'yourIDAlertTitle': return 'Никнейм или ID вашего аккаунта в TETR.IO'; - case 'yourIDText': return 'Каждый раз, когда приложение запускается, приложение будет получать статистику этого игрока. Пожалуйста, отдайте предпочтение ID, так как никнейм можно изменить.'; + case 'yourIDAlertTitle': return 'Ваш ник в TETR.IO'; + case 'yourIDText': return 'При запуске приложения оно будет получать статистику этого игрока.'; case 'language': return 'Язык (Language)'; case 'aboutApp': return 'О приложении'; case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; @@ -2054,6 +2073,7 @@ extension on _StringsRu { case 'errors.connection': return ({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; case 'errors.noSuchUser': return 'Нет такого пользователя'; case 'errors.history': return 'История данного игрока отсутствует'; + case 'errors.p1nkl0bst3rTLmatches': return 'Старых матчей Тетра Лиги не было найдено'; case 'errors.clientException': return 'Нет соединения с интернетом'; case 'errors.forbidden': return 'Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом'; case 'errors.tooManyRequests': return 'Слишком много запросов. Попробуйте позже'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 5d42d00..b5bf668 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -27,7 +27,7 @@ const String endContext2 = "endContext2"; const String statesCol = "jsonStates"; const String player1id = "player1id"; const String player2id = "player2id"; -/// Table, that store players data, their stats and some moments of time +/// Table, that store players data, their stats at some moments of time const String createTetrioUsersTable = ''' CREATE TABLE IF NOT EXISTS "tetrioUsers" ( "id" TEXT UNIQUE, @@ -66,7 +66,7 @@ const String createTetrioTLReplayStats = ''' '''; class TetrioService extends DB { - Map> _players = {}; + final Map _players = {}; // 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} @@ -82,9 +82,9 @@ class TetrioService extends DB { /// We should have only one instanse of this service static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; - late final StreamController>> _tetrioStreamController; + late final StreamController> _tetrioStreamController; TetrioService._sharedInstance() { - _tetrioStreamController = StreamController>>.broadcast(onListen: () { + _tetrioStreamController = StreamController>.broadcast(onListen: () { _tetrioStreamController.sink.add(_players); }); } @@ -95,17 +95,15 @@ class TetrioService extends DB { await _loadPlayers(); } - Stream>> get allPlayers => _tetrioStreamController.stream; + Stream> get allPlayers => _tetrioStreamController.stream; /// Loading and sending to the stream everyone. Future _loadPlayers() async { - final allPlayers = await getAllPlayers(); - try{ - _players = allPlayers.toList().first; // ??? - }catch (e){ - developer.log("_loadPlayers: allPlayers.toList().first did oopsie", name: "services/tetrio_crud", error: e); - _players = {}; + final allPlayers = await getAllPlayerToTrack(); + for (var element in allPlayers) { + _players[element] = await getNicknameByID(element); } + developer.log("_loadPlayers: $_players", name: "services/tetrio_crud"); _tetrioStreamController.add(_players); } @@ -128,7 +126,10 @@ class TetrioService extends DB { Future getNicknameByID(String id) async { if (id.length <= 16) return id; // nicknames can be up to 16 symbols in length, that's how i'm differentiate nickname from ids try{ - return await getPlayer(id).then((value) => value.last.username); + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + var request = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + return request.first[nickCol] as String; } catch (e){ return await fetchPlayer(id).then((value) => value.username); } @@ -350,16 +351,8 @@ class TetrioService extends DB { // trying to dump it to local DB await ensureDbIsOpen(); final db = getDatabaseOrThrow(); - late List states; - try{ - // checking if tetra stats aware about that player TODO: is it necessary? - states = _players[id]!; - }catch(e){ - // if somehow not - create it - var player = await fetchPlayer(id); - await createPlayer(player); - states = _players[id]!; - } + List states = await getPlayer(id); + if (states.isEmpty) await createPlayer(history.first); states.insertAll(0, history.reversed); final Map statesJson = {}; for (var e in states) { // making one big json out of this list @@ -367,7 +360,6 @@ class TetrioService extends DB { } // and putting it to local DB await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]); - _tetrioStreamController.add(_players); return history; case 404: developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); @@ -393,6 +385,100 @@ class TetrioService extends DB { } } + /// Docs later + Future> fetchAndSaveOldTLmatches(String userID) async { + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID}); + } else { + url = Uri.https('api.p1nkl0bst3r.xyz', 'tlmatches/$userID'); + } + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + // that one api returns csv instead of json + List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); + List matches = []; + + // parsing data into TetraLeagueAlphaRecord objects + for (var entry in csv){ + TetraLeagueAlphaRecord match = TetraLeagueAlphaRecord( + replayId: entry[0], + ownId: entry[0], // i gonna disting p1nkl0bst3r entries with it + timestamp: DateTime.parse(entry[1]), + endContext: [ + EndContextMulti( + userId: entry[2], + username: entry[3].toString(), + naturalOrder: 0, + inputs: -1, + piecesPlaced: -1, + handling: Handling(arr: -1, das: -1, sdf: -1, dcd: 0, cancel: true, safeLock: true), + points: entry[4], + wins: entry[4], + secondary: entry[6], + secondaryTracking: [], + tertiary: entry[5], + tertiaryTracking: [], + extra: entry[7], + extraTracking: [], + success: true + ), + EndContextMulti( + userId: entry[8], + username: entry[9].toString(), + naturalOrder: 1, + inputs: -1, + piecesPlaced: -1, + handling: Handling(arr: -1, das: -1, sdf: -1, dcd: 0, cancel: true, safeLock: true), + points: entry[10], + wins: entry[10], + secondary: entry[12], + secondaryTracking: [], + tertiary: entry[11], + tertiaryTracking: [], + extra: entry[13], + extraTracking: [], + success: false + ) + ], + replayAvalable: false + ); + matches.add(match); + } + + // trying to dump it to local DB + TetraLeagueAlphaStream fakeStream = TetraLeagueAlphaStream(userId: userID, records: matches); + saveTLMatchesFromStream(fakeStream); + + return matches; + case 404: + developer.log("fetchAndSaveOldTLmatches: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); + throw TetrioHistoryNotExist(); + case 403: + throw P1nkl0bst3rForbidden(); + case 429: + throw P1nkl0bst3rTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw P1nkl0bst3rInternalProblem(); + default: + developer.log("fetchAndSaveOldTLmatches: Failed to fetch history", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); + } + } + /// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve. Future fetchTLLeaderboard() async { try{ @@ -638,7 +724,7 @@ class TetrioService extends DB { await db.delete(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [rID]); } - /// Retrieves Blitz, 40 Lines and Zen records for a given [playerID] from Tetra Channel api. Returns Map, which contains user id (`user`), + /// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns Map, which contains user id (`user`), /// Blitz (`blitz`) and 40 Lines (`sprint`) record objects and Zen object (`zen`). Throws an exception if fails to retrieve. Future> fetchRecords(String userID) async { try{ @@ -717,9 +803,7 @@ class TetrioService extends DB { // converting to json and store final Map statesJson = {(tetrioPlayer.state.millisecondsSinceEpoch ~/ 1000).toString(): tetrioPlayer.toJson()}; db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}); - _players.addEntries({ - tetrioPlayer.userId: [tetrioPlayer] - }.entries); + _players.addEntries({tetrioPlayer.userId: tetrioPlayer.username}.entries); _tetrioStreamController.add(_players); } @@ -747,7 +831,6 @@ class TetrioService extends DB { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersToTrackTable); - developer.log("getAllPlayerToTrack: $players", name: "services/tetrio_crud"); return players.map((noteRow) => noteRow["id"].toString()); } @@ -759,25 +842,22 @@ class TetrioService extends DB { if (deletedPlayer != 1) { throw CouldNotDeletePlayer(); } else { - // _players.removeWhere((key, value) => key == id); - // _tetrioStreamController.add(_players); + _players.removeWhere((key, value) => key == id); + _tetrioStreamController.add(_players); } } /// Saves state (which is [tetrioPlayer]) to the local database. Future storeState(TetrioPlayer tetrioPlayer) async { - await ensureDbIsOpen(); - final db = getDatabaseOrThrow(); - late List states; - try { // retrieveing previous states - states = _players[tetrioPlayer.userId]!; - } catch (e) { // nothing found - player not exist - create them - await createPlayer(tetrioPlayer); - states = await getPlayer(tetrioPlayer.userId); + // if tetrio player doesn't have entry in database - just calling different function + List states = await getPlayer(tetrioPlayer.userId); + if (states.isEmpty) { + await createPlayer(tetrioPlayer); + return; } // we not going to add state, that is same, as the previous - bool test = _players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer); + bool test = states.last.isSameState(tetrioPlayer); if (test == false) states.add(tetrioPlayer); // Making map of the states @@ -786,21 +866,21 @@ class TetrioService extends DB { // Saving in format: {"unix_seconds": json_of_state} statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); } + // Rewrite our database + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); - _players[tetrioPlayer.userId]!.add(tetrioPlayer); - _tetrioStreamController.add(_players); } /// Remove state (which is [tetrioPlayer]) from the local database Future deleteState(TetrioPlayer tetrioPlayer) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); - late List states; + List states = await getPlayer(tetrioPlayer.userId); // removing state from map that contain every state of each user - _players[tetrioPlayer.userId]!.removeWhere((element) => element.state == tetrioPlayer.state); - states = _players[tetrioPlayer.userId]!; + states.removeWhere((element) => element.state == tetrioPlayer.state); // Making map of the states (without deleted one) final Map statesJson = {}; @@ -810,7 +890,6 @@ class TetrioService extends DB { // Rewriting database entry with new json await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); - _players[tetrioPlayer.userId]!.add(tetrioPlayer); _tetrioStreamController.add(_players); } @@ -830,7 +909,7 @@ class TetrioService extends DB { rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String))); // updating the stream _players.removeWhere((key, value) => key == id); - _players.addEntries({states.last.userId: states}.entries); + _players.addEntries({states.last.userId: states.last.username}.entries); _tetrioStreamController.add(_players); return states; } @@ -940,20 +1019,18 @@ class TetrioService extends DB { } } - /// Basucally, retrieves whole [tetrioUsersTable] and do stupud things idk - /// Returns god knows what. TODO: Rewrite this shit - Future>>> getAllPlayers() async { + /// Retrieves whole [tetrioUsersTable] and returns Map with [TetrioPlayer] objects of everyone in database + Future>> getAllPlayers() async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersTable); Map> data = {}; - return players.map((row) { - // what the fuck am i doing here? - var test = json.decode(row['jsonStates'] as String); + for (var entry in players){ + var test = json.decode(entry['jsonStates'] as String); List states = []; - test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), row[idCol] as String, row[nickCol] as String))); + test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), entry[idCol] as String, entry[nickCol] as String))); data.addEntries({states.last.userId: states}.entries); - return data; - }); + } + return data; } } diff --git a/lib/utils/text_shadow.dart b/lib/utils/text_shadow.dart new file mode 100644 index 0000000..8022077 --- /dev/null +++ b/lib/utils/text_shadow.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +const List textShadow = [ // man i love this shadow + Shadow(offset: Offset(0.0, 0.0), blurRadius: 3.0, color: Colors.black), + Shadow(offset: Offset(0.0, 0.0), blurRadius: 8.0, color: Colors.black), +]; \ No newline at end of file diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index b22661c..2dd567f 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -1,5 +1,3 @@ -// ignore_for_file: use_build_context_synchronously - import 'dart:io'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -70,8 +68,7 @@ class CompareState extends State { theRedSide = [null, null, average]; return setState(() {}); }on Exception { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); return; } } @@ -126,8 +123,7 @@ class CompareState extends State { } theRedSide = [player, dStates, player.tlSeason1]; } on Exception { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); } _justUpdate(); } @@ -146,8 +142,7 @@ class CompareState extends State { theGreenSide = [null, null, average]; return setState(() {}); }on Exception { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text("Falied to assign $user"))); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); return; } } @@ -202,8 +197,7 @@ class CompareState extends State { } theGreenSide = [player, dStates, player.tlSeason1]; } on Exception { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text("Falied to assign $user"))); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); } _justUpdate(); } @@ -213,19 +207,16 @@ class CompareState extends State { theGreenSide[2] = user.tlSeason1;}); } - double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko, - double notyourRD) { + double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { return ((1 / - (1 + - pow( - 10, - (notyourGlicko - yourGlicko) / - (400 * - sqrt(1 + - (3 * - pow(0.0057564273, 2) * - (pow(yourRD, 2) + pow(notyourRD, 2)) / - pow(pi, 2)))))))); + (1 + pow(10, + (notyourGlicko - yourGlicko) / + (400 * sqrt(1 + (3 * pow(0.0057564273, 2) * + (pow(yourRD, 2) + pow(notyourRD, 2)) / pow(pi, 2) + ))) + ) + ) + )); } void _justUpdate() { diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index f82a24e..c56a593 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -16,9 +16,11 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; import 'package:tetra_stats/main.dart' show prefs; import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/views/ranks_averages_view.dart' show RankAveragesView; import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; +import 'package:tetra_stats/widgets/search_box.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; @@ -40,10 +42,6 @@ final NumberFormat secs = NumberFormat("00.###"); final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final DateFormat _dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); -final List textShadow = [ // man i love this shadow - const Shadow(offset: Offset(0.0, 0.0), blurRadius: 3.0, color: Colors.black), - const Shadow(offset: Offset(0.0, 0.0), blurRadius: 8.0, color: Colors.black), -]; class MainView extends StatefulWidget { @@ -74,19 +72,6 @@ class _MainState extends State with TickerProviderStateMixin { late TabController _tabController; late bool fixedScroll; - Widget _searchTextField() { - return TextField( - maxLength: 25, - autocorrect: false, - enableSuggestions: false, - decoration: const InputDecoration(counter: Offstage()), - style: TextStyle(shadows: textShadow), - onSubmitted: (String value) { - changePlayer(value); - }, - ); - } - @override void initState() { initDB(); @@ -116,10 +101,10 @@ class _MainState extends State with TickerProviderStateMixin { /// That function initiate search of data about [player]. If [fetchHistory] is true, /// also attempting to retrieve players history. Can trow an Exception if fails - void changePlayer(String player, {bool fetchHistory = false}) { + void changePlayer(String player, {bool fetchHistory = false, bool fetchTLmatches = false}) { setState(() { _searchFor = player; - me = fetch(_searchFor, fetchHistory: fetchHistory); + me = fetch(_searchFor, fetchHistory: fetchHistory, fetchTLmatches: fetchTLmatches); }); } @@ -128,13 +113,14 @@ class _MainState extends State with TickerProviderStateMixin { } /// Retrieves data from 3 different Tetra Channel API endpoints + 1 endpoint from p1nkl0bst3r's API - /// using [nickOrID] of player. If [fetchHistory] is true, also retrieves players history from p1nkl0bst3r's API. + /// using [nickOrID] of player. /// - /// Returns list which contains players object, his TL records, previous states, TL matches, previos TL state, if player tracked (bool), news entries and topTR. + /// If [fetchHistory] is true, also retrieves players history from p1nkl0bst3r's API. If [fetchTLmatches] is true, also retrieves players old Tetra League + /// matches from p1nkl0bst3r's API. Returns list which contains [TetrioPlayer], his records, previous states, TL matches, previous TL state, + /// if player tracked (bool), news entries and topTR. /// - /// If at least one request to some endpoint fails, whole function will throw an exception. - /// TODO: Change this behavior - Future fetch(String nickOrID, {bool fetchHistory = false}) async { + /// If at least one request to Tetra Channel API fails, whole function will throw an exception. + Future fetch(String nickOrID, {bool fetchHistory = false, bool fetchTLmatches = false}) async { TetrioPlayer me; // If user trying to search with discord id @@ -173,13 +159,28 @@ class _MainState extends State with TickerProviderStateMixin { TetraLeagueAlpha? compareWith; Set uniqueTL = {}; tlMatches = tlStream.records; - var storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches + List storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches if (isTracking){ // if tracked - save data to local DB await teto.storeState(me); await teto.saveTLMatchesFromStream(tlStream); } // building list of TL matches + if(fetchTLmatches) { + try{ + List oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor); + storedRecords.addAll(oldMatches); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.length)))); + }on TetrioHistoryNotExist{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches))); + }on P1nkl0bst3rForbidden { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); + }on P1nkl0bst3rInternalProblem { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); + }on P1nkl0bst3rTooManyRequests{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); + } + } for (var match in storedRecords) { // add stored match to list only if it missing from retrived ones if (!tlMatches.contains(match)) tlMatches.add(match); @@ -192,7 +193,21 @@ class _MainState extends State with TickerProviderStateMixin { }); // Handling history - if(fetchHistory) await teto.fetchAndsaveTLHistory(_searchFor); // Retrieve if needed + if(fetchHistory){ + try{ + var history = await teto.fetchAndsaveTLHistory(_searchFor); // Retrieve if needed + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); + }on TetrioHistoryNotExist{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noHistorySaved))); + }on P1nkl0bst3rForbidden { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); + }on P1nkl0bst3rInternalProblem { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); + }on P1nkl0bst3rTooManyRequests{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); + } + } + states.addAll(await teto.getPlayer(me.userId)); for (var element in states) { // For graphs I need only unique entries if (uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1); @@ -235,11 +250,12 @@ class _MainState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { final t = Translations.of(context); + bool bigScreen = MediaQuery.of(context).size.width > 768; return Scaffold( drawer: widget.player == null ? NavDrawer(changePlayer) : null, // Side menu hidden if player provided drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.2, // 20% of left side of the screen used of Drawer gesture appBar: AppBar( - title: _showSearchBar ? _searchTextField() : Text(widget.title, style: TextStyle(shadows: textShadow)), + title: _showSearchBar ? SearchBox(onSubmit: changePlayer, bigScreen: bigScreen) : Text(widget.title, style: const TextStyle(shadows: textShadow)), backgroundColor: Colors.black, actions: widget.player == null ? [ // search bar and PopupMenuButton hidden if player provided TODO: Subject to change _showSearchBar @@ -271,6 +287,10 @@ class _MainState extends State with TickerProviderStateMixin { value: "history", child: Text(t.fetchAndsaveTLHistory), ), + PopupMenuItem( + value: "TLmatches", + child: Text(t.fetchAndSaveOldTLmatches), + ), PopupMenuItem( value: "/states", child: Text(t.showStoredData), @@ -292,6 +312,9 @@ class _MainState extends State with TickerProviderStateMixin { case "history": changePlayer(_searchFor, fetchHistory: true); break; + case "TLmatches": + changePlayer(_searchFor, fetchTLmatches: true); + break; default: context.go(value); } @@ -309,7 +332,6 @@ class _MainState extends State with TickerProviderStateMixin { case ConnectionState.active: return const Center(child: CircularProgressIndicator(color: Colors.white)); case ConnectionState.done: - //bool bigScreen = MediaQuery.of(context).size.width > 1024; if (snapshot.hasData) { return RefreshIndicator( onRefresh: () { @@ -373,15 +395,6 @@ class _MainState extends State with TickerProviderStateMixin { var err = snapshot.error as ConnectionIssue; errText = t.errors.connection(code: err.code, message: err.message); break; - case P1nkl0bst3rForbidden: - errText = t.errors.p1nkl0bst3rForbidden; - break; - case P1nkl0bst3rTooManyRequests: - errText = t.errors.p1nkl0bst3rTooManyRequests; - break; - case P1nkl0bst3rInternalProblem: - errText = kIsWeb ? t.errors.p1nkl0bst3rinternalWebVersion : t.errors.p1nkl0bst3rinternal; - break; case TetrioHistoryNotExist: errText = t.errors.history; break; @@ -466,8 +479,9 @@ class _NavDrawerState extends State { case ConnectionState.waiting: case ConnectionState.active: final allPlayers = (snapshot.data != null) - ? snapshot.data as Map> - : >{}; + ? snapshot.data as Map + : {}; + allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted List keys = allPlayers.keys.toList(); return NestedScrollView( headerSliverBuilder: (context, value) { @@ -522,7 +536,7 @@ class _NavDrawerState extends State { itemBuilder: (context, index) { var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. return ListTile( - title: Text(allPlayers[keys[i]]?.last.username as String), // Takes last known username from list of states + title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states onTap: () { widget.changePlayer(keys[i]); // changes to chosen player Navigator.of(context).pop(); // and closes itself. @@ -548,37 +562,42 @@ class _TLRecords extends StatelessWidget { @override Widget build(BuildContext context) { - bool bigScreen = MediaQuery.of(context).size.width > 768; - return ListView( // TODO: Redo using ListView.builder() - physics: const AlwaysScrollableScrollPhysics(), - children: (data.isNotEmpty) - ? [for (var value in data) ListTile( - leading: Text("${value.endContext.firstWhere((element) => element.userId == userID).points} : ${value.endContext.firstWhere((element) => element.userId != userID).points}", - style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : - const TextStyle(fontSize: 28)), - title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username}"), - subtitle: Text(_dateFormat.format(value.timestamp)), - trailing: Table(defaultColumnWidth: const IntrinsicColumnWidth(), - defaultVerticalAlignment: TableCellVerticalAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - columnWidths: const { - 0: FixedColumnWidth(50), - 2: FixedColumnWidth(50), - }, - children: [ - TableRow(children: [Text(_f2.format(value.endContext.firstWhere((element) => element.userId == userID).secondary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(value.endContext.firstWhere((element) => element.userId != userID).secondary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" APM", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - TableRow(children: [Text(_f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" PPS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - TableRow(children: [Text(_f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" VS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), - ],), - onTap: (){Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TlMatchResultView(record: value, initPlayerId: userID), - ), - );}, - )] - : [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)))], + if (data.isEmpty) return Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); + bool bigScreen = MediaQuery.of(context).size.width > 768; + return ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: data.length, + itemBuilder: (BuildContext context, int index) { + var accentColor = data[index].endContext.firstWhere((element) => element.userId == userID).success ? Colors.green : Colors.red; + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + stops: const [0, 0.05], + colors: [accentColor, Colors.transparent] + ) + ), + child: ListTile( + // tileColor: data[index].endContext.firstWhere((element) => element.userId == userID).success ? Colors.green[900] : Colors.red[900], + leading: Text("${data[index].endContext.firstWhere((element) => element.userId == userID).points} : ${data[index].endContext.firstWhere((element) => element.userId != userID).points}", + style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), + title: Text("vs. ${data[index].endContext.firstWhere((element) => element.userId != userID).username}"), + subtitle: Text(_dateFormat.format(data[index].timestamp)), + trailing: Table(defaultColumnWidth: const IntrinsicColumnWidth(), + defaultVerticalAlignment: TableCellVerticalAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + columnWidths: const { + 0: FixedColumnWidth(50), + 2: FixedColumnWidth(50), + }, + children: [ + TableRow(children: [Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId == userID).secondary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId != userID).secondary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" APM", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), + TableRow(children: [Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId == userID).tertiary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId != userID).tertiary), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" PPS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), + TableRow(children: [Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId == userID).extra), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" :", style: TextStyle(height: 1.1)), Text(_f2.format(data[index].endContext.firstWhere((element) => element.userId != userID).extra), textAlign: TextAlign.right, style: const TextStyle(height: 1.1)), const Text(" VS", textAlign: TextAlign.right, style: TextStyle(height: 1.1))]), + ],), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), + ), ); + }); } } @@ -620,7 +639,7 @@ class _HistoryChartThigy extends StatefulWidget{ final NumberFormat yFormat; /// Implements graph for the _History widget. Requires [data] which is a list of dots for the graph. [yAxisTitle] used to keep track of changes. - /// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets numer format + /// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets number format /// for left titles const _HistoryChartThigy({required this.data, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat}); @@ -896,6 +915,7 @@ class _RecordThingy extends StatelessWidget { @override Widget build(BuildContext context) { + if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; return ListView.builder( @@ -903,7 +923,7 @@ class _RecordThingy extends StatelessWidget { itemCount: 1, itemBuilder: (BuildContext context, int index) { return Column( - children: (record != null) ? [ + children: [ // show mode title if (record!.stream.contains("40l")) Text(t.sprint, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) else if (record!.stream.contains("blitz")) Text(t.blitz, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), @@ -1055,9 +1075,6 @@ class _RecordThingy extends StatelessWidget { ), ), ] - : [ // If no record, show this - Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)) - ], ); }); }); diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 1fd6f0b..96ffe5c 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -7,7 +7,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/views/main_view.dart' show MainView, textShadow; +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'; var _chartsShortTitlesDropdowns = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)]; diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index a0a0807..6edb8b9 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/main.dart' show packageInfo; import 'package:file_selector/file_selector.dart'; import 'package:file_picker/file_picker.dart'; @@ -66,6 +67,11 @@ class SettingsState extends State { await _setDefaultNickname(player); } + Future _removePlayer() async { + await prefs.remove('player'); + await _setDefaultNickname("dan63047"); + } + @override Widget build(BuildContext context) { final t = Translations.of(context); @@ -212,9 +218,21 @@ class SettingsState extends State { ), TextButton( child: Text(t.popupActions.submit), - onPressed: () { - _setPlayer(_playertext.text.toLowerCase().trim()); - Navigator.of(context).pop(); + onPressed: () async { + if (_playertext.text.isEmpty) { + _removePlayer(); + Navigator.of(context).pop(); + return; + } + late TetrioPlayer user; + try{ + user = await teto.fetchPlayer(_playertext.text.toLowerCase().trim()); + }on Exception{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.noSuchUser))); + return; + } + _setPlayer(user.userId); + if (context.mounted) Navigator.of(context).pop(); setState(() {}); }, ) diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index b353179..219b725 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -195,6 +195,9 @@ class TlMatchResultState extends State { ), ), ), + if (widget.record.ownId == widget.record.replayId) SliverToBoxAdapter( + child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), + ), SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) { switch(snapshot.connectionState){ case ConnectionState.none: @@ -470,8 +473,8 @@ class TlMatchResultState extends State { ) ], ), - const Divider(), - Column( + if (widget.record.ownId != widget.record.replayId) const Divider(), + if (widget.record.ownId != widget.record.replayId) Column( children: [ Padding( padding: const EdgeInsets.only(bottom: 16), diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart index 1916dbb..2b11976 100644 --- a/lib/views/tracked_players_view.dart +++ b/lib/views/tracked_players_view.dart @@ -70,14 +70,15 @@ class TrackedPlayersState extends State { ), backgroundColor: Colors.black, body: SafeArea( - child: StreamBuilder( - stream: teto.allPlayers, + child: FutureBuilder( + future: teto.getAllPlayers(), builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: - return const Center(child: Text('none case of StreamBuilder')); case ConnectionState.waiting: case ConnectionState.active: + return const Center(child: CircularProgressIndicator(color: Colors.white)); + case ConnectionState.done: final allPlayers = (snapshot.data != null) ? snapshot.data as Map> : >{}; List keys = allPlayers.keys.toList(); return NestedScrollView( @@ -114,7 +115,7 @@ class TrackedPlayersState extends State { icon: const Icon(Icons.delete_forever), onPressed: () { String nn = allPlayers[keys[index]]!.last.username; - teto.deletePlayer(keys[index]); + setState(() {teto.deletePlayer(keys[index]);}); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: nn)))); }, ), @@ -128,10 +129,6 @@ class TrackedPlayersState extends State { }, ); })); - case ConnectionState.done: - return const Center( - child: Text('done case of StreamBuilder', - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center)); } })), ); diff --git a/lib/widgets/search_box.dart b/lib/widgets/search_box.dart new file mode 100644 index 0000000..cf4b81c --- /dev/null +++ b/lib/widgets/search_box.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; + +const int length = 25; +final TextEditingController controller = TextEditingController(); + +class SearchBox extends StatefulWidget { + final Function onSubmit; + final bool bigScreen; + const SearchBox({required this.onSubmit, required this.bigScreen, super.key}); + + @override + State createState() => _SearchBoxState(); +} + +class _SearchBoxState extends State{ + late FocusNode textbotFocus; + + @override + void initState() { + textbotFocus = FocusNode(); + controller.addListener(() { + setState(() {}); + }); + super.initState(); + } + + @override + void dispose(){ + controller.clear(); + textbotFocus.dispose(); + super.dispose(); + } + + Color getColorOfCounter(){ + // if limit was hit + if ((length - controller.text.length) <= 0) return Colors.redAccent; + // if input more than 16 symbols (username length limit) + if ((length - controller.text.length) < 9) return Colors.yellowAccent; + // if we good (we not) + return Colors.grey; + } + + double getFontSizeOfCounter(){ + return (length - controller.text.length) <= 0 ? 24 : 16; + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + return Row( + mainAxisSize: MainAxisSize.min, + //alignment: Alignment.centerRight, + children: [ + Expanded( + child: TextField( + controller: controller, + maxLength: length, + focusNode: textbotFocus, + autofocus: true, + autocorrect: false, + enableSuggestions: false, + decoration: InputDecoration( + counter: const Offstage(), + hintText: widget.bigScreen ? t.searchHint : null, + ), + style: const TextStyle(shadows: textShadow, fontSize: 18), + onSubmitted: (String value) { + widget.onSubmit(value); + textbotFocus.unfocus(); + }, + ), + ), + AnimatedDefaultTextStyle( + style: TextStyle( + fontFamily: "Eurostile Round", + fontSize: getFontSizeOfCounter(), + color: getColorOfCounter(), + shadows: textShadow + ), + duration: Durations.short4, + curve: Curves.easeOutCirc, + child: Text("${length - controller.text.length}") + ) + ] + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index d5b7573..2207abe 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -47,6 +47,7 @@ class _TLThingyState extends State { @override Widget build(BuildContext context) { final t = Translations.of(context); + if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,)); return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; return ListView.builder( @@ -54,364 +55,360 @@ class _TLThingyState extends State { itemCount: 1, itemBuilder: (BuildContext context, int index) { return Column( - children: (currentTl.gamesPlayed > 0) - ? [ - if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (oldTl != null) Text(t.comparingWith(newDate: dateFormat.format(currentTl.timestamp), oldDate: dateFormat.format(oldTl!.timestamp)), - textAlign: TextAlign.center,), - if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), - labels: RangeLabels( - _currentRangeValues.start.round().toString(), - _currentRangeValues.end.round().toString(), - ), - onChanged: (RangeValues values) { - setState(() { - _currentRangeValues = values; - if (values.start.round() == 0){ - currentTl = widget.tl; - }else{ - currentTl = sortedStates[values.start.round()-1].tlSeason1; - } - if (values.end.round() == 0){ - oldTl = widget.tl; - }else{ - oldTl = sortedStates[values.end.round()-1].tlSeason1; - } - }); - }, - ), - if (currentTl.gamesPlayed >= 10) - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceAround, - crossAxisAlignment: WrapCrossAlignment.center, - clipBehavior: Clip.hardEdge, - children: [ - widget.userID == "5e32fc85ab319c2ab1beb07c" // he love her so much, you can't even imagine - ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? - : Image.asset("res/tetrio_tl_alpha_ranks/${currentTl.rank}.png", height: 128), - Column( - children: [ - Text("${f2.format(currentTl.rating)} TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (oldTl != null) Text( - "${fDiff.format(currentTl.rating - oldTl!.rating)} TR", - textAlign: TextAlign.center, - style: TextStyle( - color: currentTl.rating - oldTl!.rating < 0 ? - Colors.red : - Colors.green - ), - ), - Column( - children: [ - RichText( - textAlign: TextAlign.center, - softWrap: true, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan(text: "${t.top} ${f2.format(currentTl.percentile * 100)}% (${currentTl.percentileRank.toUpperCase()})"), - if (currentTl.bestRank != "z") const TextSpan(text: " • "), - if (currentTl.bestRank != "z") TextSpan(text: "${t.topRank}: ${currentTl.bestRank.toUpperCase()}"), - if (widget.topTR != null) TextSpan(text: " (${f2.format(widget.topTR)} TR)"), - TextSpan(text: " • Glicko: ${f2.format(currentTl.glicko!)}±"), - TextSpan(text: f2.format(currentTl.rd!), style: currentTl.decaying ? TextStyle(color: currentTl.rd! > 98 ? Colors.red : Colors.yellow) : null), - if (currentTl.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: currentTl.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic) - ], - ), - ), - ], - ), - ], - ), - ], - ), - 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,)], - 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 - ), - ), - if (currentTl.gamesPlayed < 10) - Text(t.gamesUntilRanked(left: 10 - currentTl.gamesPlayed), - softWrap: true, + children: [ + if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (oldTl != null) Text(t.comparingWith(newDate: dateFormat.format(currentTl.timestamp), oldDate: dateFormat.format(oldTl!.timestamp)), + textAlign: TextAlign.center,), + if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), + labels: RangeLabels( + _currentRangeValues.start.round().toString(), + _currentRangeValues.end.round().toString(), + ), + onChanged: (RangeValues values) { + setState(() { + _currentRangeValues = values; + if (values.start.round() == 0){ + currentTl = widget.tl; + }else{ + currentTl = sortedStates[values.start.round()-1].tlSeason1; + } + if (values.end.round() == 0){ + oldTl = widget.tl; + }else{ + oldTl = sortedStates[values.end.round()-1].tlSeason1; + } + }); + }, + ), + if (currentTl.gamesPlayed >= 10) + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + clipBehavior: Clip.hardEdge, + children: [ + widget.userID == "5e32fc85ab319c2ab1beb07c" // he love her so much, you can't even imagine + ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? + : Image.asset("res/tetrio_tl_alpha_ranks/${currentTl.rank}.png", height: 128), + Column( + children: [ + Text("${f2.format(currentTl.rating)} TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (oldTl != null) Text( + "${fDiff.format(currentTl.rating - oldTl!.rating)} TR", textAlign: TextAlign.center, style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28, - overflow: TextOverflow.visible, - )), + color: currentTl.rating - oldTl!.rating < 0 ? + Colors.red : + Colors.green + ), + ), + Column( + children: [ + RichText( + textAlign: TextAlign.center, + softWrap: true, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan(text: "${t.top} ${f2.format(currentTl.percentile * 100)}% (${currentTl.percentileRank.toUpperCase()})"), + if (currentTl.bestRank != "z") const TextSpan(text: " • "), + if (currentTl.bestRank != "z") TextSpan(text: "${t.topRank}: ${currentTl.bestRank.toUpperCase()}"), + if (widget.topTR != null) TextSpan(text: " (${f2.format(widget.topTR)} TR)"), + TextSpan(text: " • Glicko: ${f2.format(currentTl.glicko!)}±"), + TextSpan(text: f2.format(currentTl.rd!), style: currentTl.decaying ? TextStyle(color: currentTl.rd! > 98 ? Colors.red : Colors.yellow) : null), + if (currentTl.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: currentTl.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic) + ], + ), + ), + ], + ), + ], + ), + ], + ), + 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,)], + 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 + ), + ), + if (currentTl.gamesPlayed < 10) + Text(t.gamesUntilRanked(left: 10 - currentTl.gamesPlayed), + softWrap: true, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28, + overflow: TextOverflow.visible, + )), + Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm), + if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps), + if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs), + if (currentTl.standingLocal > 0) StatCellNum(playerStat: currentTl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), + StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed), + StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon), + StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null), + ], + ), + ), + if (currentTl.nerdStats != null) + Column( + children: [ + Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), + padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 35, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + SizedBox( + width: 200, + height: 120, + child: SfRadialGauge( + title: GaugeTitle(text: t.statCellNum.app), + axes: [RadialAxis( + startAngle: 180, + endAngle: 360, + showLabels: false, + showTicks: false, + radiusFactor: 2.1, + centerY: 0.5, + minimum: 0, + maximum: 1, + ranges: [ + GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), + GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), + GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), + GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), + GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), + ], + pointers: [ + NeedlePointer( + value: currentTl.nerdStats!.app, + enableAnimation: true, + needleLength: 0.9, + needleStartWidth: 2, + needleEndWidth: 15, + knobStyle: const KnobStyle(color: Colors.transparent), + gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) + ], + annotations: [GaugeAnnotation( + widget: TextButton(child: Text(f3.format(currentTl.nerdStats!.app), + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), + onPressed: (){ + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(t.statCellNum.app, + style: const TextStyle( + fontFamily: "Eurostile Round Extended")), + content: SingleChildScrollView( + child: ListBody(children: [ + Text(t.statCellNum.appDescription), + Text("${t.exactValue}: ${currentTl.nerdStats!.app}") + ]), + ), + actions: [ + TextButton( + child: Text(t.popupActions.ok), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + )); + },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05,), + if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.app - oldTl!.nerdStats!.app), style: TextStyle( + color: currentTl.nerdStats!.app - oldTl!.nerdStats!.app < 0 ? + Colors.red : + Colors.green + ),), positionFactor: 0.05,)], + )],), + ), + SizedBox( + width: 200, + height: 120, + child: SfRadialGauge( + title: const GaugeTitle(text: "VS / APM"), + axes: [RadialAxis( + startAngle: 180, + endAngle: 360, + showTicks: false, + showLabels: false, + radiusFactor: 2.1, + centerY: 0.5, + minimum: 1.8, + maximum: 2.4, + ranges: [ + GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), + GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), + GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), + ], + pointers: [ + NeedlePointer( + value: currentTl.nerdStats!.vsapm, + enableAnimation: true, + needleLength: 0.9, + needleStartWidth: 2, + needleEndWidth: 15, + knobStyle: const KnobStyle(color: Colors.transparent), + gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) + ], + annotations: [GaugeAnnotation( + widget: TextButton(child: Text(f3.format(currentTl.nerdStats!.vsapm), + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), + onPressed: (){ + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("VS / APM", + style: TextStyle( + fontFamily: "Eurostile Round Extended")), + content: SingleChildScrollView( + child: ListBody(children: [ + Text(t.statCellNum.vsapmDescription), + Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") + ]), + ), + actions: [ + TextButton( + child: Text(t.popupActions.ok), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + )); + },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05), + if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm), style: TextStyle( + color: currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm < 0 ? + Colors.red : + Colors.green + ),), positionFactor: 0.05,)], + )],), + ),]), + ), + Wrap( direction: Axis.horizontal, alignment: WrapAlignment.center, spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, clipBehavior: Clip.hardEdge, children: [ - if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm), - if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps), - if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs), - if (currentTl.standingLocal > 0) StatCellNum(playerStat: currentTl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), - StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed), - StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon), - StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null), - ], - ), - ), - if (currentTl.nerdStats != null) - Column( - children: [ - Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 35, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - SizedBox( - width: 200, - height: 120, - child: SfRadialGauge( - title: GaugeTitle(text: t.statCellNum.app), - axes: [RadialAxis( - startAngle: 180, - endAngle: 360, - showLabels: false, - showTicks: false, - radiusFactor: 2.1, - centerY: 0.5, - minimum: 0, - maximum: 1, - ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], - pointers: [ - NeedlePointer( - value: currentTl.nerdStats!.app, - enableAnimation: true, - needleLength: 0.9, - needleStartWidth: 2, - needleEndWidth: 15, - knobStyle: const KnobStyle(color: Colors.transparent), - gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) - ], - annotations: [GaugeAnnotation( - widget: TextButton(child: Text(f3.format(currentTl.nerdStats!.app), - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), - onPressed: (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.statCellNum.app, - style: const TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.app}") - ]), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - )); - },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05,), - if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.app - oldTl!.nerdStats!.app), style: TextStyle( - color: currentTl.nerdStats!.app - oldTl!.nerdStats!.app < 0 ? - Colors.red : - Colors.green - ),), positionFactor: 0.05,)], - )],), - ), - SizedBox( - width: 200, - height: 120, - child: SfRadialGauge( - title: const GaugeTitle(text: "VS / APM"), - axes: [RadialAxis( - startAngle: 180, - endAngle: 360, - showTicks: false, - showLabels: false, - radiusFactor: 2.1, - centerY: 0.5, - minimum: 1.8, - maximum: 2.4, - ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], - pointers: [ - NeedlePointer( - value: currentTl.nerdStats!.vsapm, - enableAnimation: true, - needleLength: 0.9, - needleStartWidth: 2, - needleEndWidth: 15, - knobStyle: const KnobStyle(color: Colors.transparent), - gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) - ], - annotations: [GaugeAnnotation( - widget: TextButton(child: Text(f3.format(currentTl.nerdStats!.vsapm), - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: Colors.white)), - onPressed: (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: const Text("VS / APM", - style: TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") - ]), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - )); - },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05), - if (oldTl != null && oldTl!.gamesPlayed > 0) GaugeAnnotation(widget: Text(fDiff.format(currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm), style: TextStyle( - color: currentTl.nerdStats!.vsapm - oldTl!.nerdStats!.vsapm < 0 ? - Colors.red : - Colors.green - ),), positionFactor: 0.05,)], - )],), - ),]), - ), - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dss,), - StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dsp,), - StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.appdsp,), - StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.cheese,), - StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.gbe,), - StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.nyaapp,), - StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.area,) - ]) - ], - ), - if (currentTl.estTr != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), - child: SizedBox( - width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, + alertWidgets: [Text(t.statCellNum.dssDescription), + Text("${t.formula}: (VS / 100) - (APM / 60)"), + Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.dss,), + StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, + alertWidgets: [Text(t.statCellNum.dspDescription), + Text("${t.formula}: DS/S / PPS"), + Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.dsp,), + StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, + alertWidgets: [Text(t.statCellNum.appdspDescription), + Text("${t.formula}: APP + DS/P"), + Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.appdsp,), + StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, + alertWidgets: [Text(t.statCellNum.cheeseDescription), + Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), + Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.cheese,), + StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, + alertWidgets: [Text(t.statCellNum.gbeDescription), + Text("${t.formula}: APP * DS/P * 2"), + Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.gbe,), + StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, + alertWidgets: [Text(t.statCellNum.nyaappDescription), + Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), + Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.nyaapp,), + StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, + alertWidgets: [Text(t.statCellNum.areaDescription), + Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), + Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], + okText: t.popupActions.ok, + higherIsBetter: true, + oldPlayerStat: oldTl?.nerdStats?.area,) + ]) + ], + ), + if (currentTl.estTr != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), + child: SizedBox( + width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${bigScreen ? t.statCellNum.estOfTR : t.statCellNum.estOfTRShort}:", + style: const TextStyle(fontSize: 24), + ), + Text( + f2.format(currentTl.estTr!.esttr), + style: const TextStyle(fontSize: 24), + ), + ], + ), + if (currentTl.rating >= 0) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${bigScreen ? t.statCellNum.estOfTR : t.statCellNum.estOfTRShort}:", - style: const TextStyle(fontSize: 24), - ), - Text( - f2.format(currentTl.estTr!.esttr), - style: const TextStyle(fontSize: 24), - ), - ], + Text( + "${bigScreen ? t.statCellNum.accOfEst : t.statCellNum.accOfEstShort}:", + style: const TextStyle(fontSize: 24), + ), + Text( + fDiff.format(currentTl.esttracc!), + style: const TextStyle(fontSize: 24), ), - if (currentTl.rating >= 0) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${bigScreen ? t.statCellNum.accOfEst : t.statCellNum.accOfEstShort}:", - style: const TextStyle(fontSize: 24), - ), - Text( - fDiff.format(currentTl.esttracc!), - style: const TextStyle(fontSize: 24), - ), - ], - ), ], ), - ), - ), - if (currentTl.nerdStats != null) Graphs(currentTl.apm!, currentTl.pps!, currentTl.vs!, currentTl.nerdStats!, currentTl.playstyle!) - ] - : [ - Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,)), - ], + ], + ), + ), + ), + if (currentTl.nerdStats != null) Graphs(currentTl.apm!, currentTl.pps!, currentTl.vs!, currentTl.nerdStats!, currentTl.playstyle!) + ] ); }, ); diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index a2fa26d..03708a4 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -5,7 +5,7 @@ import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/views/compare_view.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/views/main_view.dart' show textShadow; +import 'package:tetra_stats/utils/text_shadow.dart'; import 'dart:developer' as developer; import 'package:tetra_stats/widgets/stat_sell_num.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 45c37ae..a2fd891 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.4.0+14 +version: 1.4.1+15 environment: sdk: '>=3.0.0' diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index b186ea9..4145f76 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -30,8 +30,12 @@ }, "openSearch": "Search player", "closeSearch": "Close search", + "searchHint": "Nickname, ID or Discord userID (with \"ds:\" prefix)", "refresh": "Refresh", "fetchAndsaveTLHistory": "Get player history", + "fetchAndSaveOldTLmatches": "Get Tetra League matches history", + "fetchAndsaveTLHistoryResult": "${number} states was found", + "fetchAndSaveOldTLmatchesResult": "${number} matches was found", "showStoredData": "Show stored data", "statsCalc": "Stats Calculator", "settings": "Settings", @@ -86,8 +90,8 @@ "importCancelled": "Operation was cancelled", "importSuccess": "Import successful", "yourID": "Your TETR.IO account", - "yourIDAlertTitle": "Your TETR.IO account nickname or ID", - "yourIDText": "Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed.", + "yourIDAlertTitle": "Your nickname in TETR.IO", + "yourIDText": "When app loads, it will retrieve data for this account", "language": "Language", "aboutApp": "About app", "aboutAppText": "${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy", @@ -127,7 +131,7 @@ "winChance": "Win Chance", "byGlicko": "By Glicko", "byEstTR": "By Est. TR", - "compareViewNoValues": "Please, enter username, user ID, APM-PPS-VS values (divider doesn't matter, only order matter) or $avgR (where R is rank) to both of fields", + "compareViewNoValues": "Please, enter username, user ID, APM-PPS-VS values (divider doesn't matter, only order matter) or $avgR (where R is rank) to both fields", "compareViewWrongValue": "Falied to assign ${value}", "mostRecentOne": "Most recent one", "yes": "Yes", @@ -246,6 +250,7 @@ "connection": "Some issue with connection: ${code} ${message}", "noSuchUser": "No such user", "history": "History for that player is missing", + "p1nkl0bst3rTLmatches": "No Tetra League matches was found", "clientException": "No internet connection", "forbidden": "Your IP address is blocked.\nChange IP address or reach out to osk", "tooManyRequests": "You have been rate limited. Try again later", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 25f4ffd..f8dba3b 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -30,8 +30,12 @@ }, "openSearch": "Искать игрока", "closeSearch": "Закрыть поиск", + "searchHint": "Ник, ID или ID в Discord (с префиксом \"ds:\")", "refresh": "Обновить", "fetchAndsaveTLHistory": "Получить историю игрока", + "fetchAndSaveOldTLmatches": "Получить старые матчи Тетра Лиги", + "fetchAndsaveTLHistoryResult": "${number} состояний было найдено", + "fetchAndSaveOldTLmatchesResult": "${number} старых матчей было найдено", "showStoredData": "Показать сохранённые данные", "statsCalc": "Калькулятор статистики", "settings": "Настройки", @@ -86,8 +90,8 @@ "importCancelled": "Операция была отменена", "importSuccess": "Успешно импортировано", "yourID": "Ваш аккаунт в TETR.IO", - "yourIDAlertTitle": "Никнейм или ID вашего аккаунта в TETR.IO", - "yourIDText": "Каждый раз, когда приложение запускается, приложение будет получать статистику этого игрока. Пожалуйста, отдайте предпочтение ID, так как никнейм можно изменить.", + "yourIDAlertTitle": "Ваш ник в TETR.IO", + "yourIDText": "При запуске приложения оно будет получать статистику этого игрока.", "language": "Язык (Language)", "aboutApp": "О приложении", "aboutAppText": "${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy", @@ -246,6 +250,7 @@ "connection": "Проблема с подключением: ${code} ${message}", "noSuchUser": "Нет такого пользователя", "history": "История данного игрока отсутствует", + "p1nkl0bst3rTLmatches": "Старых матчей Тетра Лиги не было найдено", "clientException": "Нет соединения с интернетом", "forbidden": "Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом", "tooManyRequests": "Слишком много запросов. Попробуйте позже", diff --git a/res/tetrio_badges/twc23_honorary.png b/res/tetrio_badges/twc23_honorary.png new file mode 100644 index 0000000..469ce26 Binary files /dev/null and b/res/tetrio_badges/twc23_honorary.png differ