Now it's possible to compare players with rank averages.

Also I started working on i18n
This commit is contained in:
dan63047 2023-07-10 20:42:20 +03:00
parent 3fb7b1fabb
commit 04483bfb64
12 changed files with 1017 additions and 164 deletions

View File

@ -17,8 +17,8 @@
- ~~Better UI with delta and hints for stats~~ *v0.2.0, we are here*
- ~~Ability to compare player with APM-PPS-VS stats~~
- ~~Ability to fetch Tetra League leaderboard~~
- ~~Average stats for ranks~~ *dev build are here*
- Ability to compare player with avgRank
- ~~Average stats for ranks~~
- ~~Ability to compare player with avgRank~~ *dev build are here*
- UI Animations
- i18n, EN and RU locales
- Talk with osk about CORS and EndContext in TL matches

572
lib/gen/strings.g.dart Normal file
View File

@ -0,0 +1,572 @@
/// Generated file. Do not edit.
///
/// Locales: 2
/// Strings: 154 (77 per locale)
///
/// Built on 2023-07-10 at 17:08 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
import 'package:flutter/widgets.dart';
import 'package:slang/builder/model/node.dart';
import 'package:slang_flutter/slang_flutter.dart';
export 'package:slang_flutter/slang_flutter.dart';
const AppLocale _baseLocale = AppLocale.en;
/// Supported locales, see extension methods below.
///
/// Usage:
/// - LocaleSettings.setLocale(AppLocale.en) // set locale
/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
enum AppLocale with BaseAppLocale<AppLocale, _StringsEn> {
en(languageCode: 'en', build: _StringsEn.build),
ru(languageCode: 'ru', build: _StringsRu.build);
const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element
@override final String languageCode;
@override final String? scriptCode;
@override final String? countryCode;
@override final TranslationBuilder<AppLocale, _StringsEn> build;
/// Gets current instance managed by [LocaleSettings].
_StringsEn get translations => LocaleSettings.instance.translationMap[this]!;
}
/// Method A: Simple
///
/// No rebuild after locale change.
/// Translation happens during initialization of the widget (call of t).
/// Configurable via 'translate_var'.
///
/// Usage:
/// String a = t.someKey.anotherKey;
/// String b = t['someKey.anotherKey']; // Only for edge cases!
_StringsEn get t => LocaleSettings.instance.currentTranslations;
/// Method B: Advanced
///
/// All widgets using this method will trigger a rebuild when locale changes.
/// Use this if you have e.g. a settings page where the user can select the locale during runtime.
///
/// Step 1:
/// wrap your App with
/// TranslationProvider(
/// child: MyApp()
/// );
///
/// Step 2:
/// final t = Translations.of(context); // Get t variable.
/// String a = t.someKey.anotherKey; // Use t variable.
/// String b = t['someKey.anotherKey']; // Only for edge cases!
class Translations {
Translations._(); // no constructor
static _StringsEn of(BuildContext context) => InheritedLocaleData.of<AppLocale, _StringsEn>(context).translations;
}
/// The provider for method B
class TranslationProvider extends BaseTranslationProvider<AppLocale, _StringsEn> {
TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
static InheritedLocaleData<AppLocale, _StringsEn> of(BuildContext context) => InheritedLocaleData.of<AppLocale, _StringsEn>(context);
}
/// Method B shorthand via [BuildContext] extension method.
/// Configurable via 'translate_var'.
///
/// Usage (e.g. in a widget's build method):
/// context.t.someKey.anotherKey
extension BuildContextTranslationsExtension on BuildContext {
_StringsEn get t => TranslationProvider.of(this).translations;
}
/// Manages all translation instances and the current locale
class LocaleSettings extends BaseFlutterLocaleSettings<AppLocale, _StringsEn> {
LocaleSettings._() : super(utils: AppLocaleUtils.instance);
static final instance = LocaleSettings._();
// static aliases (checkout base methods for documentation)
static AppLocale get currentLocale => instance.currentLocale;
static Stream<AppLocale> getLocaleStream() => instance.getLocaleStream();
static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale useDeviceLocale() => instance.useDeviceLocale();
@Deprecated('Use [AppLocaleUtils.supportedLocales]') static List<Locale> get supportedLocales => instance.supportedLocales;
@Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
/// Provides utility functions without any side effects.
class AppLocaleUtils extends BaseAppLocaleUtils<AppLocale, _StringsEn> {
AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values);
static final instance = AppLocaleUtils._();
// static aliases (checkout base methods for documentation)
static AppLocale parse(String rawLocale) => instance.parse(rawLocale);
static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode);
static AppLocale findDeviceLocale() => instance.findDeviceLocale();
static List<Locale> get supportedLocales => instance.supportedLocales;
static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
}
// translations
// Path: <root>
class _StringsEn implements BaseTranslations<AppLocale, _StringsEn> {
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
_StringsEn.build({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = TranslationMetadata(
locale: AppLocale.en,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
) {
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <en>.
@override final TranslationMetadata<AppLocale, _StringsEn> $meta;
/// Access flat map
dynamic operator[](String key) => $meta.getTranslation(key);
late final _StringsEn _root = this; // ignore: unused_field
// Translations
Map<String, String> get locales => {
'en': 'English',
'ru': 'Russian (Русский)',
};
String get tetraLeague => 'Tetra League';
String get tlRecords => 'TL Records';
String get history => 'History';
String get sprint => '40 Lines';
String get blitz => 'Blitz';
String get other => 'Other';
String get zen => 'Zen';
String get bio => 'Bio';
String get refresh => 'Refresh';
String get showStoredData => 'Show stored data';
String get statsCalc => 'Stats Calculator';
String get settings => 'Settings';
String get track => 'Track';
String get stopTracking => 'Stop\ntracking';
String get becameTracked => 'Added to tracking list!';
String get compare => 'Compare';
String get stoppedBeingTracked => 'Removed from tracking list!';
String get tlLeaderboard => 'Tetra League leaderboard';
String get noRecords => 'No records';
String get noRecord => 'No record';
String get notEnoughData => 'Not enough data';
String get noHistorySaved => 'No history saved';
String obtainDate({required Object date}) => 'Obtained ${date}';
String fetchDate({required Object date}) => 'Fetched ${date}';
String get exactGametime => 'Exact gametime';
String get bigRedBanned => 'BANNED';
String get bigRedBadStanding => 'BAD STANDING';
String get copiedToClipboard => 'Copied to clipboard!';
String get playerRoleAccount => ' account ';
String get wasFromBeginning => 'that was from very beginning';
String get created => 'created';
String get botCreatedBy => 'by';
String get notSupporter => 'Not a supporter';
String get assignedManualy => 'That badge was assigned manualy by TETR.IO admins';
String supporter({required Object tier}) => 'Supporter tier ${tier}';
String comparingWith({required Object date}) => 'Comparing with data from ${date}';
String get top => 'Top';
String get topRank => 'Top Rank';
String get decaying => 'Decaying';
String gamesUntilRanked({required Object left}) => '${left} games until being ranked';
String get nerdStats => 'Nerd Stats';
late final _StringsStatCellNumEn statCellNum = _StringsStatCellNumEn._(_root);
Map<String, String> get playerRole => {
'user': 'User',
'banned': 'Banned',
'bot': 'Bot',
'sysop': 'System operator',
'admin': 'Admin',
'mod': 'Moderator',
'halfmod': 'Community moderator',
};
late final _StringsNumOfGameActionsEn numOfGameActions = _StringsNumOfGameActionsEn._(_root);
late final _StringsPopupActionsEn popupActions = _StringsPopupActionsEn._(_root);
}
// Path: statCellNum
class _StringsStatCellNumEn {
_StringsStatCellNumEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get xpLevel => 'XP Level';
String get hoursPlayed => 'Hours\nPlayed';
String get onlineGames => 'Online\nGames';
String get gamesWon => 'Games\nWon';
String get friends => 'Friends';
String get apm => 'Attack\nPer Minute';
String get vs => 'Versus\nScore';
String get lbp => 'Leaderboard\nplacement';
String get lbpc => 'Country LB\nplacement';
String get gamesPlayed => 'Games\nplayed';
String get gamesWonTL => 'Games\nWon';
String get winrate => 'Winrate\nprecentage';
String get level => 'Level';
String get score => 'Score';
String get spp => 'Score\nPer Piece';
String get pieces => 'Pieces\nPlaced';
String get pps => 'Pieces\nPer Second';
String get finesseFaults => 'Finesse\nFaults';
String get finessePercentage => 'Finesse\nPercentage';
String get keys => 'Key\nPresses';
String get kpp => 'KP Per\nPiece';
String get kps => 'KP Per\nSecond';
}
// Path: numOfGameActions
class _StringsNumOfGameActionsEn {
_StringsNumOfGameActionsEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get pc => 'All Clears';
String get hold => 'Holds';
String get tspinsTotal => 'T-spins total';
String get lineClears => 'Line clears';
}
// Path: popupActions
class _StringsPopupActionsEn {
_StringsPopupActionsEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get ok => 'OK';
}
// Path: <root>
class _StringsRu implements _StringsEn {
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
_StringsRu.build({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = TranslationMetadata(
locale: AppLocale.ru,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
) {
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <ru>.
@override final TranslationMetadata<AppLocale, _StringsEn> $meta;
/// Access flat map
@override dynamic operator[](String key) => $meta.getTranslation(key);
@override late final _StringsRu _root = this; // ignore: unused_field
// Translations
@override Map<String, String> get locales => {
'en': 'Английский (English)',
'ru': 'Русский',
};
@override String get tetraLeague => 'Тетра Лига';
@override String get tlRecords => 'Матчи ТЛ';
@override String get history => 'История';
@override String get sprint => '40 линий';
@override String get blitz => 'Блиц';
@override String get other => 'Другое';
@override String get zen => 'Дзен';
@override String get bio => 'Биография';
@override String get refresh => 'Обновить';
@override String get showStoredData => 'Показать сохранённые данные';
@override String get statsCalc => 'Калькулятор статистики';
@override String get settings => 'Настройки';
@override String get track => 'Отслеживать';
@override String get stopTracking => 'Перестать\nотслеживать';
@override String get becameTracked => 'Добавлен в список отслеживания!';
@override String get stoppedBeingTracked => 'Удалён из списка отслеживания!';
@override String get compare => 'Сравнить';
@override String get tlLeaderboard => 'Таблица лидеров Тетра Лиги';
@override String get noRecords => 'Нет записей';
@override String get noRecord => 'Нет рекорда';
@override String get notEnoughData => 'Недостаточно данных';
@override String get noHistorySaved => 'Нет сохранённой истории';
@override String obtainDate({required Object date}) => 'Получено ${date}';
@override String fetchDate({required Object date}) => 'На момент ${date}';
@override String get exactGametime => 'Время, проведённое в игре';
@override String get bigRedBanned => 'ЗАБАНЕН';
@override String get bigRedBadStanding => 'ПЛОХАЯ РЕПУТАЦИЯ';
@override String get copiedToClipboard => 'Скопировано в буфер обмена!';
@override String get playerRoleAccount => ', аккаунт которого ';
@override String get wasFromBeginning => 'существовал с самого начала';
@override String get created => 'создан';
@override String get botCreatedBy => 'игроком';
@override String get notSupporter => 'Нет саппортерки';
@override String supporter({required Object tier}) => 'Саппортерка ${tier} уровня';
@override String get assignedManualy => 'Этот значок был присвоен вручную администрацией TETR.IO';
@override String comparingWith({required Object date}) => 'Сравнивая с данными от ${date}';
@override String get top => 'Топ';
@override String get topRank => 'Топ Ранг';
@override String get decaying => 'Загнивает';
@override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга';
@override String get nerdStats => 'Для задротов';
@override late final _StringsStatCellNumRu statCellNum = _StringsStatCellNumRu._(_root);
@override Map<String, String> get playerRole => {
'user': 'Пользователь',
'banned': 'Заблокированный пользователь',
'bot': 'Бот',
'sysop': 'Системный оператор',
'admin': 'Администратор',
'mod': 'Модератор',
'halfmod': 'Модератор сообщества',
};
@override late final _StringsNumOfGameActionsRu numOfGameActions = _StringsNumOfGameActionsRu._(_root);
@override late final _StringsPopupActionsRu popupActions = _StringsPopupActionsRu._(_root);
}
// Path: statCellNum
class _StringsStatCellNumRu implements _StringsStatCellNumEn {
_StringsStatCellNumRu._(this._root);
@override final _StringsRu _root; // ignore: unused_field
// Translations
@override String get xpLevel => 'Уровень\nопыта';
@override String get hoursPlayed => 'Часов\nСыграно';
@override String get onlineGames => 'Онлайн\nИгр';
@override String get gamesWon => 'Онлайн\nПобед';
@override String get friends => 'Друзей';
@override String get apm => 'Атака в\nМинуту';
@override String get vs => 'Показатель\nVersus';
@override String get lbp => 'Положение\nв рейтинге';
@override String get lbpc => 'Положение\nв рейтинге страны';
@override String get gamesPlayed => 'Игр\nСыграно';
@override String get gamesWonTL => 'Побед';
@override String get winrate => 'Процент\nпобед';
@override String get level => 'Уровень';
@override String get score => 'Счёт';
@override String get spp => 'Очков\nна Фигуру';
@override String get pieces => 'Фигур\nУстановлено';
@override String get pps => 'Фигур в\nСекунду';
@override String get finesseFaults => 'Ошибок\nТехники';
@override String get finessePercentage => '% Качества\nТехники';
@override String get keys => 'Нажатий\nКлавиш';
@override String get kpp => 'Нажатий\nна Фигуру';
@override String get kps => 'Нажатий\nв Секунду';
}
// Path: numOfGameActions
class _StringsNumOfGameActionsRu implements _StringsNumOfGameActionsEn {
_StringsNumOfGameActionsRu._(this._root);
@override final _StringsRu _root; // ignore: unused_field
// Translations
@override String get pc => 'Все чисто';
@override String get hold => 'В запас';
@override String get tspinsTotal => 'T-spins всего';
@override String get lineClears => 'Линий очищено';
}
// Path: popupActions
class _StringsPopupActionsRu implements _StringsPopupActionsEn {
_StringsPopupActionsRu._(this._root);
@override final _StringsRu _root; // ignore: unused_field
// Translations
@override String get ok => 'OK';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
extension on _StringsEn {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'locales.en': return 'English';
case 'locales.ru': return 'Russian (Русский)';
case 'tetraLeague': return 'Tetra League';
case 'tlRecords': return 'TL Records';
case 'history': return 'History';
case 'sprint': return '40 Lines';
case 'blitz': return 'Blitz';
case 'other': return 'Other';
case 'zen': return 'Zen';
case 'bio': return 'Bio';
case 'refresh': return 'Refresh';
case 'showStoredData': return 'Show stored data';
case 'statsCalc': return 'Stats Calculator';
case 'settings': return 'Settings';
case 'track': return 'Track';
case 'stopTracking': return 'Stop\ntracking';
case 'becameTracked': return 'Added to tracking list!';
case 'compare': return 'Compare';
case 'stoppedBeingTracked': return 'Removed from tracking list!';
case 'tlLeaderboard': return 'Tetra League leaderboard';
case 'noRecords': return 'No records';
case 'noRecord': return 'No record';
case 'notEnoughData': return 'Not enough data';
case 'noHistorySaved': return 'No history saved';
case 'obtainDate': return ({required Object date}) => 'Obtained ${date}';
case 'fetchDate': return ({required Object date}) => 'Fetched ${date}';
case 'exactGametime': return 'Exact gametime';
case 'bigRedBanned': return 'BANNED';
case 'bigRedBadStanding': return 'BAD STANDING';
case 'copiedToClipboard': return 'Copied to clipboard!';
case 'playerRoleAccount': return ' account ';
case 'wasFromBeginning': return 'that was from very beginning';
case 'created': return 'created';
case 'botCreatedBy': return 'by';
case 'notSupporter': return 'Not a supporter';
case 'assignedManualy': return 'That badge was assigned manualy by TETR.IO admins';
case 'supporter': return ({required Object tier}) => 'Supporter tier ${tier}';
case 'comparingWith': return ({required Object date}) => 'Comparing with data from ${date}';
case 'top': return 'Top';
case 'topRank': return 'Top Rank';
case 'decaying': return 'Decaying';
case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked';
case 'nerdStats': return 'Nerd Stats';
case 'statCellNum.xpLevel': return 'XP Level';
case 'statCellNum.hoursPlayed': return 'Hours\nPlayed';
case 'statCellNum.onlineGames': return 'Online\nGames';
case 'statCellNum.gamesWon': return 'Games\nWon';
case 'statCellNum.friends': return 'Friends';
case 'statCellNum.apm': return 'Attack\nPer Minute';
case 'statCellNum.vs': return 'Versus\nScore';
case 'statCellNum.lbp': return 'Leaderboard\nplacement';
case 'statCellNum.lbpc': return 'Country LB\nplacement';
case 'statCellNum.gamesPlayed': return 'Games\nplayed';
case 'statCellNum.gamesWonTL': return 'Games\nWon';
case 'statCellNum.winrate': return 'Winrate\nprecentage';
case 'statCellNum.level': return 'Level';
case 'statCellNum.score': return 'Score';
case 'statCellNum.spp': return 'Score\nPer Piece';
case 'statCellNum.pieces': return 'Pieces\nPlaced';
case 'statCellNum.pps': return 'Pieces\nPer Second';
case 'statCellNum.finesseFaults': return 'Finesse\nFaults';
case 'statCellNum.finessePercentage': return 'Finesse\nPercentage';
case 'statCellNum.keys': return 'Key\nPresses';
case 'statCellNum.kpp': return 'KP Per\nPiece';
case 'statCellNum.kps': return 'KP Per\nSecond';
case 'playerRole.user': return 'User';
case 'playerRole.banned': return 'Banned';
case 'playerRole.bot': return 'Bot';
case 'playerRole.sysop': return 'System operator';
case 'playerRole.admin': return 'Admin';
case 'playerRole.mod': return 'Moderator';
case 'playerRole.halfmod': return 'Community moderator';
case 'numOfGameActions.pc': return 'All Clears';
case 'numOfGameActions.hold': return 'Holds';
case 'numOfGameActions.tspinsTotal': return 'T-spins total';
case 'numOfGameActions.lineClears': return 'Line clears';
case 'popupActions.ok': return 'OK';
default: return null;
}
}
}
extension on _StringsRu {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'locales.en': return 'Английский (English)';
case 'locales.ru': return 'Русский';
case 'tetraLeague': return 'Тетра Лига';
case 'tlRecords': return 'Матчи ТЛ';
case 'history': return 'История';
case 'sprint': return '40 линий';
case 'blitz': return 'Блиц';
case 'other': return 'Другое';
case 'zen': return 'Дзен';
case 'bio': return 'Биография';
case 'refresh': return 'Обновить';
case 'showStoredData': return 'Показать сохранённые данные';
case 'statsCalc': return 'Калькулятор статистики';
case 'settings': return 'Настройки';
case 'track': return 'Отслеживать';
case 'stopTracking': return 'Перестать\nотслеживать';
case 'becameTracked': return 'Добавлен в список отслеживания!';
case 'stoppedBeingTracked': return 'Удалён из списка отслеживания!';
case 'compare': return 'Сравнить';
case 'tlLeaderboard': return 'Таблица лидеров Тетра Лиги';
case 'noRecords': return 'Нет записей';
case 'noRecord': return 'Нет рекорда';
case 'notEnoughData': return 'Недостаточно данных';
case 'noHistorySaved': return 'Нет сохранённой истории';
case 'obtainDate': return ({required Object date}) => 'Получено ${date}';
case 'fetchDate': return ({required Object date}) => 'На момент ${date}';
case 'exactGametime': return 'Время, проведённое в игре';
case 'bigRedBanned': return 'ЗАБАНЕН';
case 'bigRedBadStanding': return 'ПЛОХАЯ РЕПУТАЦИЯ';
case 'copiedToClipboard': return 'Скопировано в буфер обмена!';
case 'playerRoleAccount': return ', аккаунт которого ';
case 'wasFromBeginning': return 'существовал с самого начала';
case 'created': return 'создан';
case 'botCreatedBy': return 'игроком';
case 'notSupporter': return 'Нет саппортерки';
case 'supporter': return ({required Object tier}) => 'Саппортерка ${tier} уровня';
case 'assignedManualy': return 'Этот значок был присвоен вручную администрацией TETR.IO';
case 'comparingWith': return ({required Object date}) => 'Сравнивая с данными от ${date}';
case 'top': return 'Топ';
case 'topRank': return 'Топ Ранг';
case 'decaying': return 'Загнивает';
case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга';
case 'nerdStats': return 'Для задротов';
case 'statCellNum.xpLevel': return 'Уровень\nопыта';
case 'statCellNum.hoursPlayed': return 'Часов\nСыграно';
case 'statCellNum.onlineGames': return 'Онлайн\nИгр';
case 'statCellNum.gamesWon': return 'Онлайн\nПобед';
case 'statCellNum.friends': return 'Друзей';
case 'statCellNum.apm': return 'Атака в\nМинуту';
case 'statCellNum.vs': return 'Показатель\nVersus';
case 'statCellNum.lbp': return 'Положение\nв рейтинге';
case 'statCellNum.lbpc': return 'Положение\nв рейтинге страны';
case 'statCellNum.gamesPlayed': return 'Игр\nСыграно';
case 'statCellNum.gamesWonTL': return 'Побед';
case 'statCellNum.winrate': return 'Процент\nпобед';
case 'statCellNum.level': return 'Уровень';
case 'statCellNum.score': return 'Счёт';
case 'statCellNum.spp': return 'Очков\nна Фигуру';
case 'statCellNum.pieces': return 'Фигур\nУстановлено';
case 'statCellNum.pps': return 'Фигур в\nСекунду';
case 'statCellNum.finesseFaults': return 'Ошибок\nТехники';
case 'statCellNum.finessePercentage': return '% Качества\nТехники';
case 'statCellNum.keys': return 'Нажатий\nКлавиш';
case 'statCellNum.kpp': return 'Нажатий\nна Фигуру';
case 'statCellNum.kps': return 'Нажатий\nв Секунду';
case 'playerRole.user': return 'Пользователь';
case 'playerRole.banned': return 'Заблокированный пользователь';
case 'playerRole.bot': return 'Бот';
case 'playerRole.sysop': return 'Системный оператор';
case 'playerRole.admin': return 'Администратор';
case 'playerRole.mod': return 'Модератор';
case 'playerRole.halfmod': return 'Модератор сообщества';
case 'numOfGameActions.pc': return 'Все чисто';
case 'numOfGameActions.hold': return 'В запас';
case 'numOfGameActions.tspinsTotal': return 'T-spins всего';
case 'numOfGameActions.lineClears': return 'Линий очищено';
case 'popupActions.ok': return 'OK';
default: return null;
}
}
}

View File

@ -1,6 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tetra_stats/views/main_view.dart';
import 'package:tetra_stats/views/settings_view.dart';
import 'package:tetra_stats/views/tracked_players_view.dart';
@ -11,11 +13,26 @@ void main() {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
runApp(MaterialApp(
WidgetsFlutterBinding.ensureInitialized();
runApp(TranslationProvider(
child: MyApp(),
));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const MainView(),
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
routes: {"/settings": (context) => const SettingsView(), "/states": (context) => const TrackedPlayersView(), "/calc": (context) => const CalcView()},
theme: ThemeData(
fontFamily: 'Eurostile Round',
colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white),
scaffoldBackgroundColor: Colors.black)));
scaffoldBackgroundColor: Colors.black
)
);
}
}

View File

@ -55,6 +55,18 @@ class CompareState extends State<CompareView> {
void fetchRedSide(String user) async {
try {
if (user.startsWith("\$avg")){
try{
var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0];
redSideMode = Mode.averages;
theRedSide = [null, null, average];
return setState(() {});
}on Exception {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Falied to assign $user")));
return;
}
}
var tearDownToNumbers = numbersReg.allMatches(user);
if (tearDownToNumbers.length == 3) {
redSideMode = Mode.stats;
@ -118,6 +130,18 @@ class CompareState extends State<CompareView> {
void fetchGreenSide(String user) async {
try {
if (user.startsWith("\$avg")){
try{
var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0];
greenSideMode = Mode.averages;
theGreenSide = [null, null, average];
return setState(() {});
}on Exception {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Falied to assign $user")));
return;
}
}
var tearDownToNumbers = numbersReg.allMatches(user);
if (tearDownToNumbers.length == 3) {
greenSideMode = Mode.stats;
@ -211,7 +235,7 @@ class CompareState extends State<CompareView> {
titleGreenSide = "${theGreenSide[2].apm} APM, ${theGreenSide[2].pps} PPS, ${theGreenSide[2].vs} VS";
break;
case Mode.averages:
titleGreenSide = "average";
titleGreenSide = "Average ${theGreenSide[2].rank.toUpperCase()} rank";
break;
}
switch (redSideMode){
@ -222,7 +246,7 @@ class CompareState extends State<CompareView> {
titleRedSide = "${theRedSide[2].apm} APM, ${theRedSide[2].pps} PPS, ${theRedSide[2].vs} VS";
break;
case Mode.averages:
titleRedSide = "average";
titleRedSide = "Average ${theRedSide[2].rank.toUpperCase()} rank";
break;
}
return Scaffold(
@ -376,8 +400,8 @@ class CompareState extends State<CompareView> {
),
if (theGreenSide[2].gamesPlayed > 9 &&
theRedSide[2].gamesPlayed > 9 &&
greenSideMode == Mode.player &&
redSideMode == Mode.player)
greenSideMode != Mode.stats &&
redSideMode != Mode.stats)
CompareThingy(
label: "TR",
greenSide: theGreenSide[2].rating,
@ -385,24 +409,24 @@ class CompareState extends State<CompareView> {
fractionDigits: 2,
higherIsBetter: true,
),
if (greenSideMode == Mode.player &&
redSideMode == Mode.player)
if (greenSideMode != Mode.stats &&
redSideMode != Mode.stats)
CompareThingy(
label: "Games Played",
greenSide: theGreenSide[2].gamesPlayed,
redSide: theRedSide[2].gamesPlayed,
higherIsBetter: true,
),
if (greenSideMode == Mode.player &&
redSideMode == Mode.player)
if (greenSideMode != Mode.stats &&
redSideMode != Mode.stats)
CompareThingy(
label: "Games Won",
greenSide: theGreenSide[2].gamesWon,
redSide: theRedSide[2].gamesWon,
higherIsBetter: true,
),
if (greenSideMode == Mode.player &&
redSideMode == Mode.player)
if (greenSideMode != Mode.stats &&
redSideMode != Mode.stats)
CompareThingy(
label: "WR %",
greenSide:
@ -413,8 +437,8 @@ class CompareState extends State<CompareView> {
),
if (theGreenSide[2].gamesPlayed > 9 &&
theRedSide[2].gamesPlayed > 9 &&
greenSideMode == Mode.player &&
redSideMode == Mode.player)
greenSideMode != Mode.stats &&
redSideMode != Mode.stats)
CompareThingy(
label: "Glicko",
greenSide: theGreenSide[2].glicko!,
@ -424,8 +448,8 @@ class CompareState extends State<CompareView> {
),
if (theGreenSide[2].gamesPlayed > 9 &&
theRedSide[2].gamesPlayed > 9 &&
greenSideMode == Mode.player &&
redSideMode == Mode.player)
greenSideMode != Mode.stats &&
redSideMode != Mode.stats)
CompareThingy(
label: "RD",
greenSide: theGreenSide[2].rd!,
@ -575,8 +599,8 @@ class CompareState extends State<CompareView> {
),
if (theGreenSide[2].gamesPlayed > 9 &&
theGreenSide[2].gamesPlayed > 9 &&
greenSideMode == Mode.player &&
redSideMode == Mode.player)
greenSideMode != Mode.stats &&
redSideMode != Mode.stats)
CompareThingy(
label: "Acc. of Est.",
greenSide: theGreenSide[2].esttracc!,
@ -781,7 +805,8 @@ class CompareState extends State<CompareView> {
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),
),
if (greenSideMode == Mode.player && redSideMode == Mode.player)
if (greenSideMode != Mode.stats && redSideMode != Mode.stats &&
theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9)
CompareThingy(
label: "By Glicko",
greenSide: getWinrateByTR(
@ -803,15 +828,15 @@ class CompareState extends State<CompareView> {
label: "By Est. TR",
greenSide: getWinrateByTR(
theGreenSide[2].estTr!.estglicko,
theGreenSide[2].rd!,
theGreenSide[2].rd ?? noTrRd,
theRedSide[2].estTr!.estglicko,
theRedSide[2].rd!) *
theRedSide[2].rd ?? noTrRd) *
100,
redSide: getWinrateByTR(
theRedSide[2].estTr!.estglicko,
theRedSide[2].rd!,
theRedSide[2].rd ?? noTrRd,
theGreenSide[2].estTr!.estglicko,
theGreenSide[2].rd!) *
theGreenSide[2].rd ?? noTrRd) *
100,
fractionDigits: 2,
higherIsBetter: true,
@ -834,7 +859,7 @@ class PlayerSelector extends StatelessWidget {
final Function fetch;
final Function change;
final Function updateState;
const PlayerSelector(
PlayerSelector(
{super.key,
required this.data,
required this.mode,
@ -845,16 +870,30 @@ class PlayerSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TextEditingController playerController = TextEditingController();
String underFieldString = "";
if (!listEquals(data, [null, null, null])){
switch (mode){
case Mode.player:
playerController.text = data[0] != null ? data[0].username : "???";
playerController.text = data[0] != null ? data[0].username : "";
break;
case Mode.stats:
playerController.text = "${data[2].apm} ${data[2].pps} ${data[2].vs}";
break;
case Mode.averages:
playerController.text = "average";
playerController.text = "\$avg${data[2].rank.toUpperCase()}";
break;
}
}
if (!listEquals(data, [null, null, null])){
switch (mode){
case Mode.player:
underFieldString = data[0] != null ? data[0].toString() : "???";
break;
case Mode.stats:
underFieldString = "${data[2].apm} APM, ${data[2].pps} PPS, ${data[2].vs} VS";
break;
case Mode.averages:
underFieldString = "Average ${data[2].rank.toUpperCase()} rank";
break;
}
}
@ -867,11 +906,20 @@ class PlayerSelector extends StatelessWidget {
controller: playerController,
decoration: const InputDecoration(counter: Offstage()),
onSubmitted: (String value) {
underFieldString = "Fetching...";
fetch(value);
}),
if (data[0] != null && data[1] == null)
Text(
data[0].toString(),
if (data[0] != null && data[1] != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: DropdownButton(
items: data[1],
value: data[0],
onChanged: (value) => change(value!),
),
)
else Text(
underFieldString,
style: const TextStyle(
shadows: <Shadow>[
Shadow(
@ -887,15 +935,6 @@ class PlayerSelector extends StatelessWidget {
],
),
),
if (data[0] != null && data[1] != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: DropdownButton(
items: data[1],
value: data[0],
onChanged: (value) => change(value!),
),
)
],
);
}

View File

@ -6,6 +6,7 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView;
@ -28,7 +29,7 @@ const givenTextHeightByScreenPercentage = 0.3;
final NumberFormat timeInSec = NumberFormat("#,###.###s.");
final NumberFormat f2 = NumberFormat.decimalPatternDigits(decimalDigits: 2);
final NumberFormat f4 = NumberFormat.decimalPatternDigits(decimalDigits: 4);
final DateFormat dateFormat = DateFormat.yMMMd().add_Hms();
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale).add_Hms();
class MainView extends StatefulWidget {
final String? player;
@ -46,14 +47,6 @@ Future<void> copyToClipboard(String text) async {
class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
final bodyGlobalKey = GlobalKey();
final List<Widget> myTabs = [
const Tab(text: "Tetra League"),
const Tab(text: "TL Records"),
const Tab(text: "History"),
const Tab(text: "40 Lines"),
const Tab(text: "Blitz"),
const Tab(text: "Other"),
];
bool _searchBoolean = false;
late TabController _tabController;
late ScrollController _scrollController;
@ -182,6 +175,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
return Scaffold(
drawer: widget.player == null ? NavDrawer(changePlayer) : null,
appBar: AppBar(
@ -227,21 +221,21 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
),
PopupMenuButton(
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
const PopupMenuItem(
PopupMenuItem(
value: "refresh",
child: Text('Refresh'),
child: Text(t.refresh),
),
const PopupMenuItem(
PopupMenuItem(
value: "/states",
child: Text('Show stored data'),
child: Text(t.showStoredData),
),
const PopupMenuItem(
PopupMenuItem(
value: "/calc",
child: Text('Stats Calculator'),
child: Text(t.statsCalc),
),
const PopupMenuItem(
PopupMenuItem(
value: "/settings",
child: Text('Settings'),
child: Text(t.settings),
),
],
onSelected: (value) {
@ -258,22 +252,9 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Center(
child: Text('none case of FutureBuilder',
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 42),
textAlign: TextAlign.center));
case ConnectionState.waiting:
return const Center(
child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.active:
return const Center(
child: Text('active case of FutureBuilder',
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: 42),
textAlign: TextAlign.center));
return const Center(child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.done:
//bool bigScreen = MediaQuery.of(context).size.width > 1024;
if (snapshot.hasData) {
@ -303,7 +284,14 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
child: TabBar(
controller: _tabController,
isScrollable: true,
tabs: myTabs,
tabs: [
Tab(text: t.tetraLeague),
Tab(text: t.tlRecords),
Tab(text: t.history),
Tab(text: t.sprint),
Tab(text: t.blitz),
Tab(text: t.other),
],
),
),
];
@ -314,7 +302,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
TLThingy(
tl: snapshot.data![0].tlSeason1,
userID: snapshot.data![0].userId,
oldTl: snapshot.data![4],),
oldTl: snapshot.data![4]),
_TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]),
_History(states: snapshot.data![2], update: _justUpdate),
_RecordThingy(
@ -406,7 +394,6 @@ class _NavDrawerState extends State<NavDrawer> {
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Center(child: Text('none case of StreamBuilder'));
case ConnectionState.waiting:
case ConnectionState.active:
final allPlayers = (snapshot.data != null)
@ -436,7 +423,7 @@ class _NavDrawerState extends State<NavDrawer> {
SliverToBoxAdapter(
child: ListTile(
leading: const Icon(Icons.leaderboard),
title: const Text("Tetra League leaderboard"),
title: Text(t.tlLeaderboard),
onTap: () {
Navigator.push(
context,
@ -501,7 +488,7 @@ class _TLRecords extends StatelessWidget {
),
);},
)]
: [const Center(child: Text("No records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))],
: [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))],
);
}
}
@ -527,10 +514,10 @@ class _History extends StatelessWidget{
}
),
if(chartsData[chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[chartsIndex].value!, title: "ss", yAxisTitle: chartsShortTitles[chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),)
else const Center(child: Text("Not enough data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))
else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))
],
),
] : [const Center(child: Text("No history saved", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]);
] : [Center(child: Text(t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]);
}
}
@ -599,12 +586,12 @@ class _RecordThingy extends StatelessWidget {
children: (record != null)
? [
if (record!.stream.contains("40l"))
Text("40 Lines",
Text(t.sprint,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28))
else if (record!.stream.contains("blitz"))
Text("Blitz",
Text(t.blitz,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),
@ -629,7 +616,7 @@ class _RecordThingy extends StatelessWidget {
playerStatLabel: "Leaderboard Placement",
isScreenBig: bigScreen,
higherIsBetter: false),
Text("Obtained ${dateFormat.format(record!.timestamp!)}",
Text(t.obtainDate(date: dateFormat.format(record!.timestamp!)),
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: "Eurostile Round",
@ -647,53 +634,53 @@ class _RecordThingy extends StatelessWidget {
if (record!.stream.contains("blitz"))
StatCellNum(
playerStat: record!.endContext!.level,
playerStatLabel: "Level",
playerStatLabel: t.statCellNum.level,
isScreenBig: bigScreen,
higherIsBetter: true,),
if (record!.stream.contains("blitz"))
StatCellNum(
playerStat: record!.endContext!.spp,
playerStatLabel: "Score\nPer Piece",
playerStatLabel: t.statCellNum.spp,
fractionDigits: 2,
isScreenBig: bigScreen,
higherIsBetter: true,),
StatCellNum(
playerStat: record!.endContext!.piecesPlaced,
playerStatLabel: "Pieces\nPlaced",
playerStatLabel: t.statCellNum.pieces,
isScreenBig: bigScreen,
higherIsBetter: true,),
StatCellNum(
playerStat: record!.endContext!.pps,
playerStatLabel: "Pieces\nPer Second",
playerStatLabel: t.statCellNum.pps,
fractionDigits: 2,
isScreenBig: bigScreen,
higherIsBetter: true,),
StatCellNum(
playerStat: record!.endContext!.finesse.faults,
playerStatLabel: "Finesse\nFaults",
playerStatLabel: t.statCellNum.finesseFaults,
isScreenBig: bigScreen,
higherIsBetter: false,),
StatCellNum(
playerStat:
record!.endContext!.finessePercentage * 100,
playerStatLabel: "Finesse\nPercentage",
playerStatLabel: t.statCellNum.finessePercentage,
fractionDigits: 2,
isScreenBig: bigScreen,
higherIsBetter: true,),
StatCellNum(
playerStat: record!.endContext!.inputs,
playerStatLabel: "Key\nPresses",
playerStatLabel: t.statCellNum.keys,
isScreenBig: bigScreen,
higherIsBetter: false,),
StatCellNum(
playerStat: record!.endContext!.kpp,
playerStatLabel: "KP Per\nPiece",
playerStatLabel: t.statCellNum.kpp,
fractionDigits: 2,
isScreenBig: bigScreen,
higherIsBetter: false,),
StatCellNum(
playerStat: record!.endContext!.kps,
playerStatLabel: "KP Per\nSecond",
playerStatLabel: t.statCellNum.kps,
fractionDigits: 2,
isScreenBig: bigScreen,
higherIsBetter: true,),
@ -713,8 +700,8 @@ class _RecordThingy extends StatelessWidget {
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text("All Clears:",
style: TextStyle(fontSize: 24)),
Text("${t.numOfGameActions.pc}:",
style: const TextStyle(fontSize: 24)),
Text(
record!.endContext!.clears.allClears
.toString(),
@ -726,8 +713,8 @@ class _RecordThingy extends StatelessWidget {
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text("Holds:",
style: TextStyle(fontSize: 24)),
Text("${t.numOfGameActions.hold}:",
style: const TextStyle(fontSize: 24)),
Text(
record!.endContext!.holds.toString(),
style: const TextStyle(fontSize: 24),
@ -738,8 +725,8 @@ class _RecordThingy extends StatelessWidget {
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text("T-spins total:",
style: TextStyle(fontSize: 24)),
Text("${t.numOfGameActions.tspinsTotal}:",
style: const TextStyle(fontSize: 24)),
Text(
record!.endContext!.tSpins.toString(),
style: const TextStyle(fontSize: 24),
@ -841,8 +828,8 @@ class _RecordThingy extends StatelessWidget {
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text("Line clears:",
style: TextStyle(fontSize: 24)),
Text("${t.numOfGameActions.lineClears}:",
style: const TextStyle(fontSize: 24)),
Text(
record!.endContext!.lines.toString(),
style: const TextStyle(fontSize: 24),
@ -906,7 +893,7 @@ class _RecordThingy extends StatelessWidget {
),
]
: [
const Text("No record", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))
Text(t.noRecord, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))
],
);
});
@ -930,7 +917,7 @@ class _OtherThingy extends StatelessWidget {
itemBuilder: (BuildContext context, int index) {
return Column(
children: [
Text("Other info",
Text(t.other,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),
@ -939,17 +926,17 @@ class _OtherThingy extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(0, 48, 0, 48),
child: Column(
children: [
Text("Zen",
Text(t.zen,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),
Text(
"Level ${NumberFormat.decimalPattern().format(zen!.level)}",
"${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}",
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),
Text(
"Score ${NumberFormat.decimalPattern().format(zen!.score)}",
"${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}",
style: const TextStyle(fontSize: 18)),
],
),
@ -959,7 +946,7 @@ class _OtherThingy extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(0, 0, 0, 48),
child: Column(
children: [
Text("Bio",
Text(t.bio,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)),

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
@ -65,6 +66,12 @@ class SettingsState extends State<SettingsView> {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
List<DropdownMenuItem<AppLocale>>? locales = <DropdownMenuItem<AppLocale>>[];
for (var v in AppLocale.values){
locales.add(DropdownMenuItem<AppLocale>(
value: v, child: Text(t.locales[v.languageTag]!)));
}
return Scaffold(
appBar: AppBar(
title: const Text("Settings"),
@ -212,6 +219,14 @@ class SettingsState extends State<SettingsView> {
],
)),
),
ListTile(
title: const Text("Language"),
trailing: DropdownButton(
items: locales,
value: LocaleSettings.currentLocale,
onChanged: (value) => LocaleSettings.setLocale(value!),
),
),
const Divider(),
ListTile(
title: const Text("About app"),

View File

@ -3,6 +3,8 @@ import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/calc_view.dart';
import 'package:tetra_stats/widgets/stat_sell_num.dart';
var fDiff = NumberFormat("+#,###.###;-#,###.###");
@ -17,6 +19,8 @@ class TLThingy extends StatelessWidget {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
return LayoutBuilder(builder: (context, constraints) {
bool bigScreen = constraints.maxWidth > 768;
return ListView.builder(
@ -26,8 +30,8 @@ class TLThingy extends StatelessWidget {
return Column(
children: (tl.gamesPlayed > 0)
? [
Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (oldTl != null) Text("Comparing with data from ${DateFormat.yMMMd().add_Hms().format(oldTl!.timestamp)}"),
Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (oldTl != null) Text(t.comparingWith(date: dateFormat.format(oldTl!.timestamp))),
if (tl.gamesPlayed >= 10)
Wrap(
direction: Axis.horizontal,
@ -51,7 +55,7 @@ class TLThingy extends StatelessWidget {
),
),
Text(
"Top ${f2.format(tl.percentile * 100)}% (${tl.percentileRank.toUpperCase()}) • Top Rank: ${tl.bestRank.toUpperCase()} • Glicko: ${f2.format(tl.glicko!)}±${f2.format(tl.rd!)}${tl.decaying ? 'Decaying' : ''}",
"${t.top} ${f2.format(tl.percentile * 100)}% (${tl.percentileRank.toUpperCase()}) • ${t.topRank}: ${tl.bestRank.toUpperCase()} • Glicko: ${f2.format(tl.glicko!)}±${f2.format(tl.rd!)}${tl.decaying ? '${t.decaying}' : ''}",
textAlign: TextAlign.center,
),
],
@ -73,7 +77,7 @@ class TLThingy extends StatelessWidget {
),
),
if (tl.gamesPlayed < 10)
Text("${10 - tl.gamesPlayed} games until being ranked",
Text(t.gamesUntilRanked(left: 10 - tl.gamesPlayed),
softWrap: true,
textAlign: TextAlign.center,
style: TextStyle(
@ -90,21 +94,21 @@ class TLThingy extends StatelessWidget {
crossAxisAlignment: WrapCrossAlignment.start,
clipBehavior: Clip.hardEdge,
children: [
if (tl.apm != null) StatCellNum(playerStat: tl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Attack\nPer Minute", higherIsBetter: true, oldPlayerStat: oldTl?.apm),
if (tl.pps != null) StatCellNum(playerStat: tl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Pieces\nPer Second", higherIsBetter: true, oldPlayerStat: oldTl?.pps),
if (tl.vs != null) StatCellNum(playerStat: tl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Versus\nScore", higherIsBetter: true, oldPlayerStat: oldTl?.vs),
if (tl.standing > 0) StatCellNum(playerStat: tl.standing, isScreenBig: bigScreen, playerStatLabel: "Leaderboard\nplacement", higherIsBetter: false, oldPlayerStat: oldTl?.standing),
if (tl.standingLocal > 0) StatCellNum(playerStat: tl.standingLocal, isScreenBig: bigScreen, playerStatLabel: "Country LB\nplacement", higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal),
StatCellNum(playerStat: tl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: "Games\nplayed", higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed),
StatCellNum(playerStat: tl.gamesWon, isScreenBig: bigScreen, playerStatLabel: "Games\nwon", higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon),
StatCellNum(playerStat: tl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Winrate\nprecentage", higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null),
if (tl.apm != null) StatCellNum(playerStat: tl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm),
if (tl.pps != null) StatCellNum(playerStat: tl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps),
if (tl.vs != null) StatCellNum(playerStat: tl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs),
if (tl.standing > 0) StatCellNum(playerStat: tl.standing, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbp, higherIsBetter: false, oldPlayerStat: oldTl?.standing),
if (tl.standingLocal > 0) StatCellNum(playerStat: tl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal),
StatCellNum(playerStat: tl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed),
StatCellNum(playerStat: tl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon),
StatCellNum(playerStat: tl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null),
],
),
),
if (tl.nerdStats != null)
Column(
children: [
Text("Nerd Stats", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
Padding(
padding: const EdgeInsets.fromLTRB(0, 40, 0, 0),
child: Wrap(

View File

@ -1,23 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/compare_view.dart';
import 'package:intl/intl.dart';
import 'dart:developer' as developer;
import 'package:tetra_stats/widgets/stat_sell_num.dart';
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
}
}
Future<void> copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text));
}
final DateFormat dateFormat = DateFormat.yMMMd().add_Hms();
class UserThingy extends StatelessWidget {
final TetrioPlayer player;
final bool showStateTimestamp;
@ -26,6 +19,8 @@ class UserThingy extends StatelessWidget {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
return LayoutBuilder(builder: (context, constraints) {
bool bigScreen = constraints.maxWidth > 768;
double bannerHeight = bigScreen ? 240 : 120;
@ -99,12 +94,12 @@ class UserThingy extends StatelessWidget {
child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)),
onPressed: () {
copyToClipboard(player.userId);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Copied to clipboard!")));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard)));
}),
],
)),
showStateTimestamp
? Text("Fetched ${dateFormat.format(player.state)}")
? Text(t.fetchDate(date: dateFormat.format(player.state)))
: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.center, spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, children: [
FutureBuilder(
future: teto.isPlayerTracking(player.userId),
@ -121,10 +116,10 @@ class UserThingy extends StatelessWidget {
icon: const Icon(Icons.person_remove),
onPressed: () {
teto.deletePlayerToTrack(player.userId).then((value) => setState());
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Removed from tracking list!")));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stoppedBeingTracked)));
},
),
const Text("Stop tracking")
Text(t.stopTracking, textAlign: TextAlign.center)
],
);
} else {
@ -135,10 +130,10 @@ class UserThingy extends StatelessWidget {
onPressed: () {
teto.addPlayerToTrack(player).then((value) => setState());
teto.storeState(player);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Added to tracking list!")));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked)));
},
),
const Text("Track")
Text(t.track, textAlign: TextAlign.center)
],
);
}
@ -157,7 +152,7 @@ class UserThingy extends StatelessWidget {
);
},
),
const Text("Compare")
Text(t.compare, textAlign: TextAlign.center)
],
)
]),
@ -173,7 +168,7 @@ class UserThingy extends StatelessWidget {
children: [
StatCellNum(
playerStat: player.level,
playerStatLabel: "XP Level",
playerStatLabel: t.statCellNum.xpLevel,
isScreenBig: bigScreen,
alertWidgets: [Text("${NumberFormat.decimalPatternDigits(decimalDigits: 2).format(player.xp)} XP", style: const TextStyle(fontFamily: "Eurostile Round Extended"),), Text("Progress to next level: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"), Text("Progress from 0 XP to level 5000: ${((player.xp / 67009017.7589378) * 100).toStringAsFixed(2)} %")],
higherIsBetter: true,
@ -181,32 +176,32 @@ class UserThingy extends StatelessWidget {
if (player.gameTime >= Duration.zero)
StatCellNum(
playerStat: player.gameTime.inHours,
playerStatLabel: "Hours\nPlayed",
playerStatLabel: t.statCellNum.hoursPlayed,
isScreenBig: bigScreen,
alertWidgets: [Text("Exact gametime: ${player.gameTime.toString()}")],
alertWidgets: [Text("${t.exactGametime}: ${player.gameTime.toString()}")],
higherIsBetter: true,),
if (player.gamesPlayed >= 0)
StatCellNum(
playerStat: player.gamesPlayed,
isScreenBig: bigScreen,
playerStatLabel: "Online\nGames",
playerStatLabel: t.statCellNum.onlineGames,
higherIsBetter: true,),
if (player.gamesWon >= 0)
StatCellNum(
playerStat: player.gamesWon,
isScreenBig: bigScreen,
playerStatLabel: "Games\nWon",
playerStatLabel: t.statCellNum.gamesWon,
higherIsBetter: true,),
if (player.friendCount > 0)
StatCellNum(
playerStat: player.friendCount,
isScreenBig: bigScreen,
playerStatLabel: "Friends",
playerStatLabel: t.statCellNum.friends,
higherIsBetter: true,),
],
)
: Text(
"BANNED",
t.bigRedBanned,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
@ -217,7 +212,7 @@ class UserThingy extends StatelessWidget {
),
if (player.badstanding != null && player.badstanding!)
Text(
"BAD STANDING",
t.bigRedBadStanding,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
@ -231,7 +226,7 @@ class UserThingy extends StatelessWidget {
children: [
Expanded(
child: Text(
"${player.country != null ? "${player.country?.toUpperCase()}" : ""}${player.role.capitalize()} account ${player.registrationTime == null ? "that was from very beginning" : 'created ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " by ${player.botmaster}" : ""} ${player.supporterTier == 0 ? "Not a supporter" : "Supporter tier ${player.supporterTier}"}",
"${player.country != null ? "${player.country?.toUpperCase()}" : ""}${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " ${t.botCreatedBy} ${player.botmaster}" : ""} ${player.supporterTier == 0 ? t.notSupporter : t.supporter(tier: player.supporterTier)}",
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: "Eurostile Round",
@ -268,8 +263,8 @@ class UserThingy extends StatelessWidget {
children: [
Image.asset("res/tetrio_badges/${badge.badgeId}.png"),
Text(badge.ts != null
? "Obtained ${dateFormat.format(badge.ts!)}"
: "That badge was assigned manualy by TETR.IO admins"),
? t.obtainDate(date: dateFormat.format(badge.ts!))
: t.assignedManualy),
],
)
],
@ -277,7 +272,7 @@ class UserThingy extends StatelessWidget {
),
actions: <Widget>[
TextButton(
child: const Text('OK'),
child: Text(t.popupActions.ok),
onPressed: () {
Navigator.of(context).pop();
},

View File

@ -97,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
csv:
dependency: transitive
description:
name: csv
sha256: "016b31a51a913744a0a1655c74ff13c9379e1200e246a03d96c81c5d9ed297b5"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
cupertino_icons:
dependency: "direct main"
description:
@ -230,6 +238,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -276,10 +289,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.18.0"
js:
dependency: transitive
description:
@ -288,6 +301,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.7"
json2yaml:
dependency: transitive
description:
name: json2yaml
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
url: "https://pub.dev"
source: hosted
version: "3.0.1"
json_annotation:
dependency: transitive
description:
@ -501,6 +522,22 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
slang:
dependency: "direct main"
description:
name: slang
sha256: a90af3c2a70ae7d302f47717c0578370e5b2e6040c84280c3e11c9221c2a34ae
url: "https://pub.dev"
source: hosted
version: "3.20.0"
slang_flutter:
dependency: "direct main"
description:
name: slang_flutter
sha256: f3fb0ffabc5119dbe39fb8ef134d0415a27b1da816f32f1f55c8b67d4e2ac1af
url: "https://pub.dev"
source: hosted
version: "3.20.0"
source_span:
dependency: transitive
description:
@ -629,6 +666,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
win32:
dependency: transitive
description:

View File

@ -2,18 +2,6 @@ name: tetra_stats
description: Track your and other player stats in TETR.IO
publish_to: 'none'
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.2.0+4
environment:
@ -29,6 +17,8 @@ dependencies:
http:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.2
vector_math: any
sqflite: ^2.2.8+2
@ -39,10 +29,12 @@ dependencies:
fl_chart: ^0.62.0
package_info_plus: ^4.0.2
shared_preferences: ^2.1.1
intl: ^0.18.1
intl: ^0.18.0
syncfusion_flutter_gauges: ^22.1.34
file_selector: ^0.9.4
file_picker: ^5.3.2
slang: ^3.20.0
slang_flutter: ^3.20.0
dev_dependencies:
flutter_test:
@ -69,6 +61,15 @@ flutter_launcher_icons:
generate: true
image_path: "res/icons/app.png"
targets:
$default:
builders:
slang_build_runner:
options:
input_directory: res/i18n
output_directory: lib/i18n
flutter:
uses-material-design: true
assets:

View File

@ -0,0 +1,89 @@
{
"locales(map)": {
"en": "English",
"ru": "Russian (Русский)"
},
"tetraLeague": "Tetra League",
"tlRecords": "TL Records",
"history": "History",
"sprint": "40 Lines",
"blitz": "Blitz",
"other": "Other",
"zen": "Zen",
"bio": "Bio",
"refresh": "Refresh",
"showStoredData": "Show stored data",
"statsCalc": "Stats Calculator",
"settings": "Settings",
"track": "Track",
"stopTracking": "Stop\ntracking",
"becameTracked": "Added to tracking list!",
"compare": "Compare",
"stoppedBeingTracked": "Removed from tracking list!",
"tlLeaderboard": "Tetra League leaderboard",
"noRecords": "No records",
"noRecord": "No record",
"notEnoughData": "Not enough data",
"noHistorySaved": "No history saved",
"obtainDate": "Obtained ${date}",
"fetchDate": "Fetched ${date}",
"exactGametime": "Exact gametime",
"bigRedBanned": "BANNED",
"bigRedBadStanding": "BAD STANDING",
"copiedToClipboard": "Copied to clipboard!",
"playerRoleAccount": " account ",
"wasFromBeginning": "that was from very beginning",
"created": "created",
"botCreatedBy": "by",
"notSupporter": "Not a supporter",
"assignedManualy": "That badge was assigned manualy by TETR.IO admins",
"supporter": "Supporter tier ${tier}",
"comparingWith": "Comparing with data from ${date}",
"top": "Top",
"topRank": "Top Rank",
"decaying": "Decaying",
"gamesUntilRanked": "${left} games until being ranked",
"nerdStats": "Nerd Stats",
"statCellNum":{
"xpLevel": "XP Level",
"hoursPlayed": "Hours\nPlayed",
"onlineGames": "Online\nGames",
"gamesWon": "Games\nWon",
"friends": "Friends",
"apm": "Attack\nPer Minute",
"vs": "Versus\nScore",
"lbp": "Leaderboard\nplacement",
"lbpc": "Country LB\nplacement",
"gamesPlayed": "Games\nplayed",
"gamesWonTL": "Games\nWon",
"winrate": "Winrate\nprecentage",
"level": "Level",
"score": "Score",
"spp": "Score\nPer Piece",
"pieces": "Pieces\nPlaced",
"pps": "Pieces\nPer Second",
"finesseFaults": "Finesse\nFaults",
"finessePercentage": "Finesse\nPercentage",
"keys": "Key\nPresses",
"kpp": "KP Per\nPiece",
"kps": "KP Per\nSecond"
},
"playerRole(map)": {
"user": "User",
"banned": "Banned",
"bot": "Bot",
"sysop": "System operator",
"admin": "Admin",
"mod": "Moderator",
"halfmod": "Community moderator"
},
"numOfGameActions":{
"pc": "All Clears",
"hold": "Holds",
"tspinsTotal": "T-spins total",
"lineClears": "Line clears"
},
"popupActions":{
"ok": "OK"
}
}

View File

@ -0,0 +1,89 @@
{
"locales(map)": {
"en": "Английский (English)",
"ru": "Русский"
},
"tetraLeague": "Тетра Лига",
"tlRecords": "Матчи ТЛ",
"history": "История",
"sprint": "40 линий",
"blitz": "Блиц",
"other": "Другое",
"zen": "Дзен",
"bio": "Биография",
"refresh": "Обновить",
"showStoredData": "Показать сохранённые данные",
"statsCalc": "Калькулятор статистики",
"settings": "Настройки",
"track": "Отслеживать",
"stopTracking": "Перестать\nотслеживать",
"becameTracked": "Добавлен в список отслеживания!",
"stoppedBeingTracked": "Удалён из списка отслеживания!",
"compare": "Сравнить",
"tlLeaderboard": "Таблица лидеров Тетра Лиги",
"noRecords": "Нет записей",
"noRecord": "Нет рекорда",
"notEnoughData": "Недостаточно данных",
"noHistorySaved": "Нет сохранённой истории",
"obtainDate": "Получено ${date}",
"fetchDate": "На момент ${date}",
"exactGametime": "Время, проведённое в игре",
"bigRedBanned": "ЗАБАНЕН",
"bigRedBadStanding": "ПЛОХАЯ РЕПУТАЦИЯ",
"copiedToClipboard": "Скопировано в буфер обмена!",
"playerRoleAccount": ", аккаунт которого ",
"wasFromBeginning": "существовал с самого начала",
"created": "создан",
"botCreatedBy": "игроком",
"notSupporter": "Нет саппортерки",
"supporter": "Саппортерка ${tier} уровня",
"assignedManualy": "Этот значок был присвоен вручную администрацией TETR.IO",
"comparingWith": "Сравнивая с данными от ${date}",
"top": "Топ",
"topRank": "Топ Ранг",
"decaying": "Загнивает",
"gamesUntilRanked": "${left} матчей до получения рейтинга",
"nerdStats": "Для задротов",
"statCellNum": {
"xpLevel": "Уровень\nопыта",
"hoursPlayed": "Часов\nСыграно",
"onlineGames": "Онлайн\nИгр",
"gamesWon": "Онлайн\nПобед",
"friends": "Друзей",
"apm": "Атака в\nМинуту",
"vs": "Показатель\nVersus",
"lbp": "Положение\nв рейтинге",
"lbpc": "Положение\nв рейтинге страны",
"gamesPlayed": "Игр\nСыграно",
"gamesWonTL": "Побед",
"winrate": "Процент\nпобед",
"level": "Уровень",
"score": "Счёт",
"spp": "Очков\nна Фигуру",
"pieces": "Фигур\nУстановлено",
"pps": "Фигур в\nСекунду",
"finesseFaults": "Ошибок\nТехники",
"finessePercentage": "% Качества\nТехники",
"keys": "Нажатий\nКлавиш",
"kpp": "Нажатий\nна Фигуру",
"kps": "Нажатий\nв Секунду"
},
"playerRole(map)": {
"user": "Пользователь",
"banned": "Заблокированный пользователь",
"bot": "Бот",
"sysop": "Системный оператор",
"admin": "Администратор",
"mod": "Модератор",
"halfmod": "Модератор сообщества"
},
"numOfGameActions":{
"pc": "Все чисто",
"hold": "В запас",
"tspinsTotal": "T-spins всего",
"lineClears": "Линий очищено"
},
"popupActions":{
"ok": "OK"
}
}