commit
5652efa02f
|
@ -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);
|
||||
}
|
|
@ -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']);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}';
|
||||
|
|
103
lib/main.dart
103
lib/main.dart
|
@ -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");
|
||||
|
@ -86,14 +142,39 @@ void main() async {
|
|||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// Readable [a] - [b], without sign
|
||||
String readableIntDifference(int a, int b){
|
||||
int result = a - b;
|
||||
|
||||
return NumberFormat("#,###;#,###", LocaleSettings.currentLocale.languageCode).format(result);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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.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),
|
||||
subtitle: Text(t.oskKagariDescription, style: subtitleStyle),
|
||||
trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){
|
||||
prefs.setBool("oskKagariGimmick", value);
|
||||
setState(() {
|
||||
|
|
|
@ -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 SingleChildScrollView(
|
||||
child: RecentSingleplayerGames(recent: recent, hideTitle: true)
|
||||
);
|
||||
}
|
||||
|
||||
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")
|
||||
]
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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
|
||||
]
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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)))));
|
||||
}));
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: () {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)]),
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
// }
|
||||
|
||||
// }
|
|
@ -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)
|
||||
],
|
||||
|
|
|
@ -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)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
]
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}",
|
||||
|
|
102
web/index.html
102
web/index.html
|
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue