From 7ed93d3fb165b80c80de4e2c0c73fef6f8d5edc6 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 23 Sep 2023 22:09:36 +0300 Subject: [PATCH] App now can tell what happening with connection Also now we can manage stored matches --- lib/gen/strings.g.dart | 64 ++++- lib/services/crud_exceptions.dart | 18 ++ lib/services/custom_http_client.dart | 14 + lib/services/tetrio_crud.dart | 368 ++++++++++++++++++--------- lib/views/compare_view.dart | 5 +- lib/views/main_view.dart | 296 +++++++++------------ lib/views/mathes_view.dart | 73 ++++++ lib/views/rank_averages_view.dart | 6 +- lib/views/ranks_averages_view.dart | 4 +- lib/views/states_view.dart | 13 +- pubspec.lock | 28 +- pubspec.yaml | 4 +- res/i18n/strings.i18n.json | 15 +- res/i18n/strings_ru.i18n.json | 15 +- 14 files changed, 585 insertions(+), 338 deletions(-) create mode 100644 lib/services/custom_http_client.dart create mode 100644 lib/views/mathes_view.dart diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 6f59e6c..b3e157c 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: 914 (457 per locale) +/// Strings: 940 (470 per locale) /// -/// Built on 2023-09-06 at 18:46 UTC +/// Built on 2023-09-23 at 18:57 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -223,8 +223,11 @@ class _StringsEn implements BaseTranslations { 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'; String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}'; String statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; + String matchesViewTitle({required Object nickname}) => '${nickname} TL matches'; String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD'; String stateRemoved({required Object date}) => '${date} state was removed from database!'; + String matchRemoved({required Object date}) => '${date} match was removed from database!'; + String get viewAllMatches => 'View all matches'; String get trackedPlayersViewTitle => 'Stored data'; String get trackedPlayersZeroEntrys => 'Empty list. Press "Track" button in previous view to add current player here'; String get trackedPlayersOneEntry => 'There is only one player'; @@ -654,7 +657,17 @@ class _StringsErrorsEn { // Translations String connection({required Object code, required Object message}) => 'Some issue with connection: ${code} ${message}'; String get noSuchUser => 'No such user'; - String socketException({required Object host, required Object message}) => 'Can\'t connect with ${host}: ${message}'; + String get history => 'History for that player is missing'; + 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'; + String get internal => 'Something happend on the tetr.io side'; + String get internalWebVersion => 'Something happend on the tetr.io side (or on oskware_bridge, idk honestly)'; + String get oskwareBridge => 'Something happend with oskware_bridge. Let dan63047 know'; + String get p1nkl0bst3rForbidden => 'Third party API blocked your IP address.\nChange IP address or reach out to p1nkl0bst3r'; + String get p1nkl0bst3rTooManyRequests => 'Too many requests to third party API. Try again later'; + String get p1nkl0bst3rinternal => 'Something happend on the p1nkl0bst3r side'; + String get p1nkl0bst3rinternalWebVersion => 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; } // Path: @@ -755,8 +768,11 @@ class _StringsRu implements _StringsEn { @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'; @override String stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; @override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; + @override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${nickname}'; @override String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD'; @override String stateRemoved({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!'; + @override String matchRemoved({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; + @override String get viewAllMatches => 'Все матчи'; @override String get trackedPlayersViewTitle => 'Сохранённые данные'; @override String get trackedPlayersZeroEntrys => 'Пустой список. Вернитесь на предыдущий экран и нажмите кнопку "Отслеживать", чтобы текущий игрок появился здесь'; @override String get trackedPlayersOneEntry => 'В списке только один игрок'; @@ -1186,7 +1202,17 @@ class _StringsErrorsRu implements _StringsErrorsEn { // Translations @override String connection({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; @override String get noSuchUser => 'Нет такого пользователя'; - @override String socketException({required Object host, required Object message}) => 'Невозможно подключиться к ${host}: ${message}'; + @override String get history => 'История данного игрока отсутствует'; + @override String get clientException => 'Нет соединения с интернетом'; + @override String get forbidden => 'Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом'; + @override String get tooManyRequests => 'Слишком много запросов. Попробуйте позже'; + @override String get internal => 'Что-то случилось на стороне tetr.io'; + @override String get internalWebVersion => 'Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)'; + @override String get oskwareBridge => 'Что-то случилось с oskware_bridge. Дайте dan63047 знать'; + @override String get p1nkl0bst3rForbidden => 'Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с p1nkl0bst3r-ом'; + @override String get p1nkl0bst3rTooManyRequests => 'Слишком много запросов к стороннему API. Попробуйте позже'; + @override String get p1nkl0bst3rinternal => 'Что-то случилось на стороне p1nkl0bst3r-а'; + @override String get p1nkl0bst3rinternalWebVersion => 'Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)'; } /// Flat map(s) containing all translations. @@ -1266,8 +1292,11 @@ extension on _StringsEn { 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'; case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} account on ${date}'; case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; + case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches'; case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD'; case 'stateRemoved': return ({required Object date}) => '${date} state was removed from database!'; + case 'matchRemoved': return ({required Object date}) => '${date} match was removed from database!'; + case 'viewAllMatches': return 'View all matches'; case 'trackedPlayersViewTitle': return 'Stored data'; case 'trackedPlayersZeroEntrys': return 'Empty list. Press "Track" button in previous view to add current player here'; case 'trackedPlayersOneEntry': return 'There is only one player'; @@ -1394,7 +1423,17 @@ extension on _StringsEn { case 'popupActions.ok': return 'OK'; 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.socketException': return ({required Object host, required Object message}) => 'Can\'t connect with ${host}: ${message}'; + case 'errors.history': return 'History for that player is missing'; + 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'; + case 'errors.internal': return 'Something happend on the tetr.io side'; + case 'errors.internalWebVersion': return 'Something happend on the tetr.io side (or on oskware_bridge, idk honestly)'; + case 'errors.oskwareBridge': return 'Something happend with oskware_bridge. Let dan63047 know'; + case 'errors.p1nkl0bst3rForbidden': return 'Third party API blocked your IP address.\nChange IP address or reach out to p1nkl0bst3r'; + case 'errors.p1nkl0bst3rTooManyRequests': return 'Too many requests to third party API. Try again later'; + case 'errors.p1nkl0bst3rinternal': return 'Something happend on the p1nkl0bst3r side'; + case 'errors.p1nkl0bst3rinternalWebVersion': return 'Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)'; case 'countries.': return 'Not selected'; case 'countries.AF': return 'Afghanistan'; case 'countries.AX': return 'Åland Islands'; @@ -1733,8 +1772,11 @@ extension on _StringsRu { 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'; case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; + case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${nickname}'; case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD'; case 'stateRemoved': return ({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!'; + case 'matchRemoved': return ({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; + case 'viewAllMatches': return 'Все матчи'; case 'trackedPlayersViewTitle': return 'Сохранённые данные'; case 'trackedPlayersZeroEntrys': return 'Пустой список. Вернитесь на предыдущий экран и нажмите кнопку "Отслеживать", чтобы текущий игрок появился здесь'; case 'trackedPlayersOneEntry': return 'В списке только один игрок'; @@ -1861,7 +1903,17 @@ extension on _StringsRu { case 'popupActions.ok': return 'OK'; case 'errors.connection': return ({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; case 'errors.noSuchUser': return 'Нет такого пользователя'; - case 'errors.socketException': return ({required Object host, required Object message}) => 'Невозможно подключиться к ${host}: ${message}'; + case 'errors.history': return 'История данного игрока отсутствует'; + case 'errors.clientException': return 'Нет соединения с интернетом'; + case 'errors.forbidden': return 'Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом'; + case 'errors.tooManyRequests': return 'Слишком много запросов. Попробуйте позже'; + case 'errors.internal': return 'Что-то случилось на стороне tetr.io'; + case 'errors.internalWebVersion': return 'Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)'; + case 'errors.oskwareBridge': return 'Что-то случилось с oskware_bridge. Дайте dan63047 знать'; + case 'errors.p1nkl0bst3rForbidden': return 'Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с p1nkl0bst3r-ом'; + case 'errors.p1nkl0bst3rTooManyRequests': return 'Слишком много запросов к стороннему API. Попробуйте позже'; + case 'errors.p1nkl0bst3rinternal': return 'Что-то случилось на стороне p1nkl0bst3r-а'; + case 'errors.p1nkl0bst3rinternalWebVersion': return 'Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)'; case 'countries.': return 'Не выбрана'; case 'countries.AF': return 'Афганистан'; case 'countries.AX': return 'Аландские острова'; diff --git a/lib/services/crud_exceptions.dart b/lib/services/crud_exceptions.dart index 6588e1d..2fa735c 100644 --- a/lib/services/crud_exceptions.dart +++ b/lib/services/crud_exceptions.dart @@ -6,12 +6,30 @@ class UnableToGetDocuments implements Exception {} class CouldNotDeletePlayer implements Exception {} +class CouldNotDeleteMatch implements Exception {} + class CouldNotUpdatePlayer implements Exception {} class TetrioPlayerAlreadyExist implements Exception {} class TetrioPlayerNotExist implements Exception {} +class TetrioHistoryNotExist implements Exception {} + +class TetrioTooManyRequests implements Exception {} + +class TetrioForbidden implements Exception {} + +class P1nkl0bst3rTooManyRequests implements Exception {} + +class P1nkl0bst3rForbidden implements Exception {} + +class P1nkl0bst3rInternalProblem implements Exception {} + +class TetrioOskwareBridgeProblem implements Exception {} + +class TetrioInternalProblem implements Exception {} + class ConnectionIssue implements Exception { const ConnectionIssue(this.code, this.message); diff --git a/lib/services/custom_http_client.dart b/lib/services/custom_http_client.dart new file mode 100644 index 0000000..004f2aa --- /dev/null +++ b/lib/services/custom_http_client.dart @@ -0,0 +1,14 @@ +import 'package:http/http.dart' as http; + +class UserAgentClient extends http.BaseClient { + final String userAgent; + final http.Client _inner; + + UserAgentClient(this.userAgent, this._inner); + + @override + Future send(http.BaseRequest request) { + request.headers['user-agent'] = userAgent; + return _inner.send(request); + } +} \ No newline at end of file diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 873b624..f3ad219 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; +import 'package:tetra_stats/services/custom_http_client.dart'; import 'package:http/http.dart' as http; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; @@ -53,6 +54,7 @@ class TetrioService extends DB { final Map> _recordsCache = {}; final Map _leaderboardsCache = {}; final Map _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} + final client = UserAgentClient("Tetra Stats v1.2.3 (dm @dan63047 if someone abuse that software)", http.Client()); static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; late final StreamController>> _tetrioStreamController; @@ -109,54 +111,74 @@ class TetrioService extends DB { } else { url = Uri.https('api.p1nkl0bst3r.xyz', 'tlhist/$id'); } - final response = await http.get(url); - if (response.statusCode == 200) { - List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); - List history = []; - String nick = await getNicknameByID(id); - for (List entry in csv){ - TetrioPlayer state = TetrioPlayer( - userId: id, - username: nick, - role: "p1nkl0bst3r", - state: DateTime.parse(entry[9]), - badges: [], - friendCount: -1, - gamesPlayed: -1, - gamesWon: -1, - gameTime: const Duration(seconds: -1), - xp: -1, - supporterTier: 0, - verified: false, - connections: null, - tlSeason1: TetraLeagueAlpha(timestamp: DateTime.parse(entry[9]), apm: entry[6] != '' ? entry[6] : null, pps: entry[7] != '' ? entry[7] : null, vs: entry[8] != '' ? entry[8] : null, glicko: entry[4], rd: noTrRd, gamesPlayed: entry[1], gamesWon: entry[2], bestRank: "z", decaying: false, rating: entry[3], rank: entry[5], percentileRank: entry[5], percentile: rankCutoffs[entry[5]]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), - sprint: [], - blitz: [] - ); - history.add(state); + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + List> csv = const CsvToListConverter().convert(response.body)..removeAt(0); + List history = []; + String nick = await getNicknameByID(id); + for (List entry in csv){ + TetrioPlayer state = TetrioPlayer( + userId: id, + username: nick, + role: "p1nkl0bst3r", + state: DateTime.parse(entry[9]), + badges: [], + friendCount: -1, + gamesPlayed: -1, + gamesWon: -1, + gameTime: const Duration(seconds: -1), + xp: -1, + supporterTier: 0, + verified: false, + connections: null, + tlSeason1: TetraLeagueAlpha(timestamp: DateTime.parse(entry[9]), apm: entry[6] != '' ? entry[6] : null, pps: entry[7] != '' ? entry[7] : null, vs: entry[8] != '' ? entry[8] : null, glicko: entry[4], rd: noTrRd, gamesPlayed: entry[1], gamesWon: entry[2], bestRank: "z", decaying: false, rating: entry[3], rank: entry[5], percentileRank: entry[5], percentile: rankCutoffs[entry[5]]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), + sprint: [], + blitz: [] + ); + history.add(state); + } + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + late List states; + try{ + states = _players[id]!; + }catch(e){ + var player = await fetchPlayer(id); + await createPlayer(player); + states = _players[id]!; + } + states.insertAll(0, history.reversed); + final Map statesJson = {}; + for (var e in states) { + statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); + } + 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); + 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("fetchTLHistory: Failed to fetch history", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } - await ensureDbIsOpen(); - final db = getDatabaseOrThrow(); - late List states; - try{ - states = _players[id]!; - }catch(e){ - var player = await fetchPlayer(id); - await createPlayer(player); - states = _players[id]!; - } - states.insertAll(0, history.reversed); - final Map statesJson = {}; - for (var e in states) { - statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); - } - await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]); - _tetrioStreamController.add(_players); - return history; - } - else { - developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); - throw Exception('Failed to fetch player'); + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); } } @@ -179,21 +201,39 @@ class TetrioService extends DB { } else { url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); } - final response = await http.get(url); - if (response.statusCode == 200) { - var rawJson = jsonDecode(response.body); - if (rawJson['success']) { - TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at'])); - developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); - _leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; - return leaderboard; - } else { - developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); - throw Exception("User doesn't exist"); + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + var rawJson = jsonDecode(response.body); + if (rawJson['success']) { + TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at'])); + developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + _leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + return leaderboard; + } else { + developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); + throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } - } else { - developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); - throw Exception('Failed to fetch player'); + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); } } @@ -217,22 +257,38 @@ class TetrioService extends DB { } else { url = Uri.https('ch.tetr.io', 'api/streams/league_userrecent_${userID.toLowerCase().trim()}'); } - final response = await http.get(url); + try { + final response = await client.get(url); - if (response.statusCode == 200) { - if (jsonDecode(response.body)['success']) { - TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson( - jsonDecode(response.body)['data']['records'], userID); - developer.log("getTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); - _tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream; - return stream; - } else { - developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body); - throw Exception("User doesn't exist"); + switch (response.statusCode) { + case 200: + if (jsonDecode(response.body)['success']) { + TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID); + developer.log("getTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); + _tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream; + return stream; + } else { + developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body); + throw TetrioPlayerNotExist(); + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("getTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } - } else { - developer.log("getTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode); - throw Exception('Failed to fetch player'); + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); } } @@ -257,6 +313,15 @@ class TetrioService extends DB { return matches; } + Future deleteTLMatch(String matchID) async { + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + final results = await db.delete(tetraLeagueMatchesTable, where: '$idCol = ?', whereArgs: [matchID]); + if (results != 1) { + throw CouldNotDeleteMatch(); + } + } + Future> fetchRecords(String userID) async { try{ var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID); @@ -277,29 +342,46 @@ class TetrioService extends DB { } else { url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records'); } - final response = await http.get(url); + try{ + final response = await client.get(url); - if (response.statusCode == 200) { - if (jsonDecode(response.body)['success']) { - Map jsonRecords = jsonDecode(response.body); - var sprint = jsonRecords['data']['records']['40l']['record'] != null - ? [RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'])] - : []; - var blitz = jsonRecords['data']['records']['blitz']['record'] != null - ? [RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])] - : []; - var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); - Map map = {"user": userID.toLowerCase().trim(), "sprint": sprint, "blitz": blitz, "zen": zen}; - developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud"); - _recordsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = map; - return map; - } else { - developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body); - throw Exception("User doesn't exist"); + switch (response.statusCode) { + case 200: + if (jsonDecode(response.body)['success']) { + Map jsonRecords = jsonDecode(response.body); + var sprint = jsonRecords['data']['records']['40l']['record'] != null + ? [RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'])] + : []; + var blitz = jsonRecords['data']['records']['blitz']['record'] != null + ? [RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])] + : []; + var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); + Map map = {"user": userID.toLowerCase().trim(), "sprint": sprint, "blitz": blitz, "zen": zen}; + developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud"); + _recordsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = map; + return map; + } else { + developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body); + throw TetrioPlayerNotExist(); + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchRecords Failed to fetch records", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } - } else { - developer.log("fetchRecords Failed to fetch records", name: "services/tetrio_crud", error: response.statusCode); - throw Exception('Failed to fetch player'); + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); } } @@ -332,11 +414,7 @@ class TetrioService extends DB { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final results = await db.query(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); - if (results.isEmpty) { - return false; - } else { - return true; - } + return results.isNotEmpty; } Future> getAllPlayerToTrack() async { @@ -437,43 +515,79 @@ class TetrioService extends DB { } else { dUrl = Uri.https('ch.tetr.io', 'api/users/search/${user.toLowerCase().trim()}'); } - final response = await http.get(dUrl); - if (response.statusCode == 200) { - var json = jsonDecode(response.body); - if (json['success'] && json['data'] != null) { - user = json['data']['user']['_id']; - } else { - developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); - throw TetrioPlayerNotExist(); + try{ + final response = await client.get(dUrl); + + switch (response.statusCode) { + case 200: + var json = jsonDecode(response.body); + if (json['success'] && json['data'] != null) { + user = json['data']['user']['_id']; + } else { + developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); + throw TetrioPlayerNotExist(); + } + break; + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchPlayer Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } - } else { - developer.log("fetchPlayer Failed to fetch player", 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); } } - + Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()}); } else { url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}'); } - final response = await http.get(url); + try{ + final response = await client.get(url); - if (response.statusCode == 200) { - var json = jsonDecode(response.body); - if (json['success']) { - TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username']); - developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); - _playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player; - return player; - } else { - developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); - throw TetrioPlayerNotExist(); + switch (response.statusCode) { + case 200: + var json = jsonDecode(response.body); + if (json['success']) { + TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username']); + developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); + _playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player; + return player; + } else { + developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); + throw TetrioPlayerNotExist(); + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchPlayer Failed to fetch player", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } - } else { - developer.log("fetchPlayer Failed to fetch player", 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); } } diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index d6bdca7..a93d729 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -875,7 +875,10 @@ class CompareState extends State { ) ], ) - ] : [Text(t.compareViewNoValues(avgR: "\$avgR"))], // This is so fucked up holy shit + ] : [Padding( + padding: const EdgeInsets.all(8.0), + child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), + )], // This is so fucked up holy shit ) ), ), diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 8de0627..8cb1835 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; 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:fl_chart/fl_chart.dart'; @@ -31,6 +32,7 @@ const allowedHeightForPlayerBioInPixels = 30.0; const givenTextHeightByScreenPercentage = 0.3; final NumberFormat timeInSec = NumberFormat("#,###.###s."); final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); +final NumberFormat secs = NumberFormat("00.###"); final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); @@ -348,19 +350,37 @@ class _MainState extends State with SingleTickerProviderStateMixin { var err = snapshot.error as ConnectionIssue; errText = t.errors.connection(code: err.code, message: err.message); break; - case SocketException: // TODO: Find a way to catch - var err = snapshot.error as SocketException; - errText = t.errors.socketException(host: err.address!.host, message: err.osError!.message); + 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; + case TetrioForbidden: + errText = t.errors.forbidden; + break; + case TetrioTooManyRequests: + errText = t.errors.tooManyRequests; + break; + case TetrioOskwareBridgeProblem: + errText = t.errors.oskwareBridge; + break; + case TetrioInternalProblem: + errText = kIsWeb ? t.errors.internalWebVersion : t.errors.internal; + break; + case ClientException: + errText = t.errors.clientException; break; default: errText = snapshot.error.toString(); } - return Center( - child: Text(errText, - style: const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 42), - textAlign: TextAlign.center)); + return Center(child: Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center)); } break; default: @@ -570,7 +590,7 @@ class _History extends StatelessWidget{ else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) ], ), - ] : [Center(child: Text(t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]); + ] : [Center(child: Text(t.noHistorySaved, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]); } } @@ -650,7 +670,7 @@ class _RecordThingy extends StatelessWidget { fontSize: bigScreen ? 42 : 28)), if (record!.stream.contains("40l")) if (record!.endContext!.finalTime.inMicroseconds > 60000000) Text( - "${(record!.endContext!.finalTime.inMicroseconds/1000000/60).floor()}:${(f2.format(record!.endContext!.finalTime.inMicroseconds /1000000 % 60))}", + "${(record!.endContext!.finalTime.inMicroseconds/1000000/60).floor()}:${(secs.format(record!.endContext!.finalTime.inMicroseconds /1000000 % 60))}", style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) @@ -755,194 +775,108 @@ class _RecordThingy extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("${t.numOfGameActions.pc}:", - style: const TextStyle(fontSize: 24)), - Text( - record!.endContext!.clears.allClears - .toString(), - style: const TextStyle(fontSize: 24), - ), + Text("${t.numOfGameActions.pc}:", style: const TextStyle(fontSize: 24)), + Text(record!.endContext!.clears.allClears.toString(), style: const TextStyle(fontSize: 24)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("${t.numOfGameActions.hold}:", style: const TextStyle(fontSize: 24)), + Text(record!.endContext!.holds.toString(), style: const TextStyle(fontSize: 24)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("${t.numOfGameActions.tspinsTotal}:", style: const TextStyle(fontSize: 24)), + Text(record!.endContext!.tSpins.toString(), style: const TextStyle(fontSize: 24)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text(" - T-spin zero:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.tSpinZeros.toString(), style: const TextStyle(fontSize: 18)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text(" - T-spin singles:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.tSpinSingles.toString(), style: const TextStyle(fontSize: 18)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text(" - T-spin doubles:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.tSpinDoubles.toString(), style: const TextStyle(fontSize: 18)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text(" - T-spin triples:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.tSpinTriples.toString(), style: const TextStyle(fontSize: 18)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text(" - T-spin mini zero:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.tSpinMiniZeros.toString(), style: const TextStyle(fontSize: 18)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text(" - T-spin mini singles:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.tSpinMiniSingles.toString(), style: const TextStyle(fontSize: 18)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text(" - T-spin mini doubles:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.tSpinMiniDoubles.toString(), style: const TextStyle(fontSize: 18)), + ], + ), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("${t.numOfGameActions.lineClears}:", style: const TextStyle(fontSize: 24)), + Text(record!.endContext!.lines.toString(), style: const TextStyle(fontSize: 24)), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("${t.numOfGameActions.hold}:", - style: const TextStyle(fontSize: 24)), - Text( - record!.endContext!.holds.toString(), - style: const TextStyle(fontSize: 24), - ), + const Text(" - Singles:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.singles.toString(), style: const TextStyle(fontSize: 18)), ], ), Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("${t.numOfGameActions.tspinsTotal}:", - style: const TextStyle(fontSize: 24)), - Text( - record!.endContext!.tSpins.toString(), - style: const TextStyle(fontSize: 24), - ), + const Text(" - Doubles:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.doubles.toString(), style: const TextStyle(fontSize: 18)), ], ), Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text(" - T-spin zero:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.tSpinZeros - .toString(), - style: const TextStyle(fontSize: 18), - ), + const Text(" - Triples:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.triples.toString(), style: const TextStyle(fontSize: 18)), ], ), Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text(" - T-spin singles:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.tSpinSingles - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin doubles:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.tSpinDoubles - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin triples:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.tSpinTriples - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin mini zero:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.tSpinMiniZeros - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin mini singles:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.tSpinMiniSingles - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - T-spin mini doubles:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.tSpinMiniDoubles - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text("${t.numOfGameActions.lineClears}:", - style: const TextStyle(fontSize: 24)), - Text( - record!.endContext!.lines.toString(), - style: const TextStyle(fontSize: 24), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Singles:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.singles - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Doubles:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.doubles - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Triples:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.triples - .toString(), - style: const TextStyle(fontSize: 18), - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text(" - Quads:", - style: TextStyle(fontSize: 18)), - Text( - record!.endContext!.clears.quads.toString(), - style: const TextStyle(fontSize: 18), - ), + const Text(" - Quads:", style: TextStyle(fontSize: 18)), + Text(record!.endContext!.clears.quads.toString(), style: const TextStyle(fontSize: 18)), ], ), ], @@ -951,7 +885,7 @@ class _RecordThingy extends StatelessWidget { ), ] : [ - Text(t.noRecord, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)) + Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)) ], ); }); diff --git a/lib/views/mathes_view.dart b/lib/views/mathes_view.dart new file mode 100644 index 0000000..57f9a1a --- /dev/null +++ b/lib/views/mathes_view.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/services/tetrio_crud.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/views/tl_match_view.dart'; + +final TetrioService teto = TetrioService(); +final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); + +class MatchesView extends StatefulWidget { + final String userID; + final String username; + const MatchesView({Key? key, required this.userID, required this.username}) : super(key: key); + + @override + State createState() => MatchesState(); +} + +class MatchesState extends State { + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + bool bigScreen = MediaQuery.of(context).size.width > 768; + final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); + return Scaffold( + appBar: AppBar( + title: Text(t.matchesViewTitle(nickname: widget.username)), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: FutureBuilder( + future: teto.getTLMatchesbyPlayerID(widget.userID), + builder: (context, snapshot){ + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator(color: Colors.white)); + case ConnectionState.done: + return ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: (snapshot.data!.isNotEmpty) + ? [for (var value in snapshot.data!) ListTile( + leading: Text("${value.endContext.firstWhere((element) => element.userId == widget.userID).points} : ${value.endContext.firstWhere((element) => element.userId != widget.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 != widget.userID).username}"), + subtitle: Text(dateFormat.format(value.timestamp)), + trailing: IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () { + DateTime nn = value.timestamp; + teto.deleteTLMatch(value.ownId).then((value) => setState(() { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.matchRemoved(date: dateFormat.format(nn))))); + })); + }, + ), + onTap: (){Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TlMatchResultView(record: value, initPlayerId: widget.userID), + ), + );}, + )] + : [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))], + ); + } + } + ) + ) + ); + } +} diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 76fc954..6a79b5f 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -346,7 +346,7 @@ class RankState extends State with SingleTickerProviderStateMixin { _ListEntry(value: widget.rank[1]["lowestVSAPM"], label: "VS / APM", id: widget.rank[1]["lowestVSAPMid"], username: widget.rank[1]["lowestVSAPMnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["lowestDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestDSSid"], username: widget.rank[1]["lowestDSSnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["lowestDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestDSPid"], username: widget.rank[1]["lowestDSPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestAPPDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPPDSPid"], username: widget.rank[1]["lowestAPPDSPnick"], approximate: false, fractionDigits: 3), + _ListEntry(value: widget.rank[1]["lowestAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPPDSPid"], username: widget.rank[1]["lowestAPPDSPnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["lowestCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestCheeseID"], username: widget.rank[1]["lowestCheeseNick"], approximate: false, fractionDigits: 2), _ListEntry(value: widget.rank[1]["lowestGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGBEid"], username: widget.rank[1]["lowestGBEnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["lowestNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestNyaAPPid"], username: widget.rank[1]["lowestNyaAPPnick"], approximate: false, fractionDigits: 3), @@ -380,7 +380,7 @@ class RankState extends State with SingleTickerProviderStateMixin { _ListEntry(value: widget.rank[1]["avgAPP"], label: "VS / APM", id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgAPPDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), + _ListEntry(value: widget.rank[1]["avgAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), _ListEntry(value: widget.rank[1]["avgGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), @@ -413,7 +413,7 @@ class RankState extends State with SingleTickerProviderStateMixin { _ListEntry(value: widget.rank[1]["highestVSAPM"], label: "VS / APM", id: widget.rank[1]["highestVSAPMid"], username: widget.rank[1]["highestVSAPMnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["highestDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestDSSid"], username: widget.rank[1]["highestDSSnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["highestDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestDSPid"], username: widget.rank[1]["highestDSPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestAPPDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPPDSPid"], username: widget.rank[1]["highestAPPDSPnick"], approximate: false, fractionDigits: 3), + _ListEntry(value: widget.rank[1]["highestAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPPDSPid"], username: widget.rank[1]["highestAPPDSPnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["highestCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestCheeseID"], username: widget.rank[1]["highestCheeseNick"], approximate: false, fractionDigits: 2), _ListEntry(value: widget.rank[1]["highestGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGBEid"], username: widget.rank[1]["highestGBEnick"], approximate: false, fractionDigits: 3), _ListEntry(value: widget.rank[1]["highestNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestNyaAPPid"], username: widget.rank[1]["highestNyaAPPnick"], approximate: false, fractionDigits: 3), diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart index 5fbb5d7..02a949c 100644 --- a/lib/views/ranks_averages_view.dart +++ b/lib/views/ranks_averages_view.dart @@ -44,12 +44,14 @@ class RanksAverages extends State { 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"), trailing: Text("${f2.format(averages[keys[index]]?[1]["toEnterTR"])} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), onTap: (){ - Navigator.push( + if (averages[keys[index]]?[1]["players"] > 0) { + Navigator.push( context, MaterialPageRoute( builder: (context) => RankView(rank: averages[keys[index]]!), ), ); + } }, ); }) diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart index 0cb0e45..904f12b 100644 --- a/lib/views/states_view.dart +++ b/lib/views/states_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/views/compare_view.dart'; +import 'package:tetra_stats/views/mathes_view.dart'; import 'package:tetra_stats/views/state_view.dart'; class StatesView extends StatefulWidget { @@ -21,6 +21,17 @@ class StatesState extends State { return Scaffold( appBar: AppBar( title: Text(t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.username.toUpperCase())), + actions: [ + IconButton( + onPressed: (){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MatchesView(userID: widget.states.first.userId, username: widget.states.first.username), + ), + ); + }, icon: const Icon(Icons.list), tooltip: t.viewAllMatches) + ], ), backgroundColor: Colors.black, body: SafeArea( diff --git a/pubspec.lock b/pubspec.lock index ccb4034..f6bd23c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" + sha256: "1227dc3efc4ea571eebb2dfb814506ed2cfb1d4b1b89fb918abdddde617ead3c" url: "https://pub.dev" source: hosted - version: "3.3.8" + version: "3.4.0" args: dependency: transitive description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "182c3f8350cee659f7b115e956047ee3dc672a96665883a545e81581b9a82c72" + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.3+3" file_selector_platform_interface: dependency: transitive description: @@ -361,10 +361,10 @@ packages: dependency: transitive description: name: image - sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + sha256: "6e703d5e2f8c63fb31a77753915c1ec8baebde8088844e0d29f71b8f0b108888" url: "https://pub.dev" source: hosted - version: "4.0.17" + version: "4.1.0" intl: dependency: "direct main" description: @@ -782,10 +782,10 @@ packages: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: fb115050b0c2589afe2085a62d77f5deda4db65db20a5c65a6e0c92fda89b45e + sha256: "11a41f380fbcbda5bbba03ddcdbe0545e46094ab043783c46c70e8335831df03" url: "https://pub.dev" source: hosted - version: "0.5.16" + version: "0.5.17" stack_trace: dependency: transitive description: @@ -814,18 +814,18 @@ packages: dependency: transitive description: name: syncfusion_flutter_core - sha256: "2baf60cd245a21a7069f036bbca1ca222633d38f57748e133da97a305712627c" + sha256: aea119c8117953fa5decf4a313b431e556b0959cd35ff88f8fbdc0eda9bedb06 url: "https://pub.dev" source: hosted - version: "22.2.11" + version: "23.1.36" syncfusion_flutter_gauges: dependency: "direct main" description: name: syncfusion_flutter_gauges - sha256: c086f17e84452e809b12f9832763ec4cea347b9f6e1e662a0e8addabca6cc2e5 + sha256: ae46df959f60f0fed6a8c86c8c971883ed790450f8d32f546dc8a02cb4500cbd url: "https://pub.dev" source: hosted - version: "22.2.11" + version: "23.1.36" synchronized: dependency: transitive description: @@ -1014,10 +1014,10 @@ packages: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: c97defd418eef4ec88c0d1652cdce84b9f7b63dd7198e266d06ac1710d527067 url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.0.8" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0212f7a..afd6412 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.2.2+10 +version: 1.2.3+11 environment: sdk: '>=2.19.6 <3.0.0' @@ -31,7 +31,7 @@ dependencies: package_info_plus: ^4.0.2 shared_preferences: ^2.1.1 intl: ^0.18.0 - syncfusion_flutter_gauges: ^22.1.34 + syncfusion_flutter_gauges: ^23.1.36 file_selector: ^1.0.1 file_picker: ^5.3.2 slang: ^3.20.0 diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 67e5375..1768f00 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -72,8 +72,11 @@ "aboutAppText": "${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r", "stateViewTitle": "${nickname} account on ${date}", "statesViewTitle": "${number} states of ${nickname} account", + "matchesViewTitle": "${nickname} TL matches", "statesViewEntry": "Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD", "stateRemoved": "${date} state was removed from database!", + "matchRemoved": "${date} match was removed from database!", + "viewAllMatches": "View all matches", "trackedPlayersViewTitle": "Stored data", "trackedPlayersZeroEntrys": "Empty list. Press \"Track\" button in previous view to add current player here", "trackedPlayersOneEntry": "There is only one player", @@ -209,7 +212,17 @@ "errors":{ "connection": "Some issue with connection: ${code} ${message}", "noSuchUser": "No such user", - "socketException": "Can't connect with ${host}: ${message}" + "history": "History for that player is missing", + "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", + "internal": "Something happend on the tetr.io side", + "internalWebVersion": "Something happend on the tetr.io side (or on oskware_bridge, idk honestly)", + "oskwareBridge": "Something happend with oskware_bridge. Let dan63047 know", + "p1nkl0bst3rForbidden": "Third party API blocked your IP address.\nChange IP address or reach out to p1nkl0bst3r", + "p1nkl0bst3rTooManyRequests": "Too many requests to third party API. Try again later", + "p1nkl0bst3rinternal": "Something happend on the p1nkl0bst3r side", + "p1nkl0bst3rinternalWebVersion": "Something happend on the p1nkl0bst3r side (or on oskware_bridge, idk honestly)" }, "countries(map)": { "": "Not selected", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 4111c97..c4b4f2a 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -72,8 +72,11 @@ "aboutAppText": "${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r", "stateViewTitle": "Аккаунт ${nickname} ${date}", "statesViewTitle": "${number} состояний аккаунта ${nickname}", + "matchesViewTitle": "Матчи аккаунта ${nickname}", "statesViewEntry": "${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD", "stateRemoved": "Состояние от ${date} было удалено из локальной базы данных!", + "matchRemoved": "Матч от ${date} был удален из локальной базы данных!", + "viewAllMatches": "Все матчи", "trackedPlayersViewTitle": "Сохранённые данные", "trackedPlayersZeroEntrys": "Пустой список. Вернитесь на предыдущий экран и нажмите кнопку \"Отслеживать\", чтобы текущий игрок появился здесь", "trackedPlayersOneEntry": "В списке только один игрок", @@ -209,7 +212,17 @@ "errors":{ "connection": "Проблема с подключением: ${code} ${message}", "noSuchUser": "Нет такого пользователя", - "socketException": "Невозможно подключиться к ${host}: ${message}" + "history": "История данного игрока отсутствует", + "clientException": "Нет соединения с интернетом", + "forbidden": "Ваш IP адрес заблокирован.\nСмените IP адрес или свяжитесь с osk-ом", + "tooManyRequests": "Слишком много запросов. Попробуйте позже", + "internal": "Что-то случилось на стороне tetr.io", + "internalWebVersion": "Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)", + "oskwareBridge": "Что-то случилось с oskware_bridge. Дайте dan63047 знать", + "p1nkl0bst3rForbidden": "Стороннее API заблокировало ваш IP адрес.\nСмените IP адрес или свяжитесь с p1nkl0bst3r-ом", + "p1nkl0bst3rTooManyRequests": "Слишком много запросов к стороннему API. Попробуйте позже", + "p1nkl0bst3rinternal": "Что-то случилось на стороне p1nkl0bst3r-а", + "p1nkl0bst3rinternalWebVersion": "Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)" }, "countries(map)": { "": "Не выбрана",