Compare commits

...

17 Commits

Author SHA1 Message Date
dan63047 5652efa02f
Merge pull request #94 from dan63047/master
Merge master to stable
2024-06-17 18:31:11 +03:00
dan63047 712a52ad7b I lied i forgor to fix things 2024-06-17 00:04:07 +03:00
dan63047 249c2c4a68 1.6.0 ready 2024-06-16 20:13:26 +03:00
dan63047 0262c8dcf9 stat_sell_num fix + tl_rating_thingy update 2024-06-16 16:25:16 +03:00
dan63047 10da3b5b6a Accent color + Links for singleplayer records 2024-06-16 01:49:57 +03:00
dan63047 0f90358160 details for singleplayer records
also very small refactoring (I will redo Tetra Stats from the ground one day...)
2024-06-14 23:47:36 +03:00
dan63047 7bd0d6ecd4 Top 10 records for singleplayer modes 2024-06-14 00:12:48 +03:00
dan63047 02f0cf660f Recent singleplayer games (wide viewports for now) 2024-06-13 01:32:45 +03:00
dan63047 9394b818cc I'm stupid 2024-06-11 23:51:57 +03:00
dan63047 751e7a7071 ustomization -> Relative timestamps 2024-06-11 19:30:13 +03:00
dan63047 cedb304c1a Customization -> Main representation of rating 2024-06-11 01:35:07 +03:00
dan63047 c7475e8d5c Toggle for sheetbot-like behavior of radar graphs
Man this is so jank
2024-06-10 14:55:32 +03:00
dan63047 6e9ecbf48e Preloader for web 2024-06-10 01:10:18 +03:00
dan63047 8b5bf8c4d2 Tracked players now updates in the background (off by default) 2024-06-05 01:14:34 +03:00
dan63047 68784834fe main_view now fetches data in the background + clearing cache in background 2024-06-04 02:42:44 +03:00
dan63047 3b0eb4009d New cache management system
I do hope, that code looks more clean now
2024-06-04 00:06:00 +03:00
dan63047 523038b9c8 I'm stupid 2024-05-30 01:29:37 +03:00
38 changed files with 1728 additions and 611 deletions

View File

@ -0,0 +1,15 @@
// p1nkl0bst3r data objects
class Cutoffs{
Map<String, double> tr;
Map<String, double> glicko;
Cutoffs(this.tr, this.glicko);
}
class TopTr{
String id;
double? tr;
TopTr(this.id, this.tr);
}

View File

