News + bugfixes

This commit is contained in:
dan63047 2023-10-07 19:44:54 +03:00
parent 7ed93d3fb1
commit 7060eb6e43
9 changed files with 622 additions and 222 deletions

View File

@ -1037,6 +1037,24 @@ class Distinguishment {
} }
} }
class News {
late String id;
late String stream;
late String type;
late Map<String, dynamic> data;
late DateTime timestamp;
News({required this.type, required this.id, required this.stream, required this.data, required this.timestamp});
News.fromJson(Map<String, dynamic> json){
id = json["_id"];
stream = json["stream"];
type = json["type"];
data = json["data"];
timestamp = DateTime.parse(json['ts']);
}
}
class TetrioPlayersLeaderboard { class TetrioPlayersLeaderboard {
late String type; late String type;
late DateTime timestamp; late DateTime timestamp;

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 2 /// Locales: 2
/// Strings: 940 (470 per locale) /// Strings: 970 (485 per locale)
/// ///
/// Built on 2023-09-23 at 18:57 UTC /// Built on 2023-10-07 at 16:34 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
@ -163,6 +163,8 @@ class _StringsEn implements BaseTranslations<AppLocale, _StringsEn> {
String get distinguishment => 'Distinguishment'; String get distinguishment => 'Distinguishment';
String get zen => 'Zen'; String get zen => 'Zen';
String get bio => 'Bio'; String get bio => 'Bio';
String get news => 'News';
late final _StringsNewsPartsEn newsParts = _StringsNewsPartsEn._(_root);
String get openSearch => 'Search player'; String get openSearch => 'Search player';
String get closeSearch => 'Close search'; String get closeSearch => 'Close search';
String get refresh => 'Refresh'; String get refresh => 'Refresh';
@ -562,6 +564,28 @@ class _StringsEn implements BaseTranslations<AppLocale, _StringsEn> {
}; };
} }
// Path: newsParts
class _StringsNewsPartsEn {
_StringsNewsPartsEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get leaderboardStart => 'Got ';
String get leaderboardMiddle => 'on ';
String get personalbest => 'Got a new PB in ';
String get personalbestMiddle => 'of ';
String get badgeStart => 'Obtained a ';
String get badgeEnd => 'badge';
String get rankupStart => 'Obtained ';
String rankupMiddle({required Object r}) => '${r} rank ';
String get rankupEnd => 'in Tetra League';
String get tetoSupporter => 'TETR.IO supporter';
String get supporterStart => 'Become a ';
String get supporterGiftStart => 'Received the gift of ';
String unknownNews({required Object type}) => 'Unknown news of type ${type}';
}
// Path: statCellNum // Path: statCellNum
class _StringsStatCellNumEn { class _StringsStatCellNumEn {
_StringsStatCellNumEn._(this._root); _StringsStatCellNumEn._(this._root);
@ -571,7 +595,8 @@ class _StringsStatCellNumEn {
// Translations // Translations
String get xpLevel => 'XP Level'; String get xpLevel => 'XP Level';
String get xpProgress => 'Progress to next level'; String get xpProgress => 'Progress to next level';
String get xpFrom0To5000 => 'Progress from 0 XP to level 5000'; String xpFrom0ToLevel({required Object n}) => 'Progress from 0 XP to level ${n}';
String get xpLeft => 'XP left';
String get hoursPlayed => 'Hours\nPlayed'; String get hoursPlayed => 'Hours\nPlayed';
String get onlineGames => 'Online\nGames'; String get onlineGames => 'Online\nGames';
String get gamesWon => 'Games\nWon'; String get gamesWon => 'Games\nWon';
@ -708,6 +733,8 @@ class _StringsRu implements _StringsEn {
@override String get distinguishment => 'Заслуга'; @override String get distinguishment => 'Заслуга';
@override String get zen => 'Дзен'; @override String get zen => 'Дзен';
@override String get bio => 'Биография'; @override String get bio => 'Биография';
@override String get news => 'Новости';
@override late final _StringsNewsPartsRu newsParts = _StringsNewsPartsRu._(_root);
@override String get openSearch => 'Искать игрока'; @override String get openSearch => 'Искать игрока';
@override String get closeSearch => 'Закрыть поиск'; @override String get closeSearch => 'Закрыть поиск';
@override String get refresh => 'Обновить'; @override String get refresh => 'Обновить';
@ -1107,6 +1134,28 @@ class _StringsRu implements _StringsEn {
}; };
} }
// Path: newsParts
class _StringsNewsPartsRu implements _StringsNewsPartsEn {
_StringsNewsPartsRu._(this._root);
@override final _StringsRu _root; // ignore: unused_field
// Translations
@override String get leaderboardStart => 'Взял ';
@override String get leaderboardMiddle => 'в таблице ';
@override String get personalbest => 'Поставил новый ЛР в ';
@override String get personalbestMiddle => 'с результатом в ';
@override String get badgeStart => 'Заработал значок ';
@override String get badgeEnd => '';
@override String get rankupStart => 'Заработал ';
@override String rankupMiddle({required Object r}) => '${r} ранг ';
@override String get rankupEnd => 'в Тетра Лиге';
@override String get tetoSupporter => 'TETR.IO supporter';
@override String get supporterStart => 'Стал обладателем ';
@override String get supporterGiftStart => 'Получил подарок в виде ';
@override String unknownNews({required Object type}) => 'Неизвестная новость типа ${type}';
}
// Path: statCellNum // Path: statCellNum
class _StringsStatCellNumRu implements _StringsStatCellNumEn { class _StringsStatCellNumRu implements _StringsStatCellNumEn {
_StringsStatCellNumRu._(this._root); _StringsStatCellNumRu._(this._root);
@ -1116,7 +1165,8 @@ class _StringsStatCellNumRu implements _StringsStatCellNumEn {
// Translations // Translations
@override String get xpLevel => 'Уровень\nопыта'; @override String get xpLevel => 'Уровень\nопыта';
@override String get xpProgress => 'Прогресс до следующего уровня'; @override String get xpProgress => 'Прогресс до следующего уровня';
@override String get xpFrom0To5000 => 'Прогресс от 0 XP до 5000 уровня'; @override String xpFrom0ToLevel({required Object n}) => 'Прогресс от 0 XP до ${n} уровня';
@override String get xpLeft => 'XP осталось';
@override String get hoursPlayed => 'Часов\nСыграно'; @override String get hoursPlayed => 'Часов\nСыграно';
@override String get onlineGames => 'Онлайн\nИгр'; @override String get onlineGames => 'Онлайн\nИгр';
@override String get gamesWon => 'Онлайн\nПобед'; @override String get gamesWon => 'Онлайн\nПобед';
@ -1232,6 +1282,20 @@ extension on _StringsEn {
case 'distinguishment': return 'Distinguishment'; case 'distinguishment': return 'Distinguishment';
case 'zen': return 'Zen'; case 'zen': return 'Zen';
case 'bio': return 'Bio'; case 'bio': return 'Bio';
case 'news': return 'News';
case 'newsParts.leaderboardStart': return 'Got ';
case 'newsParts.leaderboardMiddle': return 'on ';
case 'newsParts.personalbest': return 'Got a new PB in ';
case 'newsParts.personalbestMiddle': return 'of ';
case 'newsParts.badgeStart': return 'Obtained a ';
case 'newsParts.badgeEnd': return 'badge';
case 'newsParts.rankupStart': return 'Obtained ';
case 'newsParts.rankupMiddle': return ({required Object r}) => '${r} rank ';
case 'newsParts.rankupEnd': return 'in Tetra League';
case 'newsParts.tetoSupporter': return 'TETR.IO supporter';
case 'newsParts.supporterStart': return 'Become a ';
case 'newsParts.supporterGiftStart': return 'Received the gift of ';
case 'newsParts.unknownNews': return ({required Object type}) => 'Unknown news of type ${type}';
case 'openSearch': return 'Search player'; case 'openSearch': return 'Search player';
case 'closeSearch': return 'Close search'; case 'closeSearch': return 'Close search';
case 'refresh': return 'Refresh'; case 'refresh': return 'Refresh';
@ -1356,7 +1420,8 @@ extension on _StringsEn {
case 'notForWeb': return 'Function is not available for web version'; case 'notForWeb': return 'Function is not available for web version';
case 'statCellNum.xpLevel': return 'XP Level'; case 'statCellNum.xpLevel': return 'XP Level';
case 'statCellNum.xpProgress': return 'Progress to next level'; case 'statCellNum.xpProgress': return 'Progress to next level';
case 'statCellNum.xpFrom0To5000': return 'Progress from 0 XP to level 5000'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Progress from 0 XP to level ${n}';
case 'statCellNum.xpLeft': return 'XP left';
case 'statCellNum.hoursPlayed': return 'Hours\nPlayed'; case 'statCellNum.hoursPlayed': return 'Hours\nPlayed';
case 'statCellNum.onlineGames': return 'Online\nGames'; case 'statCellNum.onlineGames': return 'Online\nGames';
case 'statCellNum.gamesWon': return 'Games\nWon'; case 'statCellNum.gamesWon': return 'Games\nWon';
@ -1712,6 +1777,20 @@ extension on _StringsRu {
case 'distinguishment': return 'Заслуга'; case 'distinguishment': return 'Заслуга';
case 'zen': return 'Дзен'; case 'zen': return 'Дзен';
case 'bio': return 'Биография'; case 'bio': return 'Биография';
case 'news': return 'Новости';
case 'newsParts.leaderboardStart': return 'Взял ';
case 'newsParts.leaderboardMiddle': return 'в таблице ';
case 'newsParts.personalbest': return 'Поставил новый ЛР в ';
case 'newsParts.personalbestMiddle': return 'с результатом в ';
case 'newsParts.badgeStart': return 'Заработал значок ';
case 'newsParts.badgeEnd': return '';
case 'newsParts.rankupStart': return 'Заработал ';
case 'newsParts.rankupMiddle': return ({required Object r}) => '${r} ранг ';
case 'newsParts.rankupEnd': return 'в Тетра Лиге';
case 'newsParts.tetoSupporter': return 'TETR.IO supporter';
case 'newsParts.supporterStart': return 'Стал обладателем ';
case 'newsParts.supporterGiftStart': return 'Получил подарок в виде ';
case 'newsParts.unknownNews': return ({required Object type}) => 'Неизвестная новость типа ${type}';
case 'openSearch': return 'Искать игрока'; case 'openSearch': return 'Искать игрока';
case 'closeSearch': return 'Закрыть поиск'; case 'closeSearch': return 'Закрыть поиск';
case 'refresh': return 'Обновить'; case 'refresh': return 'Обновить';
@ -1836,7 +1915,8 @@ extension on _StringsRu {
case 'notForWeb': return 'Функция недоступна для веб версии'; case 'notForWeb': return 'Функция недоступна для веб версии';
case 'statCellNum.xpLevel': return 'Уровень\nопыта'; case 'statCellNum.xpLevel': return 'Уровень\nопыта';
case 'statCellNum.xpProgress': return 'Прогресс до следующего уровня'; case 'statCellNum.xpProgress': return 'Прогресс до следующего уровня';
case 'statCellNum.xpFrom0To5000': return 'Прогресс от 0 XP до 5000 уровня'; case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Прогресс от 0 XP до ${n} уровня';
case 'statCellNum.xpLeft': return 'XP осталось';
case 'statCellNum.hoursPlayed': return 'Часов\nСыграно'; case 'statCellNum.hoursPlayed': return 'Часов\nСыграно';
case 'statCellNum.onlineGames': return 'Онлайн\nИгр'; case 'statCellNum.onlineGames': return 'Онлайн\nИгр';
case 'statCellNum.gamesWon': return 'Онлайн\nПобед'; case 'statCellNum.gamesWon': return 'Онлайн\nПобед';

View File

@ -53,8 +53,9 @@ class TetrioService extends DB {
final Map<String, TetrioPlayer> _playersCache = {}; final Map<String, TetrioPlayer> _playersCache = {};
final Map<String, Map<String, dynamic>> _recordsCache = {}; final Map<String, Map<String, dynamic>> _recordsCache = {};
final Map<String, TetrioPlayersLeaderboard> _leaderboardsCache = {}; final Map<String, TetrioPlayersLeaderboard> _leaderboardsCache = {};
final Map<String, List<News>> _newsCache = {};
final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer}
final client = UserAgentClient("Tetra Stats v1.2.3 (dm @dan63047 if someone abuse that software)", http.Client()); final client = UserAgentClient("ebany u rot yatogo kazino blyat' (Tetra Stats v1.2.4 dev build)", http.Client());
static final TetrioService _shared = TetrioService._sharedInstance(); static final TetrioService _shared = TetrioService._sharedInstance();
factory TetrioService() => _shared; factory TetrioService() => _shared;
late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController; late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
@ -237,6 +238,63 @@ class TetrioService extends DB {
} }
} }
// TODO: Future<List<News>> getNews(String userID) async
Future<List<News>> fetchNews(String userID) async{
try{
var cached = _newsCache.entries.firstWhere((element) => element.value[0].stream == "user_$userID");
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("fetchNews: News for $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{
_newsCache.remove(cached.key);
developer.log("fetchNews: Cached news for $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
developer.log("fetchNews: Trying to retrieve news for $userID", name: "services/tetrio_crud");
}
Uri url;
if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioNews", "user": userID.toLowerCase().trim(), "limit": "100"});
} else {
url = Uri.https('ch.tetr.io', 'api/news/user_${userID.toLowerCase().trim()}', {"limit": "100"});
}
try {
final response = await client.get(url);
switch (response.statusCode) {
case 200:
var payload = jsonDecode(response.body);
if (payload['success']) {
List<News> news = [for (var entry in payload['data']['news']) News.fromJson(entry)];
developer.log("fetchNews: $userID news retrieved and cached", name: "services/tetrio_crud");
_newsCache[payload['cache']['cached_until'].toString()] = news;
return news;
} else {
developer.log("fetchNews: User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist();
}
case 403:
throw TetrioForbidden();
case 429:
throw TetrioTooManyRequests();
case 418:
throw TetrioOskwareBridgeProblem();
case 500:
case 502:
case 503:
case 504:
throw TetrioInternalProblem();
default:
developer.log("fetchNews: Failed to fetch stream", 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);
}
}
Future<TetraLeagueAlphaStream> getTLStream(String userID) async { Future<TetraLeagueAlphaStream> getTLStream(String userID) async {
try{ try{
var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID); var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID);

View File

@ -50,6 +50,14 @@ Future<void> copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
} }
String get40lTime(int microseconds){
if (microseconds > 60000000) {
return "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}";
} else{
return timeInSec.format(microseconds / 1000000);
}
}
class _MainState extends State<MainView> with SingleTickerProviderStateMixin { class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
final bodyGlobalKey = GlobalKey(); final bodyGlobalKey = GlobalKey();
bool _searchBoolean = false; bool _searchBoolean = false;
@ -128,7 +136,10 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
} }
_searchFor = me.userId; _searchFor = me.userId;
setState((){_titleNickname = me.username;}); setState((){_titleNickname = me.username;});
var tlStream = await teto.getTLStream(me.userId); List<dynamic> requests = await Future.wait([teto.getTLStream(_searchFor), teto.fetchRecords(_searchFor), teto.fetchNews(_searchFor)]);
TetraLeagueAlphaStream tlStream = requests[0] as TetraLeagueAlphaStream;
Map<String, dynamic> records = requests[1] as Map<String, dynamic>;
List<News> news = requests[2] as List<News>;
List<TetraLeagueAlphaRecord> tlMatches = []; List<TetraLeagueAlphaRecord> tlMatches = [];
bool isTracking = await teto.isPlayerTracking(me.userId); bool isTracking = await teto.isPlayerTracking(me.userId);
List<TetrioPlayer> states = []; List<TetrioPlayer> states = [];
@ -136,7 +147,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
var uniqueTL = <dynamic>{}; var uniqueTL = <dynamic>{};
if (isTracking){ if (isTracking){
await teto.storeState(me); await teto.storeState(me);
await teto.saveTLMatchesFromStream(await teto.getTLStream(me.userId)); await teto.saveTLMatchesFromStream(tlStream);
tlMatches.addAll(await teto.getTLMatchesbyPlayerID(me.userId)); tlMatches.addAll(await teto.getTLMatchesbyPlayerID(me.userId));
for (var match in tlStream.records) { for (var match in tlStream.records) {
if (!tlMatches.contains(match)) tlMatches.add(match); if (!tlMatches.contains(match)) tlMatches.add(match);
@ -180,8 +191,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))),
DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))),
]; ];
Map<String, dynamic> records = await teto.fetchRecords(me.userId); return [me, records, states, tlMatches, compareWith, isTracking, news];
return [me, records, states, tlMatches, compareWith, isTracking];
} }
void _justUpdate() { void _justUpdate() {
@ -335,7 +345,9 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
? snapshot.data![1]['blitz'][0] ? snapshot.data![1]['blitz'][0]
: null), : null),
_OtherThingy( _OtherThingy(
zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment,) zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio,
distinguishment: snapshot.data![0].distinguishment,
newsletter: snapshot.data![6],)
], ],
), ),
), ),
@ -454,7 +466,7 @@ class _NavDrawerState extends State<NavDrawer> {
final allPlayers = (snapshot.data != null) final allPlayers = (snapshot.data != null)
? snapshot.data as Map<String, List<TetrioPlayer>> ? snapshot.data as Map<String, List<TetrioPlayer>>
: <String, List<TetrioPlayer>>{}; : <String, List<TetrioPlayer>>{};
List<String> keys = allPlayers.keys.toList(); List<String> keys = allPlayers.keys.toList().reversed.toList(); // this is so dumb
return NestedScrollView( return NestedScrollView(
headerSliverBuilder: (context, value) { headerSliverBuilder: (context, value) {
return [ return [
@ -669,15 +681,7 @@ class _RecordThingy extends StatelessWidget {
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)), fontSize: bigScreen ? 42 : 28)),
if (record!.stream.contains("40l")) if (record!.stream.contains("40l"))
if (record!.endContext!.finalTime.inMicroseconds > 60000000) Text( Text(get40lTime(record!.endContext!.finalTime.inMicroseconds),
"${(record!.endContext!.finalTime.inMicroseconds/1000000/60).floor()}:${(secs.format(record!.endContext!.finalTime.inMicroseconds /1000000 % 60))}",
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28))
else Text(
timeInSec.format(
record!.endContext!.finalTime.inMicroseconds /
1000000),
style: TextStyle( style: TextStyle(
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28)) fontSize: bigScreen ? 42 : 28))
@ -897,7 +901,8 @@ class _OtherThingy extends StatelessWidget {
final TetrioZen? zen; final TetrioZen? zen;
final String? bio; final String? bio;
final Distinguishment? distinguishment; final Distinguishment? distinguishment;
const _OtherThingy({Key? key, required this.zen, required this.bio, required this.distinguishment}) final List<News>? newsletter;
const _OtherThingy({Key? key, required this.zen, required this.bio, required this.distinguishment, this.newsletter})
: super(key: key); : super(key: key);
List<InlineSpan> getDistinguishmentSetOfWidgets(String text) { List<InlineSpan> getDistinguishmentSetOfWidgets(String text) {
@ -924,15 +929,115 @@ class _OtherThingy extends StatelessWidget {
return result; return result;
} }
ListTile getNewsTile(News news){
Map<String, String> gametypes = {
"40l": t.sprint,
"blitz": t.blitz,
"5mblast": "5,000,000 Blast"
};
switch (news.type) {
case "leaderboard":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16),
text: t.newsParts.leaderboardStart,
children: [
TextSpan(text: "${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.leaderboardMiddle),
TextSpan(text: "${gametypes[news.data["gametype"]]}", style: const TextStyle(fontWeight: FontWeight.bold)),
]
)
),
subtitle: Text(dateFormat.format(news.timestamp)),
);
case "personalbest":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16),
text: t.newsParts.personalbest,
children: [
TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.personalbestMiddle),
TextSpan(text: news.data["gametype"] == "blitz" ? NumberFormat.decimalPattern().format(news.data["result"]) : get40lTime((news.data["result"]*1000).floor()), style: const TextStyle(fontWeight: FontWeight.bold)),
]
)
),
subtitle: Text(dateFormat.format(news.timestamp)),
);
case "badge":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16),
text: t.newsParts.badgeStart,
children: [
TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.badgeEnd)
]
)
),
subtitle: Text(dateFormat.format(news.timestamp)),
);
case "rankup":
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16),
text: t.newsParts.rankupStart,
children: [
TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: t.newsParts.rankupEnd)
]
)
),
subtitle: Text(dateFormat.format(news.timestamp)),
);
case "supporter":
return ListTile(
title: RichText(
text: TextSpan(
style: TextStyle(fontFamily: 'Eurostile Round', fontSize: 16),
text: t.newsParts.supporterStart,
children: [
TextSpan(text: t.newsParts.tetoSupporter, style: TextStyle(fontWeight: FontWeight.bold))
]
)
),
subtitle: Text(dateFormat.format(news.timestamp)),
);
case "supporter_gift":
return ListTile(
title: RichText(
text: TextSpan(
style: TextStyle(fontFamily: 'Eurostile Round', fontSize: 16),
text: t.newsParts.supporterGiftStart,
children: [
TextSpan(text: t.newsParts.tetoSupporter, style: TextStyle(fontWeight: FontWeight.bold))
]
)
),
subtitle: Text(dateFormat.format(news.timestamp)),
);
default:
return ListTile(
title: Text(t.newsParts.unknownNews(type: news.type)),
subtitle: Text(dateFormat.format(news.timestamp)),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
bool bigScreen = constraints.maxWidth > 768; bool bigScreen = constraints.maxWidth > 768;
return ListView.builder( return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: 1, itemCount: newsletter!.length+1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return Column( return index == 0 ? Column(
children: [ children: [
if (distinguishment != null) if (distinguishment != null)
Padding( Padding(
@ -962,7 +1067,7 @@ class _OtherThingy extends StatelessWidget {
), ),
if (zen != null) if (zen != null)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), padding: const EdgeInsets.fromLTRB(0, 0, 0, 48),
child: Column( child: Column(
children: [ children: [
Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
@ -971,9 +1076,10 @@ class _OtherThingy extends StatelessWidget {
], ],
), ),
), ),
if (newsletter != null && newsletter!.isNotEmpty)
Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
], ],
); ) : getNewsTile(newsletter![index-1]);
}, },
); );
}); });

View File

@ -377,7 +377,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
_ListEntry(value: widget.rank[0].pps, label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), _ListEntry(value: widget.rank[0].pps, label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
_ListEntry(value: widget.rank[0].vs, label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), _ListEntry(value: widget.rank[0].vs, label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
_ListEntry(value: widget.rank[1]["avgAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["avgAPP"], label: "VS / APM", id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgVSAPM"], label: "VS / APM", id: "", username: "", approximate: true, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["avgDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["avgDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
_ListEntry(value: widget.rank[1]["avgAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), _ListEntry(value: widget.rank[1]["avgAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),

View File

@ -29,7 +29,7 @@ class StatCellNum extends StatelessWidget {
return Column( return Column(
children: [ children: [
Text( Text(
f.format(playerStat), fractionDigits == null ? f.format(playerStat.floor()) : f.format(playerStat),
style: TextStyle( style: TextStyle(
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
fontSize: isScreenBig ? 32 : 24, fontSize: isScreenBig ? 32 : 24,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/compare_view.dart'; import 'package:tetra_stats/views/compare_view.dart';
@ -7,6 +8,17 @@ import 'package:intl/intl.dart';
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart';
const Map<int, double> xpTableScuffed = { // level: xp required
05000: 67009018.4885772,
10000: 763653437.386,
15000: 2337651144.54149,
20000: 4572735210.50902,
25000: 7376166347.04745,
30000: 10693620096.2168,
40000: 18728882739.482,
50000: 28468683855.2853
};
Future<void> copyToClipboard(String text) async { Future<void> copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
} }
@ -15,6 +27,7 @@ class UserThingy extends StatelessWidget {
final TetrioPlayer player; final TetrioPlayer player;
final bool showStateTimestamp; final bool showStateTimestamp;
final Function setState; final Function setState;
const UserThingy({Key? key, required this.player, required this.showStateTimestamp, required this.setState}) : super(key: key); const UserThingy({Key? key, required this.player, required this.showStateTimestamp, required this.setState}) : super(key: key);
@override @override
@ -25,212 +38,302 @@ class UserThingy extends StatelessWidget {
bool bigScreen = constraints.maxWidth > 768; bool bigScreen = constraints.maxWidth > 768;
double bannerHeight = bigScreen ? 240 : 120; double bannerHeight = bigScreen ? 240 : 120;
double pfpHeight = 128; double pfpHeight = 128;
int xpTableID = 0;
while (player.xp > xpTableScuffed.values.toList()[xpTableID]) {
xpTableID++;
}
return Column( return Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Flex( Stack(
direction: Axis.vertical, alignment: Alignment.topCenter,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Stack( if (player.bannerRevision != null)
alignment: Alignment.topCenter, Image.network("https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}",
children: [ fit: BoxFit.cover,
if (player.bannerRevision != null) height: bannerHeight,
Image.network( errorBuilder: (context, error, stackTrace) {
"https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", developer.log("Error with building banner image", name: "main_view", error: error, stackTrace: stackTrace);
fit: BoxFit.cover, return Container();
height: bannerHeight, },
errorBuilder: (context, error, stackTrace) {
developer.log("Error with building banner image", name: "main_view", error: error, stackTrace: stackTrace);
return Container();
},
),
Container(
padding: EdgeInsets.fromLTRB(0, player.bannerRevision != null ? bannerHeight / 1.4 : pfpHeight, 0, 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: player.role == "banned"
? Image.asset(
"res/avatars/tetrio_banned.png",
fit: BoxFit.fitHeight,
height: pfpHeight,
)
: player.avatarRevision != null
? Image.network("https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}",
fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) {
developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace);
return Image.asset(
"res/avatars/tetrio_anon.png",
fit: BoxFit.fitHeight,
height: pfpHeight,
);
})
: Image.asset(
"res/avatars/tetrio_anon.png",
fit: BoxFit.fitHeight,
height: pfpHeight,
),
),
), ),
if (player.verified) Padding(
Padding( padding: EdgeInsets.fromLTRB(8, player.bannerRevision != null ? bannerHeight / 1.4 : 0, 8, bigScreen ? 16 : 0),
padding: EdgeInsets.fromLTRB( child: Row(
pfpHeight - 22, mainAxisAlignment: MainAxisAlignment.center,
bigScreen // verified icon top padding: children: [
? (player.bannerRevision != null ? bannerHeight + pfpHeight - 96 : pfpHeight + pfpHeight - 32) // for big screen Column(
: (player.bannerRevision != null ? bannerHeight + pfpHeight - 58 : pfpHeight + pfpHeight - 32), // for small screen children: [
0, Wrap(
0), direction: bigScreen ? Axis.horizontal : Axis.vertical,
child: const Icon(Icons.verified), alignment: WrapAlignment.spaceBetween,
) spacing: bigScreen ? 25 : 0,
], //mainAxisAlignment: MainAxisAlignment.spaceBetween,
), crossAxisAlignment: WrapCrossAlignment.center,
Flexible( clipBehavior: Clip.hardEdge,
child: Column( children: [
children: [ Wrap(
Text(player.username, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), direction: bigScreen ? Axis.horizontal : Axis.vertical,
TextButton( alignment: WrapAlignment.start,
child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)), crossAxisAlignment: WrapCrossAlignment.center,
onPressed: () { spacing: bigScreen ? 20 : 0,
copyToClipboard(player.userId); clipBehavior: Clip.hardEdge,
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); children: [
}), Stack(
], alignment: Alignment.topCenter,
)), children: [
showStateTimestamp ClipRRect(
? Text(t.fetchDate(date: dateFormat.format(player.state))) borderRadius: BorderRadius.circular(1000),
: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.center, spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, children: [ child: player.role == "banned"
FutureBuilder( ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,)
future: teto.isPlayerTracking(player.userId), : player.avatarRevision != null
builder: (context, snapshot) { ? Image.network("https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}",
switch (snapshot.connectionState) { fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) {
case ConnectionState.none: developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace);
case ConnectionState.waiting: return Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight);
case ConnectionState.active: })
case ConnectionState.done: : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight),
if (snapshot.data != null && snapshot.data!) {
return Column(
children: [
IconButton(
icon: const Icon(Icons.person_remove),
onPressed: () {
teto.deletePlayerToTrack(player.userId).then((value) => setState());
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stoppedBeingTracked)));
},
), ),
Text(t.stopTracking, textAlign: TextAlign.center) if (player.verified)
], Padding(
); padding: EdgeInsets.fromLTRB(pfpHeight - 22, pfpHeight - 32, 0, 0),
} else { child: const Icon(Icons.verified),
return Column( )
children: [ ],
IconButton(
icon: const Icon(Icons.person_add),
onPressed: () {
teto.addPlayerToTrack(player).then((value) => setState());
teto.storeState(player);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked)));
},
),
Text(t.track, textAlign: TextAlign.center)
],
);
}
}
}),
Column(
children: [
IconButton(
icon: const Icon(Icons.balance),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompareView(greenSide: [player, null, player.tlSeason1], redSide: const [null, null, null], greenMode: Mode.player, redMode: Mode.player),
), ),
); Column(
}, children: [
), Text(player.username,
Text(t.compare, textAlign: TextAlign.center) style: TextStyle(
], fontFamily: "Eurostile Round Extended",
) fontSize: bigScreen ? 42 : 28,
]), shadows: const <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,
),
],
)),
TextButton(
child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)),
onPressed: () {
copyToClipboard(player.userId);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard)));
}),
],
),
],
),
showStateTimestamp
? 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),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data != null && snapshot.data!) {
return Column(
children: [
IconButton(
icon: const Icon(
Icons.person_remove,
shadows: <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,
),
],),
onPressed: () {
teto.deletePlayerToTrack(player.userId).then((value) => setState());
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stoppedBeingTracked)));
},
),
Text(t.stopTracking, textAlign: TextAlign.center)
],
);
} else {
return Column(
children: [
IconButton(
icon: const Icon(
Icons.person_add,
shadows: <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,
),
],),
onPressed: () {
teto.addPlayerToTrack(player).then((value) => setState());
teto.storeState(player);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked)));
},
),
Text(t.track, textAlign: TextAlign.center)
],
);
}
}
}),
Column(
children: [
IconButton(
icon: const Icon(
Icons.balance,
shadows: <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,
),
],),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompareView(greenSide: [player, null, player.tlSeason1], redSide: const [null, null, null], greenMode: Mode.player, redMode: Mode.player),
),
);
},
),
Text(t.compare, textAlign: TextAlign.center)
],
)
])
]),
],
),
],
),
),
], ],
), ),
if (!["banned", "p1nkl0bst3r"].contains(player.role)) if (!["banned", "p1nkl0bst3r"].contains(player.role))
Wrap( Wrap(
direction: Axis.horizontal, // mainAxisSize: MainAxisSize.min,
alignment: WrapAlignment.center, direction: Axis.horizontal,
spacing: 25, alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.start, spacing: 25,
clipBehavior: Clip.hardEdge, // hard WHAT??? crossAxisAlignment: WrapCrossAlignment.start,
children: [ clipBehavior: Clip.hardEdge, // hard WHAT???
StatCellNum( children: [
playerStat: player.level, StatCellNum(
playerStatLabel: t.statCellNum.xpLevel, playerStat: player.level,
isScreenBig: bigScreen, playerStatLabel: t.statCellNum.xpLevel,
alertWidgets: [Text("${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(player.xp)} XP", style: const TextStyle(fontFamily: "Eurostile Round Extended"),), Text("${t.statCellNum.xpProgress}: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"), Text("${t.statCellNum.xpFrom0To5000}: ${((player.xp / 67009017.7589378) * 100).toStringAsFixed(2)} %")], isScreenBig: bigScreen,
okText: t.popupActions.ok, alertWidgets: [
higherIsBetter: true, Text(
"${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(player.xp)} XP",
style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold)
),
Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 8),
child: SfLinearGauge(
minimum: 0,
maximum: 1,
interval: 1,
ranges: [
LinearGaugeRange(startValue: 0, endValue: player.level - player.level.floor(), color: Colors.cyanAccent),
LinearGaugeRange(startValue: 0, endValue: (player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross)
],
// markerPointers: [LinearShapePointer(value: player.level - player.level.floor(), position: LinearElementPosition.inside, shapeType: LinearShapePointerType.triangle, color: Colors.white, height: 20)],
showTicks: true,
showLabels: false
),
), ),
if (player.gameTime >= Duration.zero) Text("${t.statCellNum.xpProgress}: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"),
StatCellNum( Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - player.xp)} ${t.statCellNum.xpLeft})")],
playerStat: player.gameTime.inHours, okText: t.popupActions.ok,
playerStatLabel: t.statCellNum.hoursPlayed, higherIsBetter: true,
isScreenBig: bigScreen,
alertWidgets: [Text("${t.exactGametime}: ${player.gameTime.toString()}")],
higherIsBetter: true,),
if (player.gamesPlayed >= 0)
StatCellNum(
playerStat: player.gamesPlayed,
isScreenBig: bigScreen,
playerStatLabel: t.statCellNum.onlineGames,
higherIsBetter: true,),
if (player.gamesWon >= 0)
StatCellNum(
playerStat: player.gamesWon,
isScreenBig: bigScreen,
playerStatLabel: t.statCellNum.gamesWon,
higherIsBetter: true,),
if (player.friendCount > 0)
StatCellNum(
playerStat: player.friendCount,
isScreenBig: bigScreen,
playerStatLabel: t.statCellNum.friends,
higherIsBetter: true,),
],
), ),
if (player.role == "banned") Text( if (player.gameTime >= Duration.zero)
t.bigRedBanned, StatCellNum(
textAlign: TextAlign.center, playerStat: player.gameTime.inHours,
style: TextStyle( playerStatLabel: t.statCellNum.hoursPlayed,
fontFamily: "Eurostile Round Extended", isScreenBig: bigScreen,
fontWeight: FontWeight.w900, alertWidgets: [Text("${t.exactGametime}: ${player.gameTime.toString()}")],
color: Colors.red, higherIsBetter: true,),
fontSize: bigScreen ? 60 : 45, if (player.gamesPlayed >= 0)
), StatCellNum(
), playerStat: player.gamesPlayed,
if (player.role == "p1nkl0bst3r") Text( isScreenBig: bigScreen,
t.p1nkl0bst3rAlert, playerStatLabel: t.statCellNum.onlineGames,
higherIsBetter: true,),
if (player.gamesWon >= 0)
StatCellNum(
playerStat: player.gamesWon,
isScreenBig: bigScreen,
playerStatLabel: t.statCellNum.gamesWon,
higherIsBetter: true,),
if (player.friendCount > 0)
StatCellNum(
playerStat: player.friendCount,
isScreenBig: bigScreen,
playerStatLabel: t.statCellNum.friends,
higherIsBetter: true,),
],
),
if (player.role == "banned") Text(
t.bigRedBanned,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: TextStyle(
fontFamily: "Eurostile Round", fontFamily: "Eurostile Round Extended",
fontSize: 16, fontWeight: FontWeight.w900,
) color: Colors.red,
fontSize: bigScreen ? 60 : 45,
),
), ),
if (player.badstanding != null && player.badstanding!) if (player.role == "p1nkl0bst3r") Text(
Text( t.p1nkl0bst3rAlert,
t.bigRedBadStanding,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round",
fontWeight: FontWeight.w900, fontSize: 16,
color: Colors.red, )
fontSize: bigScreen ? 60 : 45,
),
), ),
if (player.role != "p1nkl0bst3r") Row( if (player.badstanding != null && player.badstanding!)
Text(
t.bigRedBadStanding,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: "Eurostile Round Extended",
fontWeight: FontWeight.w900,
color: Colors.red,
fontSize: bigScreen ? 60 : 45,
),
),
if (player.role != "p1nkl0bst3r") Padding(
padding: EdgeInsets.only(top: bigScreen ? 8 : 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
@ -244,6 +347,7 @@ class UserThingy extends StatelessWidget {
) )
], ],
), ),
),
Wrap( Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,

View File

@ -12,6 +12,22 @@
"distinguishment": "Distinguishment", "distinguishment": "Distinguishment",
"zen": "Zen", "zen": "Zen",
"bio": "Bio", "bio": "Bio",
"news": "News",
"newsParts":{
"leaderboardStart": "Got ",
"leaderboardMiddle": "on ",
"personalbest": "Got a new PB in ",
"personalbestMiddle": "of ",
"badgeStart": "Obtained a ",
"badgeEnd": "badge",
"rankupStart": "Obtained ",
"rankupMiddle": "${r} rank ",
"rankupEnd": "in Tetra League",
"tetoSupporter": "TETR.IO supporter",
"supporterStart": "Become a ",
"supporterGiftStart": "Received the gift of ",
"unknownNews": "Unknown news of type ${type}"
},
"openSearch": "Search player", "openSearch": "Search player",
"closeSearch": "Close search", "closeSearch": "Close search",
"refresh": "Refresh", "refresh": "Refresh",
@ -137,7 +153,8 @@
"statCellNum":{ "statCellNum":{
"xpLevel": "XP Level", "xpLevel": "XP Level",
"xpProgress": "Progress to next level", "xpProgress": "Progress to next level",
"xpFrom0To5000": "Progress from 0 XP to level 5000", "xpFrom0ToLevel": "Progress from 0 XP to level $n",
"xpLeft": "XP left",
"hoursPlayed": "Hours\nPlayed", "hoursPlayed": "Hours\nPlayed",
"onlineGames": "Online\nGames", "onlineGames": "Online\nGames",
"gamesWon": "Games\nWon", "gamesWon": "Games\nWon",

View File

@ -12,6 +12,22 @@
"distinguishment": "Заслуга", "distinguishment": "Заслуга",
"zen": "Дзен", "zen": "Дзен",
"bio": "Биография", "bio": "Биография",
"news": "Новости",
"newsParts":{
"leaderboardStart": "Взял ",
"leaderboardMiddle": "в таблице ",
"personalbest": "Поставил новый ЛР в ",
"personalbestMiddle": "с результатом в ",
"badgeStart": "Заработал значок ",
"badgeEnd": "",
"rankupStart": "Заработал ",
"rankupMiddle": "${r} ранг ",
"rankupEnd": "в Тетра Лиге",
"tetoSupporter": "TETR.IO supporter",
"supporterStart": "Стал обладателем ",
"supporterGiftStart": "Получил подарок в виде ",
"unknownNews": "Неизвестная новость типа ${type}"
},
"openSearch": "Искать игрока", "openSearch": "Искать игрока",
"closeSearch": "Закрыть поиск", "closeSearch": "Закрыть поиск",
"refresh": "Обновить", "refresh": "Обновить",
@ -137,7 +153,8 @@
"statCellNum": { "statCellNum": {
"xpLevel": "Уровень\nопыта", "xpLevel": "Уровень\nопыта",
"xpProgress": "Прогресс до следующего уровня", "xpProgress": "Прогресс до следующего уровня",
"xpFrom0To5000": "Прогресс от 0 XP до 5000 уровня", "xpFrom0ToLevel": "Прогресс от 0 XP до $n уровня",
"xpLeft": "XP осталось",
"hoursPlayed": "Часов\nСыграно", "hoursPlayed": "Часов\nСыграно",
"onlineGames": "Онлайн\nИгр", "onlineGames": "Онлайн\nИгр",
"gamesWon": "Онлайн\nПобед", "gamesWon": "Онлайн\nПобед",