commit
86d065717b
|
@ -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
|
||||
|
|
|
@ -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<String, dynamic> json) {
|
||||
userId = json['id'] ?? json['user']['_id'];
|
||||
|
|
|
@ -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<AppLocale, Translations> {
|
|||
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<AppLocale, Translations> {
|
|||
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<AppLocale, Translations> {
|
|||
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 'Слишком много запросов. Попробуйте позже';
|
||||
|
|
|
@ -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<String, List<TetrioPlayer>> _players = {};
|
||||
final Map<String, String> _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<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
|
||||
late final StreamController<Map<String, String>> _tetrioStreamController;
|
||||
TetrioService._sharedInstance() {
|
||||
_tetrioStreamController = StreamController<Map<String, List<TetrioPlayer>>>.broadcast(onListen: () {
|
||||
_tetrioStreamController = StreamController<Map<String, String>>.broadcast(onListen: () {
|
||||
_tetrioStreamController.sink.add(_players);
|
||||
});
|
||||
}
|
||||
|
@ -95,17 +95,15 @@ class TetrioService extends DB {
|
|||
await _loadPlayers();
|
||||
}
|
||||
|
||||
Stream<Map<String, List<TetrioPlayer>>> get allPlayers => _tetrioStreamController.stream;
|
||||
Stream<Map<String, String>> get allPlayers => _tetrioStreamController.stream;
|
||||
|
||||
/// Loading and sending to the stream everyone.
|
||||
Future<void> _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<String> 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<TetrioPlayer> 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<TetrioPlayer> states = await getPlayer(id);
|
||||
if (states.isEmpty) await createPlayer(history.first);
|
||||
states.insertAll(0, history.reversed);
|
||||
final Map<String, dynamic> 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<List<TetraLeagueAlphaRecord>> 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<List<dynamic>> csv = const CsvToListConverter().convert(response.body)..removeAt(0);
|
||||
List<TetraLeagueAlphaRecord> 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<TetrioPlayersLeaderboard> 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<Map<String, dynamic>> fetchRecords(String userID) async {
|
||||
try{
|
||||
|
@ -717,9 +803,7 @@ class TetrioService extends DB {
|
|||
// converting to json and store
|
||||
final Map<String, dynamic> 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<void> storeState(TetrioPlayer tetrioPlayer) async {
|
||||
await ensureDbIsOpen();
|
||||
final db = getDatabaseOrThrow();
|
||||
late List<TetrioPlayer> 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<TetrioPlayer> 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<void> deleteState(TetrioPlayer tetrioPlayer) async {
|
||||
await ensureDbIsOpen();
|
||||
final db = getDatabaseOrThrow();
|
||||
late List<TetrioPlayer> states;
|
||||
List<TetrioPlayer> 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<String, dynamic> 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<Iterable<Map<String, List<TetrioPlayer>>>> getAllPlayers() async {
|
||||
/// Retrieves whole [tetrioUsersTable] and returns Map with [TetrioPlayer] objects of everyone in database
|
||||
Future<Map<String, List<TetrioPlayer>>> getAllPlayers() async {
|
||||
await ensureDbIsOpen();
|
||||
final db = getDatabaseOrThrow();
|
||||
final players = await db.query(tetrioUsersTable);
|
||||
Map<String, List<TetrioPlayer>> 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<TetrioPlayer> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
const List<Shadow> textShadow = <Shadow>[ // 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),
|
||||
];
|
|
@ -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<CompareView> {
|
|||
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<CompareView> {
|
|||
}
|
||||
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<CompareView> {
|
|||
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<CompareView> {
|
|||
}
|
||||
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<CompareView> {
|
|||
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() {
|
||||
|
|
|
@ -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<Shadow> textShadow = <Shadow>[ // 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<MainView> 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<MainView> 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<MainView> 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<List> fetch(String nickOrID, {bool fetchHistory = false}) async {
|
||||
/// If at least one request to Tetra Channel API fails, whole function will throw an exception.
|
||||
Future<List> 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<MainView> with TickerProviderStateMixin {
|
|||
TetraLeagueAlpha? compareWith;
|
||||
Set<TetraLeagueAlpha> uniqueTL = {};
|
||||
tlMatches = tlStream.records;
|
||||
var storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches
|
||||
List<TetraLeagueAlphaRecord> 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<TetraLeagueAlphaRecord> 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<MainView> 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<MainView> 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<MainView> 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<MainView> 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<MainView> 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<MainView> 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<NavDrawer> {
|
|||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
final allPlayers = (snapshot.data != null)
|
||||
? snapshot.data as Map<String, List<TetrioPlayer>>
|
||||
: <String, List<TetrioPlayer>>{};
|
||||
? snapshot.data as Map<String, String>
|
||||
: <String, String>{};
|
||||
allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted
|
||||
List<String> keys = allPlayers.keys.toList();
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (context, value) {
|
||||
|
@ -522,7 +536,7 @@ class _NavDrawerState extends State<NavDrawer> {
|
|||
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))
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = <DropdownMenuItem>[for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)];
|
||||
|
|
|
@ -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<SettingsView> {
|
|||
await _setDefaultNickname(player);
|
||||
}
|
||||
|
||||
Future<void> _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<SettingsView> {
|
|||
),
|
||||
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(() {});
|
||||
},
|
||||
)
|
||||
|
|
|
@ -195,6 +195,9 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
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<TlMatchResultView> {
|
|||
)
|
||||
],
|
||||
),
|
||||
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),
|
||||
|
|
|
@ -70,14 +70,15 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
|
|||
),
|
||||
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<String, List<TetrioPlayer>> : <String, List<TetrioPlayer>>{};
|
||||
List<String> keys = allPlayers.keys.toList();
|
||||
return NestedScrollView(
|
||||
|
@ -114,7 +115,7 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
|
|||
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<TrackedPlayersView> {
|
|||
},
|
||||
);
|
||||
}));
|
||||
case ConnectionState.done:
|
||||
return const Center(
|
||||
child: Text('done case of StreamBuilder',
|
||||
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center));
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
|
|
@ -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<StatefulWidget> createState() => _SearchBoxState();
|
||||
}
|
||||
|
||||
class _SearchBoxState extends State<SearchBox>{
|
||||
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}")
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ class _TLThingyState extends State<TLThingy> {
|
|||
@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<TLThingy> {
|
|||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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!)
|
||||
]
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "Слишком много запросов. Попробуйте позже",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
Loading…
Reference in New Issue