@ -265,6 +265,7 @@ class TetrioPlayer {
List<RecordSingle?> blitz = [];
TetrioZen? zen;
Distinguishment? distinguishment;
DateTime? cachedUntil;
TetrioPlayer({
required this.userId,
@ -292,11 +293,12 @@ class TetrioPlayer {
required this.blitz,
this.zen,
this.distinguishment,
this.cachedUntil
});
double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1;
TetrioPlayer.fromJson(Map<String, dynamic> json, DateTime stateTime, String id, String nick) {
TetrioPlayer.fromJson(Map<String, dynamic> json, DateTime stateTime, String id, String nick, [DateTime? cUntil]) {
//developer.log("TetrioPlayer.fromJson $stateTime: $json", name: "data_objects/tetrio");
userId = id;
username = nick;
@ -324,6 +326,7 @@ class TetrioPlayer {
friendCount = json['friend_count'] ?? 0;
badstanding = json['badstanding'];
botmaster = json['botmaster'];
cachedUntil = cUntil;
}
Map<String, dynamic> toJson() {
@ -869,6 +872,21 @@ class TetraLeagueAlphaStream{
}
}
class SingleplayerStream{
late String userId;
late String type;
late List<RecordSingle> records;
SingleplayerStream({required this.userId, required this.records, required this.type});
SingleplayerStream.fromJson(List<dynamic> json, String userID, String tp) {
userId = userID;
type = tp;
records = [];
for (var value in json) {records.add(RecordSingle.fromJson(value, null));}
}
}
class TetraLeagueAlphaRecord{
late String replayId;
late String ownId;
@ -1113,19 +1131,17 @@ class RecordSingle {
late String userId;
late String replayId;
late String ownId;
late String stream;
DateTime? timestamp;
EndContextSingle? endContext;
late DateTime timestamp;
late EndContextSingle endContext;
int? rank;
RecordSingle({required this.userId, required this.replayId, required this.ownId, this.timestamp, this.endContext, this.rank});
RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.endContext, this.rank});
RecordSingle.fromJson(Map<String, dynamic> json, int? ran) {
//developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio");
ownId = json['_id'];
endContext = json['endcontext'] != null ? EndContextSingle.fromJson(json['endcontext']) : null;
endContext = EndContextSingle.fromJson(json['endcontext']);
replayId = json['replayid'];
stream = json['stream'];
timestamp = DateTime.parse(json['ts']);
userId = json['user']['_id'];
rank = ran;
@ -1134,9 +1150,7 @@ class RecordSingle {
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['_id'] = ownId;
if (endContext != null) {
data['endcontext'] = endContext!.toJson();
}
data['endcontext'] = endContext.toJson();
data['ismulti'] = false;
data['replayid'] = replayId;
data['ts'] = timestamp;
@ -1164,6 +1178,15 @@ class TetrioZen {
}
}
class UserRecords{
String id;
RecordSingle? sprint;
RecordSingle? blitz;
TetrioZen zen;
UserRecords(this.id, this.sprint, this.blitz, this.zen);
}
class Distinguishment {
late String type;
String? detail;
@ -1192,18 +1215,28 @@ class Distinguishment {
}
}
class News {
class News{
late String id;
late String stream;
late List<NewsEntry> news;
News(this.id, this.news);
News.fromJson(Map<String, dynamic> json, String? userID){
id = userID != null ? "user_$userID" : json['news'].first['stream'];
news = [for (var entry in json['news']) NewsEntry.fromJson(entry)];
}
}
class NewsEntry {
//late String id; do i need it?
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});
NewsEntry({required this.type, required this.data, required this.timestamp});
News.fromJson(Map<String, dynamic> json){
id = json["_id"];
stream = json["stream"];
NewsEntry.fromJson(Map<String, dynamic> json){
//id = json["_id"];
type = json["type"];
data = json["data"];
timestamp = DateTime.parse(json['ts']);

View File

@ -1,4 +1,5 @@
import 'dart:math';
import 'dart:typed_data';
import 'tetrio.dart';
@ -57,6 +58,14 @@ class Garbage{ // charsys where???
}
}
class RawReplay{
String id;
Uint8List asBytes;
String asString;
RawReplay(this.id, this.asBytes, this.asString);
}
class ReplayStats{
late int seed;
late int linesCleared;

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 1144 (572 per locale)
/// Strings: 1182 (591 per locale)
///
/// Built on 2024-05-28 at 20:38 UTC
/// Built on 2024-06-16 at 21:03 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
@ -157,6 +157,11 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get history => 'History';
String get sprint => '40 Lines';
String get blitz => 'Blitz';
String get recent => 'Recent';
String get recentRuns => 'Recent runs';
String blitzScore({required Object p}) => '${p} points';
String get openSPreplay => 'Open replay in TETR.IO';
String get downloadSPreplay => 'Download replay';
String get other => 'Other';
String get distinguishment => 'Distinguishment';
String get zen => 'Zen';
@ -244,14 +249,28 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get yourIDAlertTitle => 'Your nickname in TETR.IO';
String get yourIDText => 'When app loads, it will retrieve data for this account';
String get language => 'Language';
String get updateInBackground => 'Update stats in the background';
String get updateInBackgroundDescription => 'While Tetra Stats is running, it can update stats of the current player when cache expires';
String get customization => 'Customization';
String get customizationDescription => 'There is only one toggle, planned to add more settings';
String get customizationDescription => 'Change appearance of different things in Tetra Stats UI';
String get oskKagari => 'Osk Kagari gimmick';
String get oskKagariDescription => 'If on, osk\'s rank on main view will be rendered as :kagari:';
String get AccentColor => 'Accent color';
String get AccentColorDescription => 'Almost all interactive UI elements highlighted with this color';
String get timestamps => 'Timestamps';
String get timestampsDescription => 'You can choose, in which way timestamps shows time';
String get timestampsAbsoluteGMT => 'Absolute (GMT)';
String get timestampsAbsoluteLocalTime => 'Absolute (Your timezone)';
String get timestampsRelative => 'Relative';
String get rating => 'Main representation of rating';
String get ratingDescription => 'TR is not linear, while Glicko does not have boundaries and percentile is volatile';
String get ratingLBposition => 'LB position';
String get sheetbotGraphs => 'Sheetbot-like behavior for radar graphs';
String get sheetbotGraphsDescription => 'If on, points on the graphs can appear on the opposite half of the graph if value is negative';
String get lbStats => 'Show leaderboard based stats';
String get lbStatsDescription => 'That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values';
String get aboutApp => 'About app';
String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy';
String get oskKagari => 'Osk Kagari gimmick';
String get oskKagariDescription => 'If on, osk\'s rank on main view will be rendered as :kagari:';
String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}';
String statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
String matchesViewTitle({required Object nickname}) => '${nickname} TL matches';
@ -833,6 +852,11 @@ class _StringsRu implements Translations {
@override String get history => 'История';
@override String get sprint => '40 линий';
@override String get blitz => 'Блиц';
@override String get recent => 'Недавно';
@override String get recentRuns => 'Недавние';
@override String blitzScore({required Object p}) => '${p} очков';
@override String get openSPreplay => 'Открыть повтор в TETR.IO';
@override String get downloadSPreplay => 'Скачать повтор';
@override String get other => 'Другое';
@override String get distinguishment => 'Заслуга';
@override String get zen => 'Дзен';
@ -920,14 +944,28 @@ class _StringsRu implements Translations {
@override String get yourIDAlertTitle => 'Ваш ник в TETR.IO';
@override String get yourIDText => 'При запуске приложения оно будет получать статистику этого игрока.';
@override String get language => 'Язык (Language)';
@override String get updateInBackground => 'Обновлять статистику в фоне';
@override String get updateInBackgroundDescription => 'Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кеш истекает';
@override String get customization => 'Кастомизация';
@override String get customizationDescription => 'Здесь только один переключатель, в планах добавить больше';
@override String get customizationDescription => 'Измените внешний вид пользовательского интерфейса Tetra Stats';
@override String get oskKagari => '"Оск Кагари" прикол';
@override String get oskKagariDescription => 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:';
@override String get AccentColor => 'Цветовой акцент';
@override String get AccentColorDescription => 'Почти все интерактивные элементы пользовательского интерфейса окрашены в этот цвет';
@override String get timestamps => 'Метки времени';
@override String get timestampsDescription => 'Вы можете выбрать, каким образом метки времени показывают время';
@override String get timestampsAbsoluteGMT => 'Абсолютные (GMT)';
@override String get timestampsAbsoluteLocalTime => 'Абсолютные (Ваш часовой пояс)';
@override String get timestampsRelative => 'Относительные';
@override String get rating => 'Основное представление рейтинга';
@override String get ratingDescription => 'TR нелинеен, тогда как Glicko не имеет границ, а положение в таблице лидеров волатильно';
@override String get ratingLBposition => 'Позиция в рейтинге';
@override String get sheetbotGraphs => 'Графики-радары как у sheetBot';
@override String get sheetbotGraphsDescription => 'Если включено, точки на графике могут появляться на противоположной стороне графика если значение отрицательное';
@override String get lbStats => 'Показывать статистику, основанную на рейтинговой таблице';
@override String get lbStatsDescription => 'Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате';
@override String get aboutApp => 'О приложении';
@override String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy';
@override String get oskKagari => '"Оск Кагари" прикол';
@override String get oskKagariDescription => 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:';
@override String stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
@override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
@override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${nickname}';
@ -1489,6 +1527,11 @@ extension on Translations {
case 'history': return 'History';
case 'sprint': return '40 Lines';
case 'blitz': return 'Blitz';
case 'recent': return 'Recent';
case 'recentRuns': return 'Recent runs';
case 'blitzScore': return ({required Object p}) => '${p} points';
case 'openSPreplay': return 'Open replay in TETR.IO';
case 'downloadSPreplay': return 'Download replay';
case 'other': return 'Other';
case 'distinguishment': return 'Distinguishment';
case 'zen': return 'Zen';
@ -1588,14 +1631,28 @@ extension on Translations {
case 'yourIDAlertTitle': return 'Your nickname in TETR.IO';
case 'yourIDText': return 'When app loads, it will retrieve data for this account';
case 'language': return 'Language';
case 'updateInBackground': return 'Update stats in the background';
case 'updateInBackgroundDescription': return 'While Tetra Stats is running, it can update stats of the current player when cache expires';
case 'customization': return 'Customization';
case 'customizationDescription': return 'There is only one toggle, planned to add more settings';
case 'customizationDescription': return 'Change appearance of different things in Tetra Stats UI';
case 'oskKagari': return 'Osk Kagari gimmick';
case 'oskKagariDescription': return 'If on, osk\'s rank on main view will be rendered as :kagari:';
case 'AccentColor': return 'Accent color';
case 'AccentColorDescription': return 'Almost all interactive UI elements highlighted with this color';
case 'timestamps': return 'Timestamps';
case 'timestampsDescription': return 'You can choose, in which way timestamps shows time';
case 'timestampsAbsoluteGMT': return 'Absolute (GMT)';
case 'timestampsAbsoluteLocalTime': return 'Absolute (Your timezone)';
case 'timestampsRelative': return 'Relative';
case 'rating': return 'Main representation of rating';
case 'ratingDescription': return 'TR is not linear, while Glicko does not have boundaries and percentile is volatile';
case 'ratingLBposition': return 'LB position';
case 'sheetbotGraphs': return 'Sheetbot-like behavior for radar graphs';
case 'sheetbotGraphsDescription': return 'If on, points on the graphs can appear on the opposite half of the graph if value is negative';
case 'lbStats': return 'Show leaderboard based stats';
case 'lbStatsDescription': return 'That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values';
case 'aboutApp': return 'About app';
case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy';
case 'oskKagari': return 'Osk Kagari gimmick';
case 'oskKagariDescription': return 'If on, osk\'s rank on main view will be rendered as :kagari:';
case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} account on ${date}';
case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches';
@ -2081,6 +2138,11 @@ extension on _StringsRu {
case 'history': return 'История';
case 'sprint': return '40 линий';
case 'blitz': return 'Блиц';
case 'recent': return 'Недавно';
case 'recentRuns': return 'Недавние';
case 'blitzScore': return ({required Object p}) => '${p} очков';
case 'openSPreplay': return 'Открыть повтор в TETR.IO';
case 'downloadSPreplay': return 'Скачать повтор';
case 'other': return 'Другое';
case 'distinguishment': return 'Заслуга';
case 'zen': return 'Дзен';
@ -2180,14 +2242,28 @@ extension on _StringsRu {
case 'yourIDAlertTitle': return 'Ваш ник в TETR.IO';
case 'yourIDText': return 'При запуске приложения оно будет получать статистику этого игрока.';
case 'language': return 'Язык (Language)';
case 'updateInBackground': return 'Обновлять статистику в фоне';
case 'updateInBackgroundDescription': return 'Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кеш истекает';
case 'customization': return 'Кастомизация';
case 'customizationDescription': return 'Здесь только один переключатель, в планах добавить больше';
case 'customizationDescription': return 'Измените внешний вид пользовательского интерфейса Tetra Stats';
case 'oskKagari': return '"Оск Кагари" прикол';
case 'oskKagariDescription': return 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:';
case 'AccentColor': return 'Цветовой акцент';
case 'AccentColorDescription': return 'Почти все интерактивные элементы пользовательского интерфейса окрашены в этот цвет';
case 'timestamps': return 'Метки времени';
case 'timestampsDescription': return 'Вы можете выбрать, каким образом метки времени показывают время';
case 'timestampsAbsoluteGMT': return 'Абсолютные (GMT)';
case 'timestampsAbsoluteLocalTime': return 'Абсолютные (Ваш часовой пояс)';
case 'timestampsRelative': return 'Относительные';
case 'rating': return 'Основное представление рейтинга';
case 'ratingDescription': return 'TR нелинеен, тогда как Glicko не имеет границ, а положение в таблице лидеров волатильно';
case 'ratingLBposition': return 'Позиция в рейтинге';
case 'sheetbotGraphs': return 'Графики-радары как у sheetBot';
case 'sheetbotGraphsDescription': return 'Если включено, точки на графике могут появляться на противоположной стороне графика если значение отрицательное';
case 'lbStats': return 'Показывать статистику, основанную на рейтинговой таблице';
case 'lbStatsDescription': return 'Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате';
case 'aboutApp': return 'О приложении';
case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy';
case 'oskKagari': return '"Оск Кагари" прикол';
case 'oskKagariDescription': return 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:';
case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${nickname}';

View File

@ -1,10 +1,16 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'dart:developer' as developer;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/views/customization_view.dart';
import 'package:tetra_stats/views/ranks_averages_view.dart';
import 'package:tetra_stats/views/sprint_and_blitz_averages.dart';
import 'package:tetra_stats/views/tl_leaderboard_view.dart';
import 'package:window_manager/window_manager.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
@ -18,11 +24,40 @@ import 'package:go_router/go_router.dart';
late final PackageInfo packageInfo;
late SharedPreferences prefs;
ColorScheme sheme = const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white);
late TetrioService teto;
ThemeData theme = ThemeData(fontFamily: 'Eurostile Round', colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white), scaffoldBackgroundColor: Colors.black);
void setAccentColor(Color color){ // does this thing work??? yes??? no???
sheme = ColorScheme.dark(primary: color, secondary: Colors.white);
}
// Future<dynamic> computeIsolate(Future Function() function) async {
// final receivePort = ReceivePort();
// var rootToken = RootIsolateToken.instance!;
// await Isolate.spawn<_IsolateData>(
// _isolateEntry,
// _IsolateData(
// token: rootToken,
// function: function,
// answerPort: receivePort.sendPort,
// ),
// );
// return await receivePort.first;
// }
// void _isolateEntry(_IsolateData isolateData) async {
// BackgroundIsolateBinaryMessenger.ensureInitialized(isolateData.token);
// final answer = await isolateData.function();
// isolateData.answerPort.send(answer);
// }
// class _IsolateData {
// final RootIsolateToken token;
// final Function function;
// final SendPort answerPort;
// _IsolateData({
// required this.token,
// required this.function,
// required this.answerPort,
// });
// }
final router = GoRouter(
initialLocation: "/",
@ -34,6 +69,26 @@ final router = GoRouter(
GoRoute(
path: 'settings',
builder: (_, __) => const SettingsView(),
routes: [
GoRoute(
path: 'customization',
builder: (_, __) => const CustomizationView(),
),
]
),
GoRoute(
path: "leaderboard",
builder: (_, __) => const TLLeaderboardView(),
routes: [
GoRoute(
path: "LBvalues",
builder: (_, __) => const RankAveragesView(),
),
]
),
GoRoute(
path: "LBvalues",
builder: (_, __) => const RankAveragesView(),
),
GoRoute(
path: 'states',
@ -44,9 +99,9 @@ final router = GoRouter(
builder: (_, __) => const CalcView(),
),
GoRoute(
path: 'customization',
builder: (_, __) => const CustomizationView(),
),
path: 'sprintAndBlitzAverages',
builder: (_, __) => const SprintAndBlitzView(),
)
]
),
GoRoute( // that one intended for Android users, that can open https://ch.tetr.io/u/ links
@ -77,6 +132,7 @@ void main() async {
packageInfo = await PackageInfo.fromPlatform();
prefs = await SharedPreferences.getInstance();
teto = TetrioService();
// Choosing the locale
String? locale = prefs.getString("locale");
@ -85,15 +141,40 @@ void main() async {
}else{
LocaleSettings.setLocaleRaw(locale);
}
// I dont want to store old cache
Timer.periodic(const Duration(minutes: 5), (Timer timer) {
teto.cacheRoutine();
developer.log("Cache routine complete, next one in ${DateTime.now().add(const Duration(minutes: 5))}", name: "main");
// if (prefs.getBool("updateInBG") == true) teto.fetchTracked(); // TODO: Somehow avoid doing that in main isolate
});
runApp(TranslationProvider(
child: const MyApp(),
));
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
@override
void initState() {
setAccentColor(prefs.getInt("accentColor") != null ? Color(prefs.getInt("accentColor")!) : Colors.cyanAccent);
super.initState();
}
void setAccentColor(Color color){ // does this thing work??? yes??? no???
setState(() {
theme = theme.copyWith(colorScheme: theme.colorScheme.copyWith(primary: color));
});
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
@ -105,11 +186,7 @@ class MyApp extends StatelessWidget {
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
theme: ThemeData(
fontFamily: 'Eurostile Round',
colorScheme: sheme,
scaffoldBackgroundColor: Colors.black
)
theme: theme
);
}
}

View File

@ -1,8 +1,12 @@
// ignore_for_file: type_literal_in_constant_pattern
import 'dart:async';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tetra_stats/data_objects/tetra_stats.dart';
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
import 'package:tetra_stats/main.dart' show packageInfo;
import 'package:flutter/foundation.dart';
@ -65,22 +69,79 @@ const String createTetrioTLReplayStats = '''
)
''';
class CacheController {
late Map<String, dynamic> _cache;
late Map<String, String> _nicknames;
CacheController.init(){
_cache = {};
_nicknames = {};
}
String _getObjectId(dynamic object){
switch (object.runtimeType){
case TetrioPlayer:
object as TetrioPlayer;
_nicknames[object.username] = object.userId;
return object.userId;
case TetrioPlayersLeaderboard:
return object.runtimeType.toString()+object.type;
case Cutoffs:
return object.runtimeType.toString();
case TetrioPlayerFromLeaderboard: // i may be a little stupid
return "${object.runtimeType}topone";
case TetraLeagueAlphaStream:
return object.runtimeType.toString()+object.userId;
case SingleplayerStream:
return object.type+object.userId;
default:
return object.runtimeType.toString()+object.id;
}
}
void store(dynamic object, int? cachedUntil) async {
String key = _getObjectId(object) + cachedUntil!.toString();
_cache[key] = object;
}
dynamic get(String id, Type datatype){
if (_cache.isEmpty) return null;
MapEntry<String, dynamic>? objectEntry;
try{
switch (datatype){
case TetrioPlayer:
objectEntry = id.length <= 16 ? _cache.entries.firstWhere((element) => element.key.startsWith(_nicknames[id]??"huh?")) : _cache.entries.firstWhere((element) => element.key.startsWith(id));
if (id.length <= 16) id = _nicknames[id]??"huh?";
break;
default:
objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(datatype.toString()+id));
id = datatype.toString()+id;
break;
}
} on StateError{
return null;
}
if (int.parse(objectEntry.key.substring(id.length)) <= DateTime.now().millisecondsSinceEpoch){
_cache.remove(objectEntry.key);
return null;
}else{
return objectEntry.value;
}
}
void removeOld() async {
_cache.removeWhere((key, value) => int.parse(key.substring(_getObjectId(value).length)) <= DateTime.now().millisecondsSinceEpoch);
}
void reset(){
_cache.clear();
}
}
class TetrioService extends DB {
final Map<String, String> _players = {};
// I'm trying to send as less requests, as possible, so i'm caching the results of those requests.
// Usually those maps looks like this: {"cached_until_unix_milliseconds": Object}
// TODO: Make a proper caching system
final Map<String, TetrioPlayer> _playersCache = {};
final Map<String, Map<String, dynamic>> _recordsCache = {};
final Map<String, dynamic> _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]}
final Map<String, TetrioPlayersLeaderboard> _leaderboardsCache = {};
final Map<String, PlayerLeaderboardPosition> _lbPositions = {};
final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {};
final Map<String, List<Map<String, double>>> _cutoffsCache = {};
final Map<String, TetrioPlayerFromLeaderboard> _topOneFromLB = {};
final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {};
final _cache = CacheController.init(); // I'm trying to send as less requests, as possible, so i'm caching the results of those requests.
final Map<String, PlayerLeaderboardPosition> _lbPositions = {}; // separate one because attached to the leaderboard
/// Thing, that sends every request to the API endpoints
final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client());
/// We should have only one instanse of this service
@ -154,17 +215,27 @@ class TetrioService extends DB {
return _lbPositions[userID];
}
void cacheRoutine(){
_cache.removeOld();
}
/// Downloads replay from inoue (szy API). Requiers [replayID]. If request have
/// different from 200 statusCode, it will throw an excepction. Returns list, that contains same replay
/// as string and as binary.
Future<List<dynamic>> szyGetReplay(String replayID) async {
try{ // read from cache
var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID);
return cached.value;
}catch (e){
// actually going to obtain
Future<RawReplay> szyGetReplay(String replayID) async {
// Trying to get it from cache first
RawReplay? cached = _cache.get(replayID, RawReplay);
if (cached != null) return cached;
// If failed, trying to obtain replay from download directory
if (!kIsWeb){ // can't obtain download directory on web
var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
var replayFile = File("${downloadPath.path}/$replayID.ttrm");
if (replayFile.existsSync()) return RawReplay(replayID, replayFile.readAsBytesSync(), replayFile.readAsStringSync());
}
// If failed, actually trying to retrieve
Uri url;
if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID});
@ -172,22 +243,16 @@ class TetrioService extends DB {
url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
}
// Trying to obtain replay from download directory first
if (!kIsWeb){ // can't obtain download directory on web
var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
var replayFile = File("${downloadPath.path}/$replayID.ttrm");
if (replayFile.existsSync()) return [replayFile.readAsStringSync(), replayFile.readAsBytesSync()];
}
try{
final response = await client.get(url);
switch (response.statusCode) {
case 200:
developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode);
_replaysCache[replayID] = [response.body, response.bodyBytes]; // Puts results into the cache
return [response.body, response.bodyBytes];
developer.log("szyDownload: Replay $replayID downloaded", name: "services/tetrio_crud");
RawReplay replay = RawReplay(replayID, response.bodyBytes, response.body);
DateTime now = DateTime.now();
_cache.store(replay, now.millisecondsSinceEpoch + 3600000);
return replay;
// if not 200 - throw a unique for each code exception
case 404:
throw SzyNotFound();
@ -203,7 +268,7 @@ class TetrioService extends DB {
case 504:
throw SzyInternalProblem();
default:
developer.log("szyDownload: Failed to download a replay", name: "services/tetrio_crud", error: response.statusCode);
developer.log("szyDownload: Failed to download a replay $replayID", name: "services/tetrio_crud", error: response.statusCode);
throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
}
} on http.ClientException catch (e, s) { // If local http client fails
@ -219,8 +284,8 @@ class TetrioService extends DB {
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
var replayFile = File("${downloadPath.path}/$replayID.ttrm");
if (replayFile.existsSync()) throw TetrioReplayAlreadyExist();
var replay = await szyGetReplay(replayID);
await replayFile.writeAsBytes(replay[1]);
RawReplay replay = await szyGetReplay(replayID);
await replayFile.writeAsBytes(replay.asBytes);
return replayFile.path;
}
@ -235,28 +300,66 @@ class TetrioService extends DB {
if (!isAvailable) throw ReplayNotAvalable(); // if replay too old
// otherwise, actually going to download a replay and analyze it
String replay = (await szyGetReplay(replayID))[0];
String replay = (await szyGetReplay(replayID)).asString;
Map<String, dynamic> toAnalyze = jsonDecode(replay);
ReplayData data = ReplayData.fromJson(toAnalyze);
saveReplayStats(data); // saving to DB for later
return data;
}
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve.
Future<SingleplayerStream> fetchSingleplayerStream(String userID, String stream) async {
SingleplayerStream? cached = _cache.get(userID, SingleplayerStream);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream});
} else {
url = Uri.https('ch.tetr.io', 'api/streams/${stream}_${userID.toLowerCase().trim()}');
}
try {
final response = await client.get(url);
switch (response.statusCode) {
case 200:
if (jsonDecode(response.body)['success']) {
SingleplayerStream records = SingleplayerStream.fromJson(jsonDecode(response.body)['data']['records'], userID, stream);
_cache.store(records, jsonDecode(response.body)['cache']['cached_until']);
developer.log("fetchSingleplayerStream: $stream $userID stream retrieved and cached", name: "services/tetrio_crud");
return records;
} else {
developer.log("fetchSingleplayerStream: 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("fetchSingleplayerStream: Failed to fetch stream $stream $userID", 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);
}
}
/// Gets and returns Top TR for a player with given [id]. May return null if player top tr is unknown
/// or api is unavaliable (404). May throw an exception, if something else happens.
Future<double?> fetchTopTR(String id) async {
try{ // read from cache
var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired
developer.log("fetchTopTR: Top TR retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value.values.first;
}else{ // if cache expired
_topTRcache.remove(cached.key);
developer.log("fetchTopTR: Top TR expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){ // actually going to obtain
developer.log("fetchTopTR: Trying to retrieve Top TR", name: "services/tetrio_crud");
}
Future<TopTr?> fetchTopTR(String id) async {
// Trying to get it from cache first
TopTr? cached = _cache.get(id, TopTr);
if (cached != null) return cached;
Uri url;
if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
@ -269,12 +372,15 @@ class TetrioService extends DB {
switch (response.statusCode) {
case 200: // ok - return the value
_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: double.tryParse(response.body)};
return double.tryParse(response.body);
TopTr result = TopTr(id, double.tryParse(response.body));
_cache.store(result, DateTime.now().millisecondsSinceEpoch + 300000);
return result;
case 404: // not found - return null
TopTr result = TopTr(id, null);
developer.log("fetchTopTR: Probably, player doesn't have top TR", name: "services/tetrio_crud", error: response.statusCode);
_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: null};
return null;
_cache.store(result, DateTime.now().millisecondsSinceEpoch + 300000);
//_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: null};
return result;
// if not 200 or 404 - throw a unique for each code exception
case 403:
throw P1nkl0bst3rForbidden();
@ -300,19 +406,9 @@ class TetrioService extends DB {
// Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above,
// so i'm going to document only unique differences between them
Future<List<Map<String, double>>> fetchCutoffs() async {
try{
var cached = _cutoffsCache.entries.first;
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired
developer.log("fetchCutoffs: Cutoffs retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{ // if cache expired
_topTRcache.remove(cached.key);
developer.log("fetchCutoffs: Cutoffs expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){ // actually going to obtain
developer.log("fetchCutoffs: Trying to retrieve Cutoffs", name: "services/tetrio_crud");
}
Future<Cutoffs?> fetchCutoffs() async {
Cutoffs? cached = _cache.get("", Cutoffs);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
@ -328,17 +424,16 @@ class TetrioService extends DB {
case 200:
Map<String, dynamic> rawData = jsonDecode(response.body);
Map<String, dynamic> data = rawData["cutoffs"] as Map<String, dynamic>;
Map<String, double> trCutoffs = {};
Map<String, double> glickoCutoffs = {};
Cutoffs result = Cutoffs({}, {});
for (String rank in data.keys){
trCutoffs[rank] = data[rank]["rating"];
glickoCutoffs[rank] = data[rank]["glicko"];
result.tr[rank] = data[rank]["rating"];
result.glicko[rank] = data[rank]["glicko"];
}
_cutoffsCache[(rawData["ts"] + 300000).toString()] = [trCutoffs, glickoCutoffs];
return [trCutoffs, glickoCutoffs];
_cache.store(result, rawData["ts"] + 300000);
return result;
case 404:
developer.log("fetchCutoffs: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode);
return [];
return null;
// if not 200 or 404 - throw a unique for each code exception
case 403:
throw P1nkl0bst3rForbidden();
@ -362,18 +457,8 @@ class TetrioService extends DB {
}
Future<TetrioPlayerFromLeaderboard> fetchTopOneFromTheLeaderboard() async {
try{
var cached = _topOneFromLB.entries.first;
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired
developer.log("fetchTopOneFromTheLeaderboard: Leader retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{ // if cache expired
_topTRcache.remove(cached.key);
developer.log("fetchTopOneFromTheLeaderboard: Leader expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){ // actually going to obtain
developer.log("fetchTopOneFromTheLeaderboard: Trying to retrieve leader", name: "services/tetrio_crud");
}
TetrioPlayerFromLeaderboard? cached = _cache.get("topone", TetrioPlayerFromLeaderboard);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
@ -388,7 +473,9 @@ class TetrioService extends DB {
switch (response.statusCode) {
case 200:
var rawJson = jsonDecode(response.body);
return TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["users"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"]));
TetrioPlayerFromLeaderboard result = TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["users"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"]));
_cache.store(result, rawJson["cache"]["cached_until"]);
return result;
case 404:
throw TetrioPlayerNotExist();
// if not 200 or 404 - throw a unique for each code exception
@ -606,18 +693,9 @@ class TetrioService extends DB {
/// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve.
Future<TetrioPlayersLeaderboard> fetchTLLeaderboard() async {
try{
var cached = _leaderboardsCache.entries.firstWhere((element) => element.value.type == "league");
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("fetchTLLeaderboard: Leaderboard retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{
_leaderboardsCache.remove(cached.key);
developer.log("fetchTLLeaderboard: Leaderboard expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
developer.log("fetchTLLeaderboard: Trying to retrieve leaderboard", name: "services/tetrio_crud");
}
TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"});
@ -634,7 +712,8 @@ class TetrioService extends DB {
if (rawJson['success']) { // if api confirmed that everything ok
TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at']));
developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud");
_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard;
//_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard;
_cache.store(leaderboard, rawJson['cache']['cached_until']);
return leaderboard;
} else { // idk how to hit that one
developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson);
@ -661,26 +740,29 @@ class TetrioService extends DB {
}
}
// i want to know progress, so i trying to figure out this thing:
// Stream<TetrioPlayersLeaderboard> fetchTLLeaderboardAsStream() async {
// TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
// if (cached != null) return cached;
// Uri url;
// if (kIsWeb) {
// url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"});
// } else {
// url = Uri.https('ch.tetr.io', 'api/users/lists/league/all');
// }
// Stream<TetrioPlayersLeaderboard> stream = http.StreamedRequest("GET", url);
// }
TetrioPlayersLeaderboard? getCachedLeaderboard(){
return _leaderboardsCache.entries.firstOrNull?.value;
// That function will break if i decide to recive other leaderboards
// TODO: Think about better solution
return _cache.get("league", TetrioPlayersLeaderboard);
}
/// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve.
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");
}
Future<News> fetchNews(String userID) async{
News? cached = _cache.get(userID, News);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
@ -695,8 +777,8 @@ class TetrioService extends DB {
case 200:
var payload = jsonDecode(response.body);
if (payload['success']) { // if api confirmed that everything ok
List<News> news = [for (var entry in payload['data']['news']) News.fromJson(entry)];
_newsCache[payload['cache']['cached_until'].toString()] = news;
News news = News.fromJson(payload['data'], userID);
_cache.store(news, payload['cache']['cached_until']);
developer.log("fetchNews: $userID news retrieved and cached", name: "services/tetrio_crud");
return news;
} else {
@ -727,18 +809,8 @@ class TetrioService extends DB {
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve.
Future<TetraLeagueAlphaStream> fetchTLStream(String userID) async {
try{
var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("fetchTLStream: Stream $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{
_tlStreamsCache.remove(cached.key);
developer.log("fetchTLStream: Cached stream $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
developer.log("fetchTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud");
}
TetraLeagueAlphaStream? cached = _cache.get(userID, TetraLeagueAlphaStream);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
@ -753,7 +825,7 @@ class TetrioService extends DB {
case 200:
if (jsonDecode(response.body)['success']) {
TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID);
_tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream;
_cache.store(stream, jsonDecode(response.body)['cache']['cached_until']);
developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud");
return stream;
} else {
@ -864,21 +936,11 @@ class TetrioService extends DB {
await db.delete(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [rID]);
}
/// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns Map, which contains user id (`user`),
/// Blitz (`blitz`) and 40 Lines (`sprint`) record objects and Zen object (`zen`). Throws an exception if fails to retrieve.
Future<Map<String, dynamic>> fetchRecords(String userID) async {
try{
var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("fetchRecords: $userID records retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{
_recordsCache.remove(cached.key);
developer.log("fetchRecords: $userID records expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
developer.log("fetchRecords: Trying to retrieve $userID records", name: "services/tetrio_crud");
}
/// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns `UserRecords`.
/// Throws an exception if fails to retrieve.
Future<UserRecords> fetchRecords(String userID) async {
UserRecords? cached = _cache.get(userID, UserRecords);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
@ -892,7 +954,7 @@ class TetrioService extends DB {
switch (response.statusCode) {
case 200:
if (jsonDecode(response.body)['success']) {
Map jsonRecords = jsonDecode(response.body);
Map jsonRecords = jsonDecode(response.body);
var sprint = jsonRecords['data']['records']['40l']['record'] != null
? RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'])
: null;
@ -900,10 +962,10 @@ class TetrioService extends DB {
? RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])
: null;
var zen = TetrioZen.fromJson(jsonRecords['data']['zen']);
Map<String, dynamic> map = {"user": userID.toLowerCase().trim(), "sprint": sprint, "blitz": blitz, "zen": zen};
_recordsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = map;
UserRecords result = UserRecords(userID, sprint, blitz, zen);
_cache.store(result, jsonDecode(response.body)['cache']['cached_until']);
developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud");
return map;
return result;
} else {
developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist();
@ -997,8 +1059,7 @@ class TetrioService extends DB {
}
// we not going to add state, that is same, as the previous
bool test = states.last.isSameState(tetrioPlayer);
if (test == false) states.add(tetrioPlayer);
if (!states.last.isSameState(tetrioPlayer)) states.add(tetrioPlayer);
// Making map of the states
final Map<String, dynamic> statesJson = {};
@ -1058,18 +1119,8 @@ class TetrioService extends DB {
/// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user.
/// If [isItDiscordID] is true, function expects [user] to be a discord user id. Throws an exception if fails to retrieve.
Future<TetrioPlayer> fetchPlayer(String user, {bool isItDiscordID = false}) async {
try{
var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("fetchPlayer: User $user retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud");
return cached.value;
}else{
_playersCache.remove(cached.key);
developer.log("fetchPlayer: Cached user $user expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
}
}catch(e){
developer.log("fetchPlayer: Trying to retrieve $user", name: "services/tetrio_crud");
}
TetrioPlayer? cached = _cache.get(user, TetrioPlayer);
if (cached != null) return cached;
if (isItDiscordID){
// trying to find player with given discord id
@ -1130,8 +1181,8 @@ class TetrioService extends DB {
var json = jsonDecode(response.body);
if (json['success']) {
// parse and count stats
TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username']);
_playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player;
TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true));
_cache.store(player, json['cache']['cached_until']);
developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud");
return player;
} else {
@ -1173,4 +1224,15 @@ class TetrioService extends DB {
}
return data;
}
Future<void> fetchTracked() async {
for (String userID in (await getAllPlayerToTrack())) {
TetrioPlayer player = await fetchPlayer(userID);
storeState(player);
sleep(Durations.extralong4);
TetraLeagueAlphaStream matches = await fetchTLStream(userID);
saveTLMatchesFromStream(matches);
sleep(Durations.extralong4);
}
}
}

View File

@ -8,5 +8,13 @@ final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings
final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3);
final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2);
final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..minimumFractionDigits = 0;
final NumberFormat f1 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 1);
final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode);
final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2;
final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2;
/// Readable [a] - [b], without sign
String readableIntDifference(int a, int b){
int result = a - b;
return NumberFormat("#,###;#,###", LocaleSettings.currentLocale.languageCode).format(result);
}

View File

@ -0,0 +1,76 @@
import 'package:intl/intl.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode);
final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode);
/// Returns string, that represents time difference between [dateTime] and now
String relativeDateTime(DateTime dateTime){
Duration difference = dateTime.difference(DateTime.now());
bool inPast = difference.isNegative;
Duration absDifference = difference.abs();
double timeInterval;
// years
timeInterval = absDifference.inSeconds / 31536000;
if (timeInterval >= 100.0) {
return inPast ? "${timeInterval.truncate()} years ago" : "in ${timeInterval.truncate()} years";
} else if (timeInterval >= 10.0) {
return inPast ? "${f1.format(timeInterval)} years ago" : "in ${f1.format(timeInterval)} years";
} else if (timeInterval >= 1.0) {
return inPast ? "${f2.format(timeInterval)} years ago" : "in ${f2.format(timeInterval)} years";
}
// months
timeInterval = absDifference.inSeconds / 2592000;
if (timeInterval >= 10.0) {
return inPast ? "${timeInterval.truncate()} months ago" : "in ${timeInterval.truncate()} months";
} else if (timeInterval >= 1.0) {
return inPast ? "${f1.format(timeInterval)} months ago" : "in ${f1.format(timeInterval)} months";
}
// days
timeInterval = absDifference.inSeconds / 86400;
if (timeInterval >= 10.0) {
return inPast ? "${timeInterval.truncate()} days ago" : "in ${timeInterval.truncate()} days";
} else if (timeInterval >= 1.0) {
return inPast ? "${f1.format(timeInterval)} days ago" : "in ${f1.format(timeInterval)} days";
}
// hours
timeInterval = absDifference.inSeconds / 3600;
if (timeInterval >= 10.0) {
return inPast ? "${timeInterval.truncate()} hours ago" : "in ${timeInterval.truncate()} hours";
} else if (timeInterval >= 1.0) {
return inPast ? "${f1.format(timeInterval)} hours ago" : "in ${f1.format(timeInterval)} hours";
}
// minutes
timeInterval = absDifference.inSeconds / 60;
if (timeInterval >= 10.0) {
return inPast ? "${timeInterval.truncate()} minutes ago" : "in ${timeInterval.truncate()} minutes";
} else if (timeInterval >= 1.0) {
return inPast ? "${f1.format(timeInterval)} minutes ago" : "in ${f1.format(timeInterval)} minutes";
}
// seconds
timeInterval = absDifference.inMilliseconds / 1000;
if (timeInterval >= 10.0) {
return inPast ? "${timeInterval.truncate()} seconds ago" : "in ${timeInterval.truncate()} seconds";
} else {
return inPast ? "${f1.format(timeInterval)} seconds ago" : "in ${f1.format(timeInterval)} seconds";
}
}
/// Takes number of [microseconds] and returns readable 40 lines time
String get40lTime(int microseconds){
return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000);
}
/// Readable [a] - [b], without sign
String readableTimeDifference(Duration a, Duration b){
Duration result = a - b;
return NumberFormat("0.000s;0.000s", LocaleSettings.currentLocale.languageCode).format(result.inMilliseconds/1000);
}

View File

@ -1,3 +1,5 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
@ -5,7 +7,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/widgets/vs_graphs.dart';
import 'package:window_manager/window_manager.dart';
@ -18,7 +20,6 @@ Mode greenSideMode = Mode.player;
List<dynamic> theGreenSide = [null, null, null]; // TetrioPlayer?, List<DropdownMenuItem<TetrioPlayer>>?, TetraLeagueAlpha?
Mode redSideMode = Mode.player;
List<dynamic> theRedSide = [null, null, null];
final TetrioService teto = TetrioService();
final DateFormat dateFormat = DateFormat.yMd(LocaleSettings.currentLocale.languageCode).add_Hm();
var numbersReg = RegExp(r'\d+(\.\d*)*');
late String oldWindowTitle;

View File

@ -1,7 +1,9 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:tetra_stats/views/settings_view.dart' show subtitleStyle;
import 'package:tetra_stats/main.dart' show MyAppState, prefs;
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:window_manager/window_manager.dart';
@ -17,8 +19,10 @@ class CustomizationView extends StatefulWidget {
}
class CustomizationState extends State<CustomizationView> {
late SharedPreferences prefs;
late bool oskKagariGimmick;
late bool sheetbotRadarGraphs;
late int ratingMode;
late int timestampMode;
void changeColor(Color color) {
setState(() => pickerColor = color);
@ -30,7 +34,7 @@ class CustomizationState extends State<CustomizationView> {
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.settings}");
}
_getPreferences().then((value) => setState((){}));
_getPreferences();
super.initState();
}
@ -40,13 +44,27 @@ class CustomizationState extends State<CustomizationView> {
super.dispose();
}
Future<void> _getPreferences() async {
prefs = await SharedPreferences.getInstance();
void _getPreferences() {
if (prefs.getBool("oskKagariGimmick") != null) {
oskKagariGimmick = prefs.getBool("oskKagariGimmick")!;
} else {
oskKagariGimmick = true;
}
if (prefs.getBool("sheetbotRadarGraphs") != null) {
sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")!;
} else {
sheetbotRadarGraphs = false;
}
if (prefs.getInt("ratingMode") != null) {
ratingMode = prefs.getInt("ratingMode")!;
} else {
ratingMode = 0;
}
if (prefs.getInt("timestampMode") != null) {
timestampMode = prefs.getInt("timestampMode")!;
} else {
timestampMode = 0;
}
}
ThemeData getTheme(BuildContext context, Color color){
@ -64,48 +82,89 @@ class CustomizationState extends State<CustomizationView> {
}
return Scaffold(
appBar: AppBar(
title: Text(t.settings),
title: Text(t.customization),
),
backgroundColor: Colors.black,
body: SafeArea(
child: ListView(
children: [
// ListTile(
// title: const Text("Accent color"),
// trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary)),
// onTap: () {
// showDialog(
// context: context,
// builder: (BuildContext context) => AlertDialog(
// title: const Text('Pick an accent color'),
// content: SingleChildScrollView(
// child: ColorPicker(
// pickerColor: pickerColor,
// onColorChanged: changeColor,
// ),
// ),
// actions: <Widget>[
// ElevatedButton(
// child: const Text('Set'),
// onPressed: () {
// setState(() {
// setAccentColor(pickerColor);
// });
// Navigator.of(context).pop();
// },
// ),
// ]));
// }),
// const ListTile(
// title: Text("Font"),
// subtitle: Text("Not implemented"),
// ),
ListTile(
title: Text(t.AccentColor),
subtitle: Text(t.AccentColorDescription, style: subtitleStyle),
trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25),
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Pick an accent color'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: pickerColor,
onColorChanged: changeColor,
),
),
actions: <Widget>[
ElevatedButton(
child: const Text('Set'),
onPressed: () {
setState(() {
context.findAncestorStateOfType<MyAppState>()?.setAccentColor(pickerColor);
prefs.setInt("accentColor", pickerColor.value);
});
Navigator.of(context).pop();
},
),
]));
}
),
// const ListTile(
// title: Text("Stats Table in TL mathes list"),
// subtitle: Text("Not implemented"),
// ),
ListTile(title: Text(t.oskKagari),
subtitle: Text(t.oskKagariDescription),
ListTile(title: Text(t.timestamps),
subtitle: Text(t.timestampsDescription, style: subtitleStyle),
trailing: DropdownButton(
value: timestampMode,
items: <DropdownMenuItem>[
DropdownMenuItem(value: 0, child: Text(t.timestampsAbsoluteGMT)),
DropdownMenuItem(value: 1, child: Text(t.timestampsAbsoluteLocalTime)),
DropdownMenuItem(value: 2, child: Text(t.timestampsRelative))
],
onChanged: (dynamic value){
prefs.setInt("timestampMode", value);
setState(() {
timestampMode = value;
});
},
),
),
ListTile(title: Text(t.rating),
subtitle: Text(t.ratingDescription, style: subtitleStyle),
trailing: DropdownButton(
value: ratingMode,
items: <DropdownMenuItem>[
const DropdownMenuItem(value: 0, child: Text("TR")),
const DropdownMenuItem(value: 1, child: Text("Glicko")),
DropdownMenuItem(value: 2, child: Text(t.ratingLBposition))
],
onChanged: (dynamic value){
prefs.setInt("ratingMode", value);
setState(() {
ratingMode = value;
});
},
),
),
ListTile(title: Text(t.sheetbotGraphs),
subtitle: Text(t.sheetbotGraphsDescription, style: subtitleStyle),
trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){
prefs.setBool("sheetbotRadarGraphs", value);
setState(() {
sheetbotRadarGraphs = value;
});
}),),
ListTile(title: Text(t.oskKagari),
subtitle: Text(t.oskKagariDescription, style: subtitleStyle),
trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){
prefs.setBool("oskKagariGimmick", value);
setState(() {

View File

@ -1,5 +1,6 @@
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: type_literal_in_constant_pattern, use_build_context_synchronously
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
@ -10,39 +11,38 @@ import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:tetra_stats/data_objects/tetra_stats.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/main.dart' show prefs;
import 'package:tetra_stats/main.dart' show prefs, teto;
import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/open_in_browser.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/ranks_averages_view.dart' show RankAveragesView;
import 'package:tetra_stats/views/sprint_and_blitz_averages.dart';
import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView;
import 'package:tetra_stats/views/singleplayer_record_view.dart';
import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView;
import 'package:tetra_stats/widgets/finesse_thingy.dart';
import 'package:tetra_stats/widgets/lineclears_thingy.dart';
import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart';
import 'package:tetra_stats/widgets/recent_sp_games.dart';
import 'package:tetra_stats/widgets/search_box.dart';
import 'package:tetra_stats/widgets/singleplayer_record.dart';
import 'package:tetra_stats/widgets/sp_trailing_stats.dart';
import 'package:tetra_stats/widgets/stat_sell_num.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/widgets/tl_thingy.dart';
import 'package:tetra_stats/widgets/user_thingy.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
final TetrioService teto = TetrioService(); // thing, that manadge our local DB
int _chartsIndex = 0;
bool _gamesPlayedInsteadOfDateAndTime = false;
late ZoomPanBehavior _zoomPanBehavior;
bool _smooth = false;
List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"];
late ScrollController _scrollController;
final NumberFormat _timeInSec = NumberFormat("#,###.###s.", LocaleSettings.currentLocale.languageCode);
final NumberFormat secs = NumberFormat("00.###", LocaleSettings.currentLocale.languageCode);
final DateFormat _dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
class MainView extends StatefulWidget {
final String? player;
@ -59,25 +59,6 @@ Future<void> copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text));
}
/// Takes number of [microseconds] and returns readable 40 lines time
String get40lTime(int microseconds){
return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000);
}
/// Readable [a] - [b], without sign
String readableTimeDifference(Duration a, Duration b){
Duration result = a - b;
return NumberFormat("0.000s;0.000s", LocaleSettings.currentLocale.languageCode).format(result.inMilliseconds/1000);
}
/// Readable [a] - [b], without sign
String readableIntDifference(int a, int b){
int result = a - b;
return NumberFormat("#,###;#,###", LocaleSettings.currentLocale.languageCode).format(result);
}
class _MainState extends State<MainView> with TickerProviderStateMixin {
Future<List> me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up
TetrioPlayersLeaderboard? everyone;
@ -94,10 +75,10 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
//var tableData = <TableRow>[];
final bodyGlobalKey = GlobalKey();
bool _showSearchBar = false;
Timer backgroundUpdate = Timer(const Duration(days: 365), (){});
bool _TLHistoryWasFetched = false;
late TabController _tabController;
late TabController _wideScreenTabController;
late bool fixedScroll;
String get title => "Tetra Stats: $_titleNickname";
@ -105,7 +86,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
void initState() {
initDB();
_scrollController = ScrollController();
_tabController = TabController(length: 6, vsync: this);
_tabController = TabController(length: 7, vsync: this);
_wideScreenTabController = TabController(length: 4, vsync: this);
_zoomPanBehavior = ZoomPanBehavior(
enablePinching: true,
@ -158,6 +139,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
Future<List> fetch(String nickOrID, {bool fetchHistory = false, bool fetchTLmatches = false}) async {
TetrioPlayer me;
_TLHistoryWasFetched = false;
backgroundUpdate.cancel();
// If user trying to search with discord id
if (nickOrID.startsWith("ds:")){
@ -174,23 +156,32 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
// Requesting Tetra League (alpha), records, news and top TR of player
late List<dynamic> requests;
late TetraLeagueAlphaStream tlStream;
late Map<String, dynamic> records;
late List<News> news;
late UserRecords records;
late News news;
late SingleplayerStream recent;
late SingleplayerStream sprint;
late SingleplayerStream blitz;
late TetrioPlayerFromLeaderboard? topOne;
late double? topTR;
requests = await Future.wait([ // all at once
late TopTr? topTR;
requests = await Future.wait([ // all at once (7 requests to oskware lmao)
teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor),
teto.fetchSingleplayerStream(_searchFor, "any_userrecent"),
teto.fetchSingleplayerStream(_searchFor, "40l_userbest"),
teto.fetchSingleplayerStream(_searchFor, "blitz_userbest"),
prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=><Map<String, double>>[]),
(me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null),
if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR
(me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR
]);
tlStream = requests[0] as TetraLeagueAlphaStream;
records = requests[1] as Map<String, dynamic>;
news = requests[2] as List<News>;
topOne = requests[4] as TetrioPlayerFromLeaderboard?;
topTR = requests.elementAtOrNull(5) as double?; // No TR - no Top TR
records = requests[1] as UserRecords;
news = requests[2] as News;
recent = requests[3] as SingleplayerStream;
sprint = requests[4] as SingleplayerStream;
blitz = requests[5] as SingleplayerStream;
topOne = requests[7] as TetrioPlayerFromLeaderboard?;
topTR = requests[8] as TopTr?; // No TR - no Top TR
meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId);
if (prefs.getBool("showPositions") == true){
@ -202,8 +193,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!);
}
}
Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as List<Map<String, double>>).elementAtOrNull(0);
Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as List<Map<String, double>>).elementAtOrNull(1);
Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[6] as Cutoffs?)?.tr;
Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[6] as Cutoffs?)?.glicko;
if (me.tlSeason1.gamesPlayed > 9) {
thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank];
@ -308,7 +299,14 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
compareWith = null;
chartsData = [];
}
return [me, records, states, tlMatches, compareWith, isTracking, news, topTR];
if (prefs.getBool("updateInBG") == true) {
backgroundUpdate = Timer(me.cachedUntil!.difference(DateTime.now()), () {
changePlayer(me.userId);
});
}
return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz];
}
/// Triggers widgets rebuild
@ -440,6 +438,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
Tab(text: t.history),
Tab(text: t.sprint),
Tab(text: t.blitz),
Tab(text: t.recentRuns),
Tab(text: t.other),
],
),
@ -459,7 +458,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
tl: snapshot.data![0].tlSeason1,
userID: snapshot.data![0].userId,
states: snapshot.data![2],
topTR: snapshot.data![7],
topTR: snapshot.data![7]?.tr,
bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff,
@ -478,14 +477,14 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
),
],),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,),
_OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
_TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, recent: snapshot.data![8], sprintStream: snapshot.data![9], blitzStream: snapshot.data![10]),
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
] : [
TLThingy(
tl: snapshot.data![0].tlSeason1,
userID: snapshot.data![0].userId,
states: snapshot.data![2],
topTR: snapshot.data![7],
topTR: snapshot.data![7]?.tr,
bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff,
@ -499,9 +498,10 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
),
_TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank),
_RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank),
_OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![9]),
SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank, stream: snapshot.data![10]),
_RecentSingleplayersThingy(snapshot.data![8]),
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6])
],
),
),
@ -643,12 +643,7 @@ class _NavDrawerState extends State<NavDrawer> {
leading: const Icon(Icons.leaderboard),
title: Text(t.tlLeaderboard),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TLLeaderboardView(),
),
);
context.go("/leaderboard");
},
),
),
@ -657,12 +652,7 @@ class _NavDrawerState extends State<NavDrawer> {
leading: const Icon(Icons.compress),
title: Text(t.rankAveragesViewTitle),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RankAveragesView(),
),
);
context.go("/LBvalues");
},
),
),
@ -671,12 +661,7 @@ class _NavDrawerState extends State<NavDrawer> {
leading: const Icon(Icons.bar_chart),
title: Text(t.sprintAndBlitsViewTitle),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SprintAndBlitzView(),
),
);
context.go("/sprintAndBlitzAverages");
},
),
),
@ -758,7 +743,7 @@ class _TLRecords extends StatelessWidget {
leading: Text("${data[index].endContext.firstWhere((element) => element.userId == userID).points} : ${data[index].endContext.firstWhere((element) => element.userId != userID).points}",
style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)),
title: Text("vs. ${data[index].endContext.firstWhere((element) => element.userId != userID).username}"),
subtitle: Text(_dateFormat.format(data[index].timestamp)),
subtitle: Text(timestamp(data[index].timestamp), style: const TextStyle(color: Colors.grey)),
trailing: TrailingStats(
data[index].endContext.firstWhere((element) => element.userId == userID).secondary,
data[index].endContext.firstWhere((element) => element.userId == userID).tertiary,
@ -921,7 +906,7 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> {
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20),
),
),
Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : _dateFormat.format(data.timestamp))
Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp))
],
),
);
@ -964,39 +949,38 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> {
primaryYAxis: const NumericAxis(
rangePadding: ChartRangePadding.additional,
),
margin: const EdgeInsets.all(0),
series: <CartesianSeries>[
if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>(
enableTooltip: true,
// splineType: SplineType.cardinal,
// cardinalSplineTension: 0.2,
dataSource: widget.data,
animationDuration: 0,
opacity: _smooth ? 0 : 1,
xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed,
yValueMapper: (_HistoryChartSpot data, _) => data.stat,
color: Theme.of(context).colorScheme.primary,
trendlines:<Trendline>[
Trendline(
isVisible: _smooth,
period: (widget.data.length/175).floor(),
type: TrendlineType.movingAverage,
color: Colors.blue)
color: Theme.of(context).colorScheme.primary)
],
)
else StepLineSeries<_HistoryChartSpot, DateTime>(
enableTooltip: true,
// splineType: SplineType.cardinal,
// cardinalSplineTension: 0.2,
dataSource: widget.data,
animationDuration: 0,
opacity: _smooth ? 0 : 1,
xValueMapper: (_HistoryChartSpot data, _) => data.timestamp,
yValueMapper: (_HistoryChartSpot data, _) => data.stat,
color: Theme.of(context).colorScheme.primary,
trendlines:<Trendline>[
Trendline(
isVisible: _smooth,
period: (widget.data.length/175).floor(),
type: TrendlineType.movingAverage,
color: Colors.blue)
color: Theme.of(context).colorScheme.primary)
],
),
],
@ -1010,9 +994,12 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> {
class _TwoRecordsThingy extends StatelessWidget {
final RecordSingle? sprint;
final RecordSingle? blitz;
final SingleplayerStream recent;
final SingleplayerStream sprintStream;
final SingleplayerStream blitzStream;
final String? rank;
const _TwoRecordsThingy({required this.sprint, required this.blitz, this.rank});
const _TwoRecordsThingy({required this.sprint, required this.blitz, this.rank, required this.recent, required this.sprintStream, required this.blitzStream});
Color getColorOfRank(int rank){
if (rank == 1) return Colors.yellowAccent;
@ -1028,23 +1015,23 @@ class _TwoRecordsThingy extends StatelessWidget {
//if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)));
late MapEntry closestAverageBlitz;
late bool blitzBetterThanClosestAverage;
bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.endContext!.score > blitzAverages[rank]! : null;
bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.endContext.score > blitzAverages[rank]! : null;
late MapEntry closestAverageSprint;
late bool sprintBetterThanClosestAverage;
bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.endContext!.finalTime < sprintAverages[rank]! : null;
bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.endContext.finalTime < sprintAverages[rank]! : null;
if (sprint != null) {
closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.endContext!.finalTime).abs() < (b -sprint!.endContext!.finalTime).abs() ? a : b));
sprintBetterThanClosestAverage = sprint!.endContext!.finalTime < closestAverageSprint.value;
closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.endContext.finalTime).abs() < (b -sprint!.endContext.finalTime).abs() ? a : b));
sprintBetterThanClosestAverage = sprint!.endContext.finalTime < closestAverageSprint.value;
}
if (blitz != null){
closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.endContext!.score).abs() < (b -blitz!.endContext!.score).abs() ? a : b));
blitzBetterThanClosestAverage = blitz!.endContext!.score > closestAverageBlitz.value;
closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.endContext.score).abs() < (b -blitz!.endContext.score).abs() ? a : b));
blitzBetterThanClosestAverage = blitz!.endContext.score > closestAverageBlitz.value;
}
return SingleChildScrollView(child: Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
crossAxisAlignment: WrapCrossAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
@ -1060,24 +1047,24 @@ class _TwoRecordsThingy extends StatelessWidget {
children: [
Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
RichText(text: TextSpan(
text: sprint != null ? get40lTime(sprint!.endContext!.finalTime.inMicroseconds) : "---",
text: sprint != null ? get40lTime(sprint!.endContext.finalTime.inMicroseconds) : "---",
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey),
//children: [TextSpan(text: get40lTime(record!.endContext!.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
//children: [TextSpan(text: get40lTime(record!.endContext.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
),
),
if (sprint != null) RichText(text: TextSpan(
text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext!.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
))
else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext!.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle(
else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.endContext.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)),
if (sprint!.rank != null) TextSpan(text: "${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank!))),
if (sprint!.rank != null) const TextSpan(text: ""),
TextSpan(text: _dateFormat.format(sprint!.timestamp!)),
TextSpan(text: timestamp(sprint!.timestamp)),
]
),
),
@ -1089,14 +1076,39 @@ class _TwoRecordsThingy extends StatelessWidget {
alignment: WrapAlignment.spaceBetween,
spacing: 20,
children: [
StatCellNum(playerStat: sprint!.endContext!.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext!.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: sprint!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
],
),
if (sprint != null) FinesseThingy(sprint?.endContext?.finesse, sprint?.endContext?.finessePercentage),
if (sprint != null) LineclearsThingy(sprint!.endContext!.clears, sprint!.endContext!.lines, sprint!.endContext!.holds, sprint!.endContext!.tSpins),
if (sprint != null) Text("${sprint!.endContext!.inputs} KP • ${f2.format(sprint!.endContext!.kps)} KPS")
if (sprint != null) FinesseThingy(sprint?.endContext.finesse, sprint?.endContext.finessePercentage),
if (sprint != null) LineclearsThingy(sprint!.endContext.clears, sprint!.endContext.lines, sprint!.endContext.holds, sprint!.endContext.tSpins),
if (sprint != null) Text("${sprint!.endContext.inputs} KP • ${f2.format(sprint!.endContext.kps)} KPS"),
Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20,
children: [
TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${sprint!.replayId}"));}, child: Text(t.openSPreplay)),
TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${sprint!.replayId}"));}, child: Text(t.downloadSPreplay)),
],
),
if (sprintStream.records.length > 1) SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 1; i < sprintStream.records.length; i++) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))),
leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ),
title: Text(get40lTime(sprintStream.records[i].endContext.finalTime.inMicroseconds),
style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(sprintStream.records[i].endContext)
)
],
),
)
]
),
Column(
@ -1116,7 +1128,7 @@ class _TwoRecordsThingy extends StatelessWidget {
text: "",
style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white),
children: [
TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.endContext!.score) : "---"),
TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.endContext.score) : "---"),
//WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48))
]
),
@ -1127,13 +1139,13 @@ class _TwoRecordsThingy extends StatelessWidget {
text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext!.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
))
else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext!.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle(
else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.endContext.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)),
TextSpan(text: _dateFormat.format(blitz!.timestamp!)),
TextSpan(text: timestamp(blitz!.timestamp)),
if (blitz!.rank != null) const TextSpan(text: ""),
if (blitz!.rank != null) TextSpan(text: "${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank!))),
]
@ -1150,145 +1162,69 @@ class _TwoRecordsThingy extends StatelessWidget {
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20,
children: [
StatCellNum(playerStat: blitz!.endContext!.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext!.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true)
StatCellNum(playerStat: blitz!.endContext.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: blitz!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true)
],
),
if (blitz != null) FinesseThingy(blitz?.endContext?.finesse, blitz?.endContext?.finessePercentage),
if (blitz != null) LineclearsThingy(blitz!.endContext!.clears, blitz!.endContext!.lines, blitz!.endContext!.holds, blitz!.endContext!.tSpins),
if (blitz != null) Text("${blitz!.endContext!.piecesPlaced} P • ${blitz!.endContext!.inputs} KP • ${f2.format(blitz!.endContext!.kpp)} KPP • ${f2.format(blitz!.endContext!.kps)} KPS")
if (blitz != null) FinesseThingy(blitz?.endContext.finesse, blitz?.endContext.finessePercentage),
if (blitz != null) LineclearsThingy(blitz!.endContext.clears, blitz!.endContext.lines, blitz!.endContext.holds, blitz!.endContext.tSpins),
if (blitz != null) Text("${blitz!.endContext.piecesPlaced} P • ${blitz!.endContext.inputs} KP • ${f2.format(blitz!.endContext.kpp)} KPP • ${f2.format(blitz!.endContext.kps)} KPS"),
Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20,
children: [
TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${blitz!.replayId}"));}, child: Text(t.openSPreplay)),
TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${blitz!.replayId}"));}, child: Text(t.downloadSPreplay)),
],
),
if (blitzStream.records.length > 1) SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 1; i < sprintStream.records.length; i++) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))),
leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ),
title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].endContext.score)} points",
style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(blitzStream.records[i].endContext)
)
],
),
)
],
),
SizedBox(
width: 400,
child: RecentSingleplayerGames(recent: recent),
)
]),
));
}
}
class _RecordThingy extends StatelessWidget {
final RecordSingle? record;
final String? rank;
class _RecentSingleplayersThingy extends StatelessWidget {
final SingleplayerStream recent;
/// Widget that displays data from [record]
const _RecordThingy({required this.record, this.rank});
Color getColorOfRank(int rank){
if (rank == 1) return Colors.yellowAccent;
if (rank == 2) return Colors.blueGrey;
if (rank == 3) return Colors.brown[400]!;
if (rank <= 9) return Colors.blueAccent;
if (rank <= 99) return Colors.greenAccent;
return Colors.grey;
}
const _RecentSingleplayersThingy(this.recent);
@override
Widget build(BuildContext context) {
if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)));
late MapEntry closestAverageBlitz;
late bool blitzBetterThanClosestAverage;
bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext!.score > blitzAverages[rank]! : null;
late MapEntry closestAverageSprint;
late bool sprintBetterThanClosestAverage;
bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext!.finalTime < sprintAverages[rank]! : null;
if (record!.stream.contains("40l")) {
closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.endContext!.finalTime).abs() < (b -record!.endContext!.finalTime).abs() ? a : b));
sprintBetterThanClosestAverage = record!.endContext!.finalTime < closestAverageSprint.value;
}else if (record!.stream.contains("blitz")){
closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-record!.endContext!.score).abs() < (b -record!.endContext!.score).abs() ? a : b));
blitzBetterThanClosestAverage = record!.endContext!.score > closestAverageBlitz.value;
}
return LayoutBuilder(
builder: (context, constraints) {
bool bigScreen = constraints.maxWidth > 768;
return SingleChildScrollView(
controller: _scrollController,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (record!.stream.contains("40l")) Padding(padding: const EdgeInsets.only(right: 8.0),
child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96)
),
if (record!.stream.contains("blitz")) Padding(padding: const EdgeInsets.only(right: 8.0),
child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96)
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (record!.stream.contains("40l")) Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
if (record!.stream.contains("blitz")) Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
RichText(text: TextSpan(
text: record!.stream.contains("40l") ? get40lTime(record!.endContext!.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record!.endContext!.score),
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white),
),
),
RichText(text: TextSpan(
text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [
if (record!.stream.contains("40l") && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext!.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
))
else if (record!.stream.contains("40l") && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext!.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
))
else if (record!.stream.contains("blitz") && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext!.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
))
else if (record!.stream.contains("blitz") && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext!.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)),
if (record!.rank != null) TextSpan(text: "${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank!))),
if (record!.rank != null) const TextSpan(text: ""),
TextSpan(text: _dateFormat.format(record!.timestamp!)),
]
),
)
],),
],
),
if (record!.stream.contains("40l")) Wrap(
alignment: WrapAlignment.spaceBetween,
spacing: 20,
children: [
StatCellNum(playerStat: record!.endContext!.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext!.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
],
),
if (record!.stream.contains("blitz")) Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20,
children: [
StatCellNum(playerStat: record!.endContext!.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext!.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext!.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true)
],
),
FinesseThingy(record?.endContext?.finesse, record?.endContext?.finessePercentage),
LineclearsThingy(record!.endContext!.clears, record!.endContext!.lines, record!.endContext!.holds, record!.endContext!.tSpins),
if (record!.stream.contains("40l")) Text("${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kps)} KPS"),
if (record!.stream.contains("blitz")) Text("${record!.endContext!.piecesPlaced} P • ${record!.endContext!.inputs} KP • ${f2.format(record!.endContext!.kpp)} KPP • ${f2.format(record!.endContext!.kps)} KPS")
]
),
),
);
}
return SingleChildScrollView(
child: RecentSingleplayerGames(recent: recent, hideTitle: true)
);
}
}
class _OtherThingy extends StatelessWidget {
final TetrioZen? zen;
final String? bio;
final Distinguishment? distinguishment;
final List<News>? newsletter;
final News? newsletter;
/// Widget, that shows players [distinguishment], [bio], [zen] and [newsletter]
const _OtherThingy({required this.zen, required this.bio, required this.distinguishment, this.newsletter});
@ -1337,7 +1273,7 @@ class _OtherThingy extends StatelessWidget {
}
/// Handles [news] entry and returns widget that contains this entry
ListTile getNewsTile(News news){
ListTile getNewsTile(NewsEntry news){
Map<String, String> gametypes = {
"40l": t.sprint,
"blitz": t.blitz,
@ -1359,7 +1295,7 @@ class _OtherThingy extends StatelessWidget {
]
)
),
subtitle: Text(_dateFormat.format(news.timestamp)),
subtitle: Text(timestamp(news.timestamp)),
);
case "personalbest":
return ListTile(
@ -1374,7 +1310,7 @@ class _OtherThingy extends StatelessWidget {
]
)
),
subtitle: Text(_dateFormat.format(news.timestamp)),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/icons/improvement-local.png",
height: 48,
@ -1396,7 +1332,7 @@ class _OtherThingy extends StatelessWidget {
]
)
),
subtitle: Text(_dateFormat.format(news.timestamp)),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/tetrio_badges/${news.data["type"]}.png",
height: 48,
@ -1418,7 +1354,7 @@ class _OtherThingy extends StatelessWidget {
]
)
),
subtitle: Text(_dateFormat.format(news.timestamp)),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png",
height: 48,
@ -1439,7 +1375,7 @@ class _OtherThingy extends StatelessWidget {
]
)
),
subtitle: Text(_dateFormat.format(news.timestamp)),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/icons/supporter-tag.png",
height: 48,
@ -1460,7 +1396,7 @@ class _OtherThingy extends StatelessWidget {
]
)
),
subtitle: Text(_dateFormat.format(news.timestamp)),
subtitle: Text(timestamp(news.timestamp)),
leading: Image.asset(
"res/icons/supporter-tag.png",
height: 48,
@ -1473,7 +1409,7 @@ class _OtherThingy extends StatelessWidget {
default: // if type is unknown
return ListTile(
title: Text(t.newsParts.unknownNews(type: news.type)),
subtitle: Text(_dateFormat.format(news.timestamp)),
subtitle: Text(timestamp(news.timestamp)),
);
}
}
@ -1519,7 +1455,7 @@ class _OtherThingy extends StatelessWidget {
],
),
),
if (newsletter != null && newsletter!.isNotEmpty && showNewsTitle)
if (newsletter != null && newsletter!.news.isNotEmpty && showNewsTitle)
Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
],
);
@ -1535,9 +1471,9 @@ class _OtherThingy extends StatelessWidget {
SizedBox(width: 450, child: getShit(context, true, false)),
SizedBox(width: constraints.maxWidth - 450, child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: newsletter!.length+1,
itemCount: newsletter!.news.length+1,
itemBuilder: (BuildContext context, int index) {
return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter![index-1]);
return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter!.news[index-1]);
}
))
]
@ -1546,9 +1482,9 @@ class _OtherThingy extends StatelessWidget {
else {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: newsletter!.length+1,
itemCount: newsletter!.news.length+1,
itemBuilder: (BuildContext context, int index) {
return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter![index-1]);
return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter!.news[index-1]);
},
);
}

View File

@ -2,12 +2,11 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/tl_match_view.dart';
import 'package:window_manager/window_manager.dart';
final TetrioService teto = TetrioService();
late String oldWindowTitle;
class MatchesView extends StatefulWidget {

View File

@ -5,7 +5,7 @@ import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/views/rank_averages_view.dart';
import 'package:window_manager/window_manager.dart';
import 'main_view.dart'; // lol
import 'package:tetra_stats/main.dart' show teto;
class RankAveragesView extends StatefulWidget {
const RankAveragesView({super.key});

View File

@ -1,20 +1,19 @@
import 'dart:io';
import 'package:go_router/go_router.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/main.dart' show packageInfo;
import 'package:tetra_stats/main.dart' show packageInfo, teto, prefs;
import 'package:file_selector/file_selector.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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';
import 'package:tetra_stats/utils/open_in_browser.dart';
import 'package:window_manager/window_manager.dart';
late String oldWindowTitle;
TextStyle subtitleStyle = const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey);
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@ -24,10 +23,9 @@ class SettingsView extends StatefulWidget {
}
class SettingsState extends State<SettingsView> {
late SharedPreferences prefs;
final TetrioService teto = TetrioService();
String defaultNickname = "Checking...";
late bool showPositions;
late bool updateInBG;
final TextEditingController _playertext = TextEditingController();
@override
@ -46,9 +44,9 @@ class SettingsState extends State<SettingsView> {
super.dispose();
}
Future<void> _getPreferences() async {
prefs = await SharedPreferences.getInstance();
void _getPreferences() {
showPositions = prefs.getBool("showPositions") ?? false;
updateInBG = prefs.getBool("updateInBG") ?? false;
_setDefaultNickname(prefs.getString("player"));
}
@ -93,7 +91,7 @@ class SettingsState extends State<SettingsView> {
children: [
ListTile(
title: Text(t.exportDB),
subtitle: Text(t.exportDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
subtitle: Text(t.exportDBDescription, style: subtitleStyle),
onTap: () {
if (kIsWeb){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb)));
@ -147,7 +145,7 @@ class SettingsState extends State<SettingsView> {
),
ListTile(
title: Text(t.importDB),
subtitle: Text(t.importDBDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
subtitle: Text(t.importDBDescription, style: subtitleStyle),
onTap: () {
if (kIsWeb){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb)));
@ -199,6 +197,7 @@ class SettingsState extends State<SettingsView> {
),
ListTile(
title: Text(t.yourID),
subtitle: Text(t.yourIDText, style: subtitleStyle),
trailing: Text(defaultNickname),
onTap: () => showDialog(
context: context,
@ -244,6 +243,7 @@ class SettingsState extends State<SettingsView> {
),
ListTile(
title: Text(t.language),
subtitle: Text("By default, the system language will be selected (if available among Tetra Stats locales, otherwise English)", style: subtitleStyle),
trailing: DropdownButton(
items: locales,
value: LocaleSettings.currentLocale,
@ -261,8 +261,16 @@ class SettingsState extends State<SettingsView> {
subtitle: Text(t.customizationDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
trailing: const Icon(Icons.arrow_right),
onTap: () {
context.go("/customization");
context.go("/settings/customization");
},),
ListTile(title: Text(t.updateInBackground),
subtitle: Text(t.updateInBackgroundDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
trailing: Switch(value: updateInBG, onChanged: (bool value){
prefs.setBool("updateInBG", value);
setState(() {
updateInBG = value;
});
}),),
ListTile(title: Text(t.lbStats),
subtitle: Text(t.lbStatsDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
trailing: Switch(value: showPositions, onChanged: (bool value){

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/widgets/singleplayer_record.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
class SingleplayerRecordView extends StatelessWidget {
final RecordSingle record;
const SingleplayerRecordView({super.key, required this.record});
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
//bool bigScreen = MediaQuery.of(context).size.width >= 368;
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: Text("${
switch (record.endContext.gameType){
"40l" => t.sprint,
"blitz" => t.blitz,
String() => "5000000 Blast",
}
} ${timestamp(record.timestamp)}"),
),
body: SafeArea(
child: SingleChildScrollView(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
SingleplayerRecord(record: record, hideTitle: true),
// TODO: Insert replay link here
]
)
],
)
)
),
);
}
}

View File

@ -1,12 +1,11 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/main_view.dart';
import 'package:window_manager/window_manager.dart';
late String oldWindowTitle;
@ -39,6 +38,7 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
bool bigScreen = MediaQuery.of(context).size.width >= 368;
return Scaffold(
appBar: AppBar(
title: Text(t.sprintAndBlitsViewTitle),
@ -50,6 +50,7 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
children: [
Container(
alignment: Alignment.center,
width: MediaQuery.of(context).size.width,
constraints: const BoxConstraints(maxWidth: 600),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
@ -59,18 +60,18 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: TableBorder.all(color: Colors.grey.shade900),
columnWidths: {0: const FixedColumnWidth(48)},
columnWidths: const {0: FixedColumnWidth(48)},
children: [
TableRow(
children: [
Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(t.sprint, textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
child: Text(t.sprint, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(t.blitz, textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
child: Text(t.blitz, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
]
),
@ -80,11 +81,11 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/${sprintEntry.key}.png", height: 48)),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(get40lTime(sprintEntry.value.inMicroseconds), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
child: Text(get40lTime(sprintEntry.value.inMicroseconds), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(NumberFormat.decimalPattern().format(blitzAverages[sprintEntry.key]), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
child: Text(NumberFormat.decimalPattern().format(blitzAverages[sprintEntry.key]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
),
]
)

View File

@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/views/mathes_view.dart';
import 'package:tetra_stats/views/state_view.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:window_manager/window_manager.dart';
class StatesView extends StatefulWidget {
@ -37,7 +39,6 @@ class StatesState extends State<StatesView> {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
return Scaffold(
appBar: AppBar(
title: Text(t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.username.toUpperCase())),
@ -59,14 +60,14 @@ class StatesState extends State<StatesView> {
itemCount: widget.states.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(dateFormat.format(widget.states[index].state)),
title: Text(timestamp(widget.states[index].state)),
subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: NumberFormat.compact().format(widget.states[index].tlSeason1.rd))),
trailing: IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () {
DateTime nn = widget.states[index].state;
teto.deleteState(widget.states[index]).then((value) => setState(() {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: dateFormat.format(nn)))));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn)))));
}));
},
),

View File

@ -174,8 +174,8 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
prototypeItem: ListTile(
leading: Text("0", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)),
title: Text("ehhh...", style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)),
trailing: Container(height: bigScreen ? 48 : 36, width: 1,),
subtitle: Text("eh..."),
trailing: SizedBox(height: bigScreen ? 48 : 36, width: 1,),
subtitle: const Text("eh..."),
),
itemBuilder: (context, index) {
return ListTile(

View File

@ -1,12 +1,14 @@
// ignore_for_file: use_build_context_synchronously
// ignore_for_file: use_build_context_synchronously, type_literal_in_constant_pattern
import 'dart:io';
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy;
import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/widgets/vs_graphs.dart';
import 'main_view.dart' show teto, secs;
import 'package:tetra_stats/main.dart' show teto;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
@ -14,11 +16,8 @@ import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/open_in_browser.dart';
import 'package:window_manager/window_manager.dart';
// ignore: avoid_web_libraries_in_flutter
// import 'dart:html' show AnchorElement, document;
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
int roundSelector = -1; // -1 = match averages, otherwise round number-1
List<DropdownMenuItem> rounds = []; // index zero will be match stats
bool timeWeightedStatsAvaliable = true;
@ -49,7 +48,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
replayData = teto.analyzeReplay(widget.record.replayId, widget.record.replayAvalable);
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${dateFormat.format(widget.record.timestamp)}");
windowManager.setTitle("Tetra Stats: ${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${timestamp(widget.record.timestamp)}");
}
super.initState();
}
@ -708,7 +707,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
final t = Translations.of(context);
return Scaffold(
appBar: AppBar(
title: Text("${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${dateFormat.format(widget.record.timestamp)}"),
title: Text("${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${timestamp(widget.record.timestamp)}"),
actions: [
PopupMenuButton(
enabled: widget.record.replayAvalable,

View File

@ -4,12 +4,12 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/utils/filesizes_converter.dart';
import 'package:tetra_stats/views/states_view.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:window_manager/window_manager.dart';
final TetrioService teto = TetrioService();
late String oldWindowTitle;
class TrackedPlayersView extends StatefulWidget {
@ -38,7 +38,6 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
return Scaffold(
appBar: AppBar(
title: Text(t.trackedPlayersViewTitle),
@ -110,7 +109,7 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
itemBuilder: (context, index) {
return ListTile(
title: Text(t.trackedPlayersEntry(nickname: allPlayers[keys[index]]!.last.username, numberOfStates: allPlayers[keys[index]]!.length)),
subtitle: Text(t.trackedPlayersDescription(firstStateDate: dateFormat.format(allPlayers[keys[index]]!.first.state), lastStateDate: dateFormat.format(allPlayers[keys[index]]!.last.state))),
subtitle: Text(t.trackedPlayersDescription(firstStateDate: timestamp(allPlayers[keys[index]]!.first.state), lastStateDate: timestamp(allPlayers[keys[index]]!.last.state))),
trailing: IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () {

View File

@ -1,3 +1,5 @@
// ignore_for_file: curly_braces_in_flow_control_structures
import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/utils/numers_formats.dart';

View File

@ -1,3 +1,5 @@
// ignore_for_file: curly_braces_in_flow_control_structures
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';

View File

@ -1,10 +1,269 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart';
import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart';
import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart';
import 'package:fl_chart/src/utils/canvas_wrapper.dart';
import 'package:fl_chart/src/utils/utils.dart';
import 'package:tetra_stats/main.dart' show prefs;
import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
class MyRadarChartPainter extends RadarChartPainter{
MyRadarChartPainter() : super() {
_backgroundPaint = Paint()
..style = PaintingStyle.fill
..isAntiAlias = true;
_borderPaint = Paint()..style = PaintingStyle.stroke;
_gridPaint = Paint()..style = PaintingStyle.stroke;
_tickPaint = Paint()..style = PaintingStyle.stroke;
_graphPaint = Paint();
_graphBorderPaint = Paint();
_graphPointPaint = Paint();
_ticksTextPaint = TextPainter();
_titleTextPaint = TextPainter();
sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")??false;
}
late Paint _borderPaint;
late Paint _backgroundPaint;
late Paint _gridPaint;
late Paint _tickPaint;
late Paint _graphPaint;
late Paint _graphBorderPaint;
late Paint _graphPointPaint;
late TextPainter _ticksTextPaint;
late TextPainter _titleTextPaint;
late bool sheetbotRadarGraphs;
@override
double getChartCenterValue(RadarChartData data) {
final dataSetMaxValue = sheetbotRadarGraphs ? max(data.maxEntry.value, data.minEntry.value.abs()) : data.maxEntry.value;
final dataSetMinValue = data.minEntry.value;
final tickSpace = getSpaceBetweenTicks(data);
final centerValue = (dataSetMinValue < 0 && sheetbotRadarGraphs) ? 0.0 : dataSetMinValue;
return dataSetMaxValue == dataSetMinValue
? getDefaultChartCenterValue()
: centerValue;
}
@override
double getSpaceBetweenTicks(RadarChartData data) {
final defaultCenterValue = getDefaultChartCenterValue();
final dataSetMaxValue = sheetbotRadarGraphs ? max(data.maxEntry.value, data.minEntry.value.abs()) : data.maxEntry.value;
final dataSetMinValue = (data.minEntry.value < 0 && sheetbotRadarGraphs) ? 0.0 : data.minEntry.value;
final tickSpace = sheetbotRadarGraphs ? dataSetMaxValue / data.tickCount : (dataSetMaxValue - dataSetMinValue) / data.tickCount;
final defaultTickSpace =
(dataSetMaxValue - defaultCenterValue) / (data.tickCount + 1);
return dataSetMaxValue == dataSetMinValue ? defaultTickSpace : tickSpace;
}
@override
double getScaledPoint(RadarEntry point, double radius, RadarChartData data) {
final centerValue = getChartCenterValue(data);
final distanceFromPointToCenter = point.value - centerValue;
final distanceFromMaxToCenter = max(data.maxEntry.value, data.minEntry.value.abs()) - centerValue;
if (distanceFromMaxToCenter == 0) {
return radius * distanceFromPointToCenter / 0.001;
}
return radius * distanceFromPointToCenter / distanceFromMaxToCenter;
}
@override
double getFirstTickValue(RadarChartData data) {
final defaultCenterValue = getDefaultChartCenterValue();
final dataSetMaxValue = sheetbotRadarGraphs ? max(data.maxEntry.value, data.minEntry.value.abs()) : data.maxEntry.value;
final dataSetMinValue = (data.minEntry.value < 0 && sheetbotRadarGraphs) ? 0.0 : data.minEntry.value;
return dataSetMaxValue == dataSetMinValue
? (dataSetMaxValue - defaultCenterValue) / (data.tickCount + 1) +
defaultCenterValue
: dataSetMinValue;
}
@override
void drawTicks(
BuildContext context,
CanvasWrapper canvasWrapper,
PaintHolder<RadarChartData> holder,
) {
final data = holder.data;
final size = canvasWrapper.size;
final centerX = radarCenterX(size);
final centerY = radarCenterY(size);
final centerOffset = Offset(centerX, centerY);
/// controls Radar chart size
final radius = radarRadius(size);
_backgroundPaint.color = data.radarBackgroundColor;
_borderPaint
..color = data.radarBorderData.color
..strokeWidth = data.radarBorderData.width;
if (data.radarShape == RadarShape.circle) {
/// draw radar background
canvasWrapper
..drawCircle(centerOffset, radius, _backgroundPaint)
/// draw radar border
..drawCircle(centerOffset, radius, _borderPaint);
} else {
final path =
_generatePolygonPath(centerX, centerY, radius, data.titleCount);
/// draw radar background
canvasWrapper
..drawPath(path, _backgroundPaint)
/// draw radar border
..drawPath(path, _borderPaint);
}
final tickSpace = getSpaceBetweenTicks(data);
final ticks = <double>[];
var tickValue = getFirstTickValue(data);
for (var i = 0; i <= data.tickCount; i++) {
ticks.add(tickValue);
tickValue += tickSpace;
}
final tickDistance = radius / (ticks.length-1);
_tickPaint
..color = data.tickBorderData.color
..strokeWidth = data.tickBorderData.width;
/// draw radar ticks
ticks.sublist(1, ticks.length).asMap().forEach(
(index, tick) {
final tickRadius = tickDistance * (index + 1);
if (data.radarShape == RadarShape.circle) {
canvasWrapper.drawCircle(centerOffset, tickRadius, _tickPaint);
} else {
canvasWrapper.drawPath(
_generatePolygonPath(centerX, centerY, tickRadius, data.titleCount),
_tickPaint,
);
}
_ticksTextPaint
..text = TextSpan(
text: percentage.format(tick),
style: Utils().getThemeAwareTextStyle(context, data.ticksTextStyle),
)
..textDirection = TextDirection.ltr
..layout(maxWidth: size.width);
canvasWrapper.drawText(
_ticksTextPaint,
Offset(centerX + 5, centerY - tickRadius - _ticksTextPaint.height/2),
);
},
);
}
Path _generatePolygonPath(
double centerX,
double centerY,
double radius,
int count,
) {
final path = Path()..moveTo(centerX, centerY - radius);
final angle = (2 * pi) / count;
for (var index = 0; index < count; index++) {
final xAngle = cos(angle * index - pi / 2);
final yAngle = sin(angle * index - pi / 2);
path.lineTo(centerX + radius * xAngle, centerY + radius * yAngle);
}
path.lineTo(centerX, centerY - radius);
return path;
}
}
class MyRadarChartLeaf extends RadarChartLeaf{
MyRadarChartLeaf({required super.data, required super.targetData});
@override
RenderRadarChart createRenderObject(BuildContext context) => MyRenderRadarChart(
context,
data,
targetData,
MediaQuery.of(context).textScaler,
);
}
class MyRenderRadarChart extends RenderRadarChart{
MyRenderRadarChart(super.context, super.data, super.targetData, super.textScaler);
@override
RadarChartPainter painter = MyRadarChartPainter();
}
class MyRadarChart extends ImplicitlyAnimatedWidget {
const MyRadarChart(
this.data, {
super.key,
Duration swapAnimationDuration = const Duration(milliseconds: 150),
Curve swapAnimationCurve = Curves.linear,
}) : super(
duration: swapAnimationDuration,
curve: swapAnimationCurve,
);
/// Determines how the [RadarChart] should be look like.
final RadarChartData data;
@override
RadarChartState createState() => RadarChartState();
}
class RadarChartState extends AnimatedWidgetBaseState<MyRadarChart> {
/// we handle under the hood animations (implicit animations) via this tween,
/// it lerps between the old [RadarChartData] to the new one.
RadarChartDataTween? _radarChartDataTween;
@override
Widget build(BuildContext context) {
final showingData = _getDate();
return MyRadarChartLeaf(
data: _radarChartDataTween!.evaluate(animation),
targetData: showingData,
);
}
RadarChartData _getDate() {
return widget.data;
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_radarChartDataTween = visitor(
_radarChartDataTween,
widget.data,
(dynamic value) =>
RadarChartDataTween(begin: value as RadarChartData, end: widget.data),
) as RadarChartDataTween?;
}
}
class Graphs extends StatelessWidget{
const Graphs(
this.apm,
this.pps,
@ -37,7 +296,7 @@ class Graphs extends StatelessWidget{
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
child: MyRadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
@ -73,6 +332,8 @@ class Graphs extends StatelessWidget{
},
dataSets: [
RadarDataSet(
fillColor: Theme.of(context).colorScheme.primary.withAlpha(100),
borderColor: Theme.of(context).colorScheme.primary,
dataEntries: [
RadarEntry(value: apm * apmWeight),
RadarEntry(value: pps * ppsWeight),
@ -114,7 +375,7 @@ class Graphs extends StatelessWidget{
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
child: MyRadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
@ -140,6 +401,8 @@ class Graphs extends StatelessWidget{
},
dataSets: [
RadarDataSet(
fillColor: Theme.of(context).colorScheme.primary.withAlpha(100),
borderColor: Theme.of(context).colorScheme.primary,
dataEntries: [
RadarEntry(value: playstyle.opener),
RadarEntry(value: playstyle.stride),
@ -169,7 +432,7 @@ class Graphs extends StatelessWidget{
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
child: MyRadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
@ -195,6 +458,8 @@ class Graphs extends StatelessWidget{
},
dataSets: [
RadarDataSet(
fillColor: Theme.of(context).colorScheme.primary.withAlpha(100),
borderColor: Theme.of(context).colorScheme.primary,
dataEntries: [
RadarEntry(value: attack),
RadarEntry(value: speed),

View File

@ -13,14 +13,14 @@ class TrailingStats extends StatelessWidget{
@override
Widget build(BuildContext context) {
const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100);
const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100, fontSize: 13);
return Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
defaultVerticalAlignment: TableCellVerticalAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
columnWidths: const {
0: FixedColumnWidth(42),
2: FixedColumnWidth(42),
0: FixedColumnWidth(48),
2: FixedColumnWidth(48),
},
children: [
TableRow(children: [Text(f2.format(yourAPM), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourAPM), textAlign: TextAlign.right, style: style), const Text(" APM", textAlign: TextAlign.right, style: style)]),

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/singleplayer_record_view.dart';
import 'package:tetra_stats/widgets/sp_trailing_stats.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
class RecentSingleplayerGames extends StatelessWidget{
final SingleplayerStream recent;
final bool hideTitle;
const RecentSingleplayerGames({required this.recent, this.hideTitle = false, super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
if (!hideTitle) Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(t.recent, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
),
for(RecordSingle record in recent.records) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: record))),
leading: Text(
switch (record.endContext.gameType){
"40l" => "40L",
"blitz" => "BLZ",
"5mblast" => "5MB",
String() => "huh",
},
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9)
),
title: Text(
switch (record.endContext.gameType){
"40l" => get40lTime(record.endContext.finalTime.inMicroseconds),
"blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(record.endContext.score)),
"5mblast" => get40lTime(record.endContext.finalTime.inMicroseconds),
String() => "huh",
},
style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(record.endContext)
)
],
);
}
}

View File

@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/open_in_browser.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'package:tetra_stats/views/singleplayer_record_view.dart';
import 'package:tetra_stats/widgets/finesse_thingy.dart';
import 'package:tetra_stats/widgets/lineclears_thingy.dart';
import 'package:tetra_stats/widgets/sp_trailing_stats.dart';
import 'package:tetra_stats/widgets/stat_sell_num.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
class SingleplayerRecord extends StatelessWidget {
final RecordSingle? record;
final SingleplayerStream? stream;
final String? rank;
final bool hideTitle;
/// Widget that displays data from [record]
const SingleplayerRecord({super.key, required this.record, this.stream, this.rank, this.hideTitle = false});
Color getColorOfRank(int rank){
if (rank == 1) return Colors.yellowAccent;
if (rank == 2) return Colors.blueGrey;
if (rank == 3) return Colors.brown[400]!;
if (rank <= 9) return Colors.blueAccent;
if (rank <= 99) return Colors.greenAccent;
return Colors.grey;
}
@override
Widget build(BuildContext context) {
if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)));
late MapEntry closestAverageBlitz;
late bool blitzBetterThanClosestAverage;
bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.score > blitzAverages[rank]! : null;
late MapEntry closestAverageSprint;
late bool sprintBetterThanClosestAverage;
bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.endContext.finalTime < sprintAverages[rank]! : null;
if (record!.endContext.gameType == "40l") {
closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.endContext.finalTime).abs() < (b -record!.endContext.finalTime).abs() ? a : b));
sprintBetterThanClosestAverage = record!.endContext.finalTime < closestAverageSprint.value;
}else if (record!.endContext.gameType == "blitz"){
closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-record!.endContext.score).abs() < (b -record!.endContext.score).abs() ? a : b));
blitzBetterThanClosestAverage = record!.endContext.score > closestAverageBlitz.value;
}
return LayoutBuilder(
builder: (context, constraints) {
bool bigScreen = constraints.maxWidth > 768;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (record!.endContext.gameType == "40l") Padding(padding: const EdgeInsets.only(right: 8.0),
child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96)
),
if (record!.endContext.gameType == "blitz") Padding(padding: const EdgeInsets.only(right: 8.0),
child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96)
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (record!.endContext.gameType == "40l" && !hideTitle) Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
if (record!.endContext.gameType == "blitz" && !hideTitle) Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
RichText(text: TextSpan(
text: record!.endContext.gameType == "40l" ? get40lTime(record!.endContext.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record!.endContext.score),
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white),
),
),
RichText(text: TextSpan(
text: "",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
children: [
if (record!.endContext.gameType == "40l" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
))
else if (record!.endContext.gameType == "40l" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.endContext.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle(
color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
))
else if (record!.endContext.gameType == "blitz" && (rank != null && rank != "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent
))
else if (record!.endContext.gameType == "blitz" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.endContext.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle(
color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent
)),
if (record!.rank != null) TextSpan(text: "${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank!))),
if (record!.rank != null) const TextSpan(text: ""),
TextSpan(text: timestamp(record!.timestamp)),
]
),
)
],),
],
),
if (record!.endContext.gameType == "40l") Wrap(
alignment: WrapAlignment.spaceBetween,
spacing: 20,
children: [
StatCellNum(playerStat: record!.endContext.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
],
),
if (record!.endContext.gameType == "blitz") Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20,
children: [
StatCellNum(playerStat: record!.endContext.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
StatCellNum(playerStat: record!.endContext.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true)
],
),
FinesseThingy(record?.endContext.finesse, record?.endContext.finessePercentage),
LineclearsThingy(record!.endContext.clears, record!.endContext.lines, record!.endContext.holds, record!.endContext.tSpins),
if (record!.endContext.gameType == "40l") Text("${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kps)} KPS"),
if (record!.endContext.gameType == "blitz") Text("${record!.endContext.piecesPlaced} P • ${record!.endContext.inputs} KP • ${f2.format(record!.endContext.kpp)} KPP • ${f2.format(record!.endContext.kps)} KPS"),
Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 20,
children: [
TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${record!.replayId}"));}, child: Text(t.openSPreplay)),
TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${record!.replayId}"));}, child: Text(t.downloadSPreplay)),
],
),
if (stream != null && stream!.records.length > 1) for(int i = 1; i < stream!.records.length; i++) ListTile(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: stream!.records[i]))),
leading: Text("#${i+1}",
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9)
),
title: Text(
switch (stream!.records[i].endContext.gameType){
"40l" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds),
"blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(stream!.records[i].endContext.score)),
"5mblast" => get40lTime(stream!.records[i].endContext.finalTime.inMicroseconds),
String() => "huh",
},
style: const TextStyle(fontSize: 18)),
subtitle: Text(timestamp(stream!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)),
trailing: SpTrailingStats(stream!.records[i].endContext)
)
]
),
),
);
}
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
class SpTrailingStats extends StatelessWidget{
final EndContextSingle endContext;
const SpTrailingStats(this.endContext, {super.key});
@override
Widget build(BuildContext context) {
const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100, fontSize: 13);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("${endContext.piecesPlaced} P, ${f2.format(endContext.pps)} PPS", style: style, textAlign: TextAlign.right),
Text("${intf.format(endContext.finessePercentage*100)}% F, ${endContext.finesse?.faults} FF", style: style, textAlign: TextAlign.right),
Text(switch(endContext.gameType){
"40l" => "${f2.format(endContext.kps)} KPS, ${f2.format(endContext.kpp)} KPP",
"blitz" => "${intf.format(endContext.spp)} SPP, lvl ${endContext.level}",
"5mblast" => "${intf.format(endContext.spp)} SPP, ${endContext.lines} L",
String() => "huh"
}, style: style, textAlign: TextAlign.right)
],
);
}
}

View File

@ -43,16 +43,16 @@ class StatCellNum extends StatelessWidget {
@override
Widget build(BuildContext context) {
NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits ?? 0);
NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = fractionDigits ?? 0;
NumberFormat fractionf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits ?? 0)..maximumIntegerDigits = 0;
num fraction = playerStat.isNegative ? 1 - (playerStat - playerStat.floor()) : playerStat - playerStat.floor();
int integer = playerStat.isNegative ? (playerStat + fraction).toInt() : (playerStat - fraction).toInt();
String formated = f.format(playerStat);
List<String> splited = formated.split(f.symbols.DECIMAL_SEP);
return Column(
children: [
RichText(
text: TextSpan(text: intf.format(integer),
text: TextSpan(text: splited[0],
children: [
TextSpan(text: fractionf.format(fraction).substring(1), style: smallDecimal ? const TextStyle(fontSize: 16) : null)
if ((fractionDigits??0) > 0) TextSpan(text: f.symbols.DECIMAL_SEP+splited[1], style: smallDecimal ? const TextStyle(fontFamily: "Eurostile Round", fontSize: 16) : null)
],
style: TextStyle(
fontFamily: "Eurostile Round Extended",

View File

@ -0,0 +1,20 @@
import 'package:intl/intl.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart';
import 'package:tetra_stats/utils/relative_timestamps.dart';
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
String timestamp(DateTime dateTime){
int timestampMode = prefs.getInt("timestampMode")??0;
return timestampMode == 2 ? relativeDateTime(dateTime) : dateFormat.format(timestampMode == 1 ? dateTime.toLocal() : dateTime);
}
// class TextTimestamp extends StatelessWidget{
// @override
// Widget build(BuildContext context) {
// // TODO: implement build
// return;
// }
// }

View File

@ -70,7 +70,7 @@ class TLProgress extends StatelessWidget{
if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"),
if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"),
if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"),
if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null))
if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null))
]
)
),
@ -82,8 +82,8 @@ class TLProgress extends StatelessWidget{
maximum: 1,
interval: 1,
ranges: [
if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.rating)!, color: Colors.cyanAccent, position: LinearElementPosition.cross)
else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Colors.cyanAccent, position: LinearElementPosition.cross),
if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.rating)!, color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross)
else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross),
if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent, position: LinearElementPosition.inside),
if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside)
],

View File

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show prefs;
import 'package:tetra_stats/utils/numers_formats.dart';
var fDiff = NumberFormat("+#,###.####;-#,###.####");
class TLRatingThingy extends StatelessWidget{
final String userID;
final TetraLeagueAlpha tlData;
final TetraLeagueAlpha? oldTl;
final double? topTR;
const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR});
@override
Widget build(BuildContext context) {
bool oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true;
bool bigScreen = MediaQuery.of(context).size.width >= 768;
String decimalSeparator = f4.symbols.DECIMAL_SEP;
List<String> formatedTR = f4.format(tlData.rating).split(decimalSeparator);
List<String> formatedGlicko = f4.format(tlData.glicko).split(decimalSeparator);
List<String> formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator);
return Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.spaceAround,
crossAxisAlignment: WrapCrossAlignment.center,
clipBehavior: Clip.hardEdge,
children: [
(userID == "5e32fc85ab319c2ab1beb07c" && oskKagariGimmick) // he love her so much, you can't even imagine
? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform?
: Image.asset("res/tetrio_tl_alpha_ranks/${tlData.rank}.png", height: 128),
Column(
children: [
RichText(
text: TextSpan(
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white),
children: switch(prefs.getInt("ratingMode")){
1 => [
TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (formatedGlicko.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedGlicko[1]),
TextSpan(text: " Glicko", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28))
],
2 => [
TextSpan(text: "${t.top} ${formatedPercentile[0]}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (formatedPercentile.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedPercentile[1]),
TextSpan(text: " %", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28))
],
_ => [
TextSpan(text: formatedTR[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (formatedTR.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedTR[1]),
TextSpan(text: " TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28))
],
}
)
),
if (oldTl != null) Text(
switch(prefs.getInt("ratingMode")){
1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko",
2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %",
_ => "${fDiff.format(tlData.rating - oldTl!.rating)} TR"
},
textAlign: TextAlign.center,
style: TextStyle(
color: tlData.rating - oldTl!.rating < 0 ?
Colors.red :
Colors.green
),
),
Column(
children: [
RichText(
textAlign: TextAlign.center,
softWrap: true,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(text: prefs.getInt("ratingMode") == 2 ? "${f2.format(tlData.rating)} TR • % ${t.rank}: ${tlData.percentileRank.toUpperCase()}" : "${t.top} ${f2.format(tlData.percentile * 100)}% (${tlData.percentileRank.toUpperCase()})"),
if (tlData.bestRank != "z") const TextSpan(text: ""),
if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"),
if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"),
TextSpan(text: "${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.rating)} TR • RD: " : "Glicko: ${f2.format(tlData.glicko!)}±"}"),
TextSpan(text: f2.format(tlData.rd!), style: tlData.decaying ? TextStyle(color: tlData.rd! > 98 ? Colors.red : Colors.yellow) : null),
if (tlData.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: tlData.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic)
],
),
),
],
),
],
),
],
);
}
}

View File

@ -3,17 +3,17 @@ import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart';
import 'package:tetra_stats/utils/colors_functions.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/widgets/gauget_num.dart';
import 'package:tetra_stats/widgets/graphs.dart';
import 'package:tetra_stats/widgets/stat_sell_num.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/widgets/tl_progress_bar.dart';
import 'package:tetra_stats/widgets/tl_rating_thingy.dart';
var fDiff = NumberFormat("+#,###.###;-#,###.###");
var intFDiff = NumberFormat("+#,###;-#,###");
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
var intFDiff = NumberFormat("+#,###.000;-#,###.000");
class TLThingy extends StatefulWidget {
final TetraLeagueAlpha tl;
@ -48,7 +48,6 @@ class _TLThingyState extends State<TLThingy> {
void initState() {
_currentRangeValues = const RangeValues(0, 1);
sortedStates = widget.states.reversed.toList();
oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true;
oldTl = sortedStates.elementAtOrNull(1)?.tlSeason1;
currentTl = widget.tl;
super.initState();
@ -57,8 +56,9 @@ class _TLThingyState extends State<TLThingy> {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
NumberFormat fractionfEstTR = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2)..maximumIntegerDigits = 0;
NumberFormat fractionfEstTRAcc = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3)..maximumIntegerDigits = 0;
String decimalSeparator = f2.symbols.DECIMAL_SEP;
List<String> estTRformated = f2.format(currentTl.estTr!.esttr).split(decimalSeparator);
List<String> estTRaccFormated = intFDiff.format(currentTl.esttracc!).split(".");
if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,));
return LayoutBuilder(builder: (context, constraints) {
bool bigScreen = constraints.maxWidth >= 768;
@ -69,7 +69,7 @@ class _TLThingyState extends State<TLThingy> {
return Column(
children: [
if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (oldTl != null) Text(t.comparingWith(newDate: dateFormat.format(currentTl.timestamp), oldDate: dateFormat.format(oldTl!.timestamp)),
if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)),
textAlign: TextAlign.center,),
if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(),
labels: RangeLabels(
@ -92,52 +92,7 @@ class _TLThingyState extends State<TLThingy> {
});
},
),
if (currentTl.gamesPlayed >= 10)
Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.spaceAround,
crossAxisAlignment: WrapCrossAlignment.center,
clipBehavior: Clip.hardEdge,
children: [
(widget.userID == "5e32fc85ab319c2ab1beb07c" && oskKagariGimmick) // he love her so much, you can't even imagine
? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform?
: Image.asset("res/tetrio_tl_alpha_ranks/${currentTl.rank}.png", height: 128),
Column(
children: [
Text("${f2.format(currentTl.rating)} TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
if (oldTl != null) Text(
"${fDiff.format(currentTl.rating - oldTl!.rating)} TR",
textAlign: TextAlign.center,
style: TextStyle(
color: currentTl.rating - oldTl!.rating < 0 ?
Colors.red :
Colors.green
),
),
Column(
children: [
RichText(
textAlign: TextAlign.center,
softWrap: true,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(text: "${t.top} ${f2.format(currentTl.percentile * 100)}% (${currentTl.percentileRank.toUpperCase()})"),
if (currentTl.bestRank != "z") const TextSpan(text: ""),
if (currentTl.bestRank != "z") TextSpan(text: "${t.topRank}: ${currentTl.bestRank.toUpperCase()}"),
if (widget.topTR != null) TextSpan(text: " (${f2.format(widget.topTR)} TR)"),
TextSpan(text: " • Glicko: ${f2.format(currentTl.glicko!)}±"),
TextSpan(text: f2.format(currentTl.rd!), style: currentTl.decaying ? TextStyle(color: currentTl.rd! > 98 ? Colors.red : Colors.yellow) : null),
if (currentTl.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: currentTl.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic)
],
),
),
],
),
],
),
],
),
if (currentTl.gamesPlayed >= 10) TLRatingThingy(userID: widget.userID, tlData: currentTl, oldTl: oldTl, topTR: widget.topTR),
if (currentTl.gamesPlayed > 9) TLProgress(
tlData: currentTl,
previousRankTRcutoff: widget.thatRankCutoff,
@ -290,12 +245,10 @@ class _TLThingyState extends State<TLThingy> {
),
if (currentTl.estTr != null)
Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
padding: const EdgeInsets.fromLTRB(8, 20, 8, 20),
child: Container(
//alignment: Alignment.center,
width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85,
height: 70,
constraints: const BoxConstraints(maxWidth: 768),
constraints: const BoxConstraints(maxWidth: 500),
child: Stack(
children: [
Positioned(
@ -306,9 +259,9 @@ class _TLThingyState extends State<TLThingy> {
Text(t.statCellNum.estOfTR, style: const TextStyle(height: 0.1),),
RichText(
text: TextSpan(
text: intf.format(currentTl.estTr!.esttr.truncate()),
text: estTRformated[0],
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white),
children: [TextSpan(text: fractionfEstTR.format(currentTl.estTr!.esttr - currentTl.estTr!.esttr.truncate()).substring(1), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
children: [TextSpan(text: decimalSeparator+estTRformated[1], style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))]
),
),
RichText(text: TextSpan(
@ -335,10 +288,10 @@ class _TLThingyState extends State<TLThingy> {
Text(t.statCellNum.accOfEst, style: const TextStyle(height: 0.1),),
RichText(
text: TextSpan(
text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? intFDiff.format(currentTl.esttracc!.truncate()) : "---",
text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? estTRaccFormated[0] : "---",
style: TextStyle(fontFamily: "Eurostile Round", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white),
children: [
TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? fractionfEstTRAcc.format(currentTl.esttracc!.isNegative ? 1 - (currentTl.esttracc! - currentTl.esttracc!.truncate()) : (currentTl.esttracc! - currentTl.esttracc!.truncate())).substring(1) : ".---", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))
TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? decimalSeparator+estTRaccFormated[1] : ".---", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))
]
),
),

View File

@ -4,11 +4,13 @@ import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/views/compare_view.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/utils/text_shadow.dart';
import 'dart:developer' as developer;
import 'package:tetra_stats/widgets/stat_sell_num.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
const Map<int, double> xpTableScuffed = { // level: xp required
05000: 67009018.4885772,
@ -35,7 +37,6 @@ 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;
@ -125,7 +126,7 @@ class UserThingy extends StatelessWidget {
],
),
showStateTimestamp
? Text(t.fetchDate(date: dateFormat.format(player.state)))
? Text(t.fetchDate(date: timestamp(player.state)))
: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.center, spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, children: [
FutureBuilder(
future: teto.isPlayerTracking(player.userId),
@ -339,7 +340,7 @@ class UserThingy extends StatelessWidget {
),
children: [
if (player.country != null) TextSpan(text: "${t.countries[player.country]}"),
TextSpan(text: "${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}"),
TextSpan(text: "${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${timestamp(player.registrationTime!)}'}"),
if (player.supporterTier > 0) const TextSpan(text: ""),
if (player.supporterTier > 0) WidgetSpan(child: Icon(player.supporterTier > 1 ? Icons.star : Icons.star_border, color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic),
if (player.supporterTier > 0) TextSpan(text: player.supporterTier.toString(), style: TextStyle(color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white))
@ -385,7 +386,7 @@ class UserThingy extends StatelessWidget {
children: [
Image.asset("res/tetrio_badges/${badge.badgeId}.png"),
Text(badge.ts != null
? t.obtainDate(date: dateFormat.format(badge.ts!))
? t.obtainDate(date: timestamp(badge.ts!))
: t.assignedManualy),
],
)

View File

@ -1,5 +1,6 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:tetra_stats/widgets/graphs.dart' show MyRadarChart;
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart';
@ -31,7 +32,7 @@ class VsGraphs extends StatelessWidget{
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
child: MyRadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
@ -134,7 +135,7 @@ class VsGraphs extends StatelessWidget{
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
child: MyRadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,
@ -211,7 +212,7 @@ class VsGraphs extends StatelessWidget{
child: SizedBox(
height: 310,
width: 310,
child: RadarChart(
child: MyRadarChart(
RadarChartData(
radarShape: RadarShape.polygon,
tickCount: 4,

View File

@ -2,7 +2,7 @@ name: tetra_stats
description: Track your and other player stats in TETR.IO
publish_to: 'none'
version: 1.5.3+19
version: 1.6.0+20
environment:
sdk: '>=3.0.0'

View File

@ -8,6 +8,11 @@
"history": "History",
"sprint": "40 Lines",
"blitz": "Blitz",
"recent": "Recent",
"recentRuns": "Recent runs",
"blitzScore": "$p points",
"openSPreplay": "Open replay in TETR.IO",
"downloadSPreplay": "Download replay",
"other": "Other",
"distinguishment": "Distinguishment",
"zen": "Zen",
@ -109,14 +114,28 @@
"yourIDAlertTitle": "Your nickname in TETR.IO",
"yourIDText": "When app loads, it will retrieve data for this account",
"language": "Language",
"updateInBackground": "Update stats in the background",
"updateInBackgroundDescription": "While Tetra Stats is running, it can update stats of the current player when cache expires",
"customization": "Customization",
"customizationDescription": "There is only one toggle, planned to add more settings",
"customizationDescription": "Change appearance of different things in Tetra Stats UI",
"oskKagari": "Osk Kagari gimmick",
"oskKagariDescription": "If on, osk's rank on main view will be rendered as :kagari:",
"AccentColor": "Accent color",
"AccentColorDescription": "Almost all interactive UI elements highlighted with this color",
"timestamps": "Timestamps",
"timestampsDescription": "You can choose, in which way timestamps shows time",
"timestampsAbsoluteGMT": "Absolute (GMT)",
"timestampsAbsoluteLocalTime": "Absolute (Your timezone)",
"timestampsRelative": "Relative",
"rating": "Main representation of rating",
"ratingDescription": "TR is not linear, while Glicko does not have boundaries and percentile is volatile",
"ratingLBposition": "LB position",
"sheetbotGraphs": "Sheetbot-like behavior for radar graphs",
"sheetbotGraphsDescription": "If on, points on the graphs can appear on the opposite half of the graph if value is negative",
"lbStats": "Show leaderboard based stats",
"lbStatsDescription": "That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values",
"aboutApp": "About app",
"aboutAppText": "${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy",
"oskKagari": "Osk Kagari gimmick",
"oskKagariDescription": "If on, osk's rank on main view will be rendered as :kagari:",
"stateViewTitle": "${nickname} account on ${date}",
"statesViewTitle": "${number} states of ${nickname} account",
"matchesViewTitle": "${nickname} TL matches",

View File

@ -8,6 +8,11 @@
"history": "История",
"sprint": "40 линий",
"blitz": "Блиц",
"recent": "Недавно",
"recentRuns": "Недавние",
"blitzScore": "$p очков",
"openSPreplay": "Открыть повтор в TETR.IO",
"downloadSPreplay": "Скачать повтор",
"other": "Другое",
"distinguishment": "Заслуга",
"zen": "Дзен",
@ -109,14 +114,28 @@
"yourIDAlertTitle": "Ваш ник в TETR.IO",
"yourIDText": "При запуске приложения оно будет получать статистику этого игрока.",
"language": "Язык (Language)",
"updateInBackground": "Обновлять статистику в фоне",
"updateInBackgroundDescription": "Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кеш истекает",
"customization": "Кастомизация",
"customizationDescription": "Здесь только один переключатель, в планах добавить больше",
"customizationDescription": "Измените внешний вид пользовательского интерфейса Tetra Stats",
"oskKagari": "\"Оск Кагари\" прикол",
"oskKagariDescription": "Если включено, вместо настоящего ранга оска будет рендерится :kagari:",
"AccentColor": "Цветовой акцент",
"AccentColorDescription": "Почти все интерактивные элементы пользовательского интерфейса окрашены в этот цвет",
"timestamps": "Метки времени",
"timestampsDescription": "Вы можете выбрать, каким образом метки времени показывают время",
"timestampsAbsoluteGMT": "Абсолютные (GMT)",
"timestampsAbsoluteLocalTime": "Абсолютные (Ваш часовой пояс)",
"timestampsRelative": "Относительные",
"rating": "Основное представление рейтинга",
"ratingDescription": "TR нелинеен, тогда как Glicko не имеет границ, а положение в таблице лидеров волатильно",
"ratingLBposition": "Позиция в рейтинге",
"sheetbotGraphs": "Графики-радары как у sheetBot",
"sheetbotGraphsDescription": "Если включено, точки на графике могут появляться на противоположной стороне графика если значение отрицательное",
"lbStats": "Показывать статистику, основанную на рейтинговой таблице",
"lbStatsDescription": "Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате",
"aboutApp": "О приложении",
"aboutAppText": "${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy",
"oskKagari": "\"Оск Кагари\" прикол",
"oskKagariDescription": "Если включено, вместо настоящего ранга оска будет рендерится :kagari:",
"stateViewTitle": "Аккаунт ${nickname} ${date}",
"statesViewTitle": "${number} состояний аккаунта ${nickname}",
"matchesViewTitle": "Матчи аккаунта ${nickname}",

View File

@ -36,21 +36,115 @@
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<style>
@font-face {
font-family: 'Eurostile Round';
src: local('Eurostile Round Medium'), local('EurostileRound-Medium'), url('https://dan63.by/static/EurostileRound-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Eurostile Round Extended';
src: local('Eurostile Round Extended Regular'), local('EurostileRoundExtended-Regular'), url('https://dan63.by/static/EurostileRoundExtended-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Eurostile Round Extended';
src: local('Eurostile Round Extended Black'), local('EurostileRoundExtended-Black'), url('https://dan63.by/static/EurostileRoundExtended-Black.ttf') format('truetype');
font-weight: 900;
font-style: normal;
}
@keyframes breathing {
from{
opacity: 1;
}
to{
opacity: 0.3;
}
}
*{
margin: 0;
padding: 0;
}
body{
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
background-color: #000;
color: #fff;
font-family: 'Eurostile Round';
font-weight: 500;
font-style: normal;
}
#preloader{
display: flex;
width: fit-content;
height: fit-content;
align-items: center;
justify-content: center;
background-color: #111;
border-radius: 24px;
padding: 8px;
background-image: linear-gradient(240deg, #0000009e, transparent);
}
.hidden{
display: none!important;
}
.title{
font-family: "Eurostile Round Extended";
font-weight: 100;
padding-bottom: 8px;
}
.subtitle{
color: grey;
}
.logo{
height: 128px;
padding-right: 8px;
border-radius: 24px;
}
#progress{
padding-top: 8px;
animation: 1s cubic-bezier(.46,.03,.52,.96) infinite alternate breathing;
}
@media (max-width: 502px){
#preloader{
flex-direction: column;
text-align: center;
}
}
</style>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<div id="preloader">
<img class="logo" src="icons/Icon-192.png" alt="Tetra Stats icon">
<div>
<h1 class="title">Tetra Stats</h1>
<p class="subtitle">Track your and other player stats in TETR.IO.<br>Made by dan63047</p>
<p id="progress">Loading...</p>
</div>
</div>
<script>
window.addEventListener('load', function(ev) {
let progress = document.querySelector("#progress");
let preloader = document.querySelector("#preloader");
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
onEntrypointLoaded: async function(engineInitializer) {
console.log(serviceWorkerVersion);
let appRunner = await engineInitializer.initializeEngine();
progress.innerHTML = "Booting...";
await appRunner.runApp();
preloader.classList.add("hidden");
}
});
});