zenith recent runs + some fixes
This commit is contained in:
parent
c1561fba80
commit
31659d646d
|
@ -40,51 +40,26 @@ jobs:
|
|||
tag: Auto-${{ github.run_number }}
|
||||
body: Builded with GitHub Action workflow
|
||||
token: ${{ secrets.TOKEN }}
|
||||
# build-and-release-linux:
|
||||
# name: Build Linux App
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: subosito/flutter-action@v1
|
||||
# - uses: ashutoshvarma/setup-ninja@master
|
||||
# with:
|
||||
# channel: 'stable'
|
||||
# flutter-version: '3.16.5'
|
||||
# - name: Install project dependencies
|
||||
# run: flutter pub get
|
||||
# - name: Build artifacts
|
||||
# run: flutter build linux --release
|
||||
# - name: Archive Release
|
||||
# uses: thedoctor0/zip-release@master
|
||||
# with:
|
||||
# type: 'zip'
|
||||
# filename: TetraStats-${{github.ref_name}}-windows.zip
|
||||
# directory: build/linux/x64/runner/Release/bundle
|
||||
# - name: Push to Releases
|
||||
# uses: ncipollo/release-action@v1
|
||||
# with:
|
||||
# prerelease: true
|
||||
# allowUpdates: true
|
||||
# replacesArtifacts: false
|
||||
# discussionCategory: autobuilded-releases
|
||||
# artifacts: "build/linux/x64/runner/Release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
|
||||
# tag: Auto-${{ github.run_number }}
|
||||
# body: Builded with GitHub Action workflow
|
||||
# token: ${{ secrets.TOKEN }}
|
||||
build-and-release-android:
|
||||
name: Build Android App
|
||||
build-and-release-linux:
|
||||
name: Build Linux App
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '12.x'
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v1
|
||||
- uses: ashutoshvarma/setup-ninja@master
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.16.5'
|
||||
- run: flutter pub get
|
||||
# - run: flutter test // lmao. Tests? Who needs it?
|
||||
- run: flutter build apk --split-per-abi
|
||||
- name: Install project dependencies
|
||||
run: flutter pub get
|
||||
- name: Build artifacts
|
||||
run: flutter build linux --release
|
||||
- name: Archive Release
|
||||
uses: thedoctor0/zip-release@master
|
||||
with:
|
||||
type: 'zip'
|
||||
filename: TetraStats-${{github.ref_name}}-linux.zip
|
||||
directory: build/linux/x64/runner/Release/bundle
|
||||
- name: Push to Releases
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
|
@ -92,7 +67,32 @@ jobs:
|
|||
allowUpdates: true
|
||||
replacesArtifacts: false
|
||||
discussionCategory: autobuilded-releases
|
||||
artifacts: "build/app/outputs/flutter-apk/*"
|
||||
artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
|
||||
tag: Auto-${{ github.run_number }}
|
||||
body: Builded with GitHub Action workflow
|
||||
token: ${{ secrets.TOKEN }}
|
||||
# build-and-release-android:
|
||||
# name: Build Android App
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v1
|
||||
# - uses: actions/setup-java@v1
|
||||
# with:
|
||||
# java-version: '12.x'
|
||||
# - uses: subosito/flutter-action@v1
|
||||
# with:
|
||||
# flutter-version: '3.16.5'
|
||||
# - run: flutter pub get
|
||||
# # - run: flutter test // lmao. Tests? Who needs it?
|
||||
# - run: flutter build apk --split-per-abi
|
||||
# - name: Push to Releases
|
||||
# uses: ncipollo/release-action@v1
|
||||
# with:
|
||||
# prerelease: true
|
||||
# allowUpdates: true
|
||||
# replacesArtifacts: false
|
||||
# discussionCategory: autobuilded-releases
|
||||
# artifacts: "build/app/outputs/flutter-apk/*"
|
||||
# tag: Auto-${{ github.run_number }}
|
||||
# body: Builded with GitHub Action workflow
|
||||
# token: ${{ secrets.TOKEN }}
|
|
@ -693,6 +693,7 @@ class ResultsStats {
|
|||
double get spp => score / piecesPlaced;
|
||||
double get kps => inputs / (finalTime.inMicroseconds / 1000000);
|
||||
double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0;
|
||||
double get cps => zenith != null ? zenith!.avgrankpts / (finalTime.inMilliseconds / 1000 * 60) : 0;
|
||||
|
||||
ResultsStats(
|
||||
{
|
||||
|
@ -1399,7 +1400,7 @@ class TetraLeagueAlpha {
|
|||
}
|
||||
|
||||
class RecordSingle {
|
||||
late String userId;
|
||||
late String? userId;
|
||||
late String replayId;
|
||||
late String ownId;
|
||||
late String gamemode;
|
||||
|
@ -1408,6 +1409,7 @@ class RecordSingle {
|
|||
late int rank;
|
||||
late int countryRank;
|
||||
late AggregateStats aggregateStats;
|
||||
late RecordExtras extras;
|
||||
|
||||
RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats});
|
||||
|
||||
|
@ -1418,10 +1420,17 @@ class RecordSingle {
|
|||
stats = ResultsStats.fromJson(json['results']['stats']);
|
||||
replayId = json['replayid'];
|
||||
timestamp = DateTime.parse(json['ts']);
|
||||
userId = json['user']['id'];
|
||||
if (json['user'] != null) userId = json['user']['id'];
|
||||
rank = ran;
|
||||
countryRank = cran;
|
||||
aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']);
|
||||
var ex = json['extras'] as Map<String, dynamic>;
|
||||
switch (ex.keys.firstOrNull){
|
||||
case "zenith":
|
||||
extras = ZenithExtras.fromJson(json['extras']['zenith']);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
@ -1460,6 +1469,18 @@ class AggregateStats{
|
|||
}
|
||||
}
|
||||
|
||||
class RecordExtras{
|
||||
|
||||
}
|
||||
|
||||
class ZenithExtras extends RecordExtras{
|
||||
List<String> mods = [];
|
||||
|
||||
ZenithExtras.fromJson(Map<String, dynamic> json){
|
||||
for (var mod in json["mods"]) mods.add(mod);
|
||||
}
|
||||
}
|
||||
|
||||
class TetrioZen {
|
||||
late int level;
|
||||
late int score;
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 2
|
||||
/// Strings: 1198 (599 per locale)
|
||||
/// Strings: 1216 (608 per locale)
|
||||
///
|
||||
/// Built on 2024-07-27 at 18:54 UTC
|
||||
/// Built on 2024-07-31 at 20:51 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
|
@ -226,7 +226,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
|
|||
String get seasonStarts => 'Season starts in:';
|
||||
String get myMessadgeHeader => 'A messadge from dan63';
|
||||
String get myMessadgeBody => 'TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.';
|
||||
String preSeasonMessage({required Object n}) => 'Right now you can play unranked FT3 matches against absolutely random player.\nSeason ${n} rules applied';
|
||||
String preSeasonMessage({required Object n}) => 'Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied';
|
||||
String get nanow => 'Not avaliable for now...';
|
||||
String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}';
|
||||
String get seasonEnded => 'Season has ended';
|
||||
|
@ -242,6 +242,17 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
|
|||
String get neverPlayedTL => 'That user never played Tetra League';
|
||||
String get botTL => 'Bots are not allowed to play Tetra League';
|
||||
String get anonTL => 'Guests are not allowed to play Tetra League';
|
||||
String get quickPlay => 'Quick Play';
|
||||
String get expert => 'Expert';
|
||||
String get withMods => 'With mods';
|
||||
String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
|
||||
zero: 'with ${n} mods',
|
||||
one: 'with ${n} mod',
|
||||
two: 'with ${n} mods',
|
||||
few: 'with ${n} mods',
|
||||
many: 'with ${n} mods',
|
||||
other: 'with ${n} mods',
|
||||
);
|
||||
String get exportDB => 'Export local database';
|
||||
String get exportDBDescription => 'It contains states and Tetra League records of the tracked players and list of tracked players.';
|
||||
String get desktopExportAlertTitle => 'Desktop export';
|
||||
|
@ -929,7 +940,7 @@ class _StringsRu implements Translations {
|
|||
@override String get seasonStarts => 'Сезон начнётся через:';
|
||||
@override String get myMessadgeHeader => 'Сообщение от dan63';
|
||||
@override String get myMessadgeBody => 'TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.';
|
||||
@override String preSeasonMessage({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона';
|
||||
@override String preSeasonMessage({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона';
|
||||
@override String get nanow => 'Пока недоступно...';
|
||||
@override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}';
|
||||
@override String get seasonEnded => 'Сезон закончился';
|
||||
|
@ -945,6 +956,17 @@ class _StringsRu implements Translations {
|
|||
@override String get neverPlayedTL => 'Этот игрок никогда не играл в Тетра Лигу';
|
||||
@override String get botTL => 'Ботам нельзя играть в Тетра Лигу';
|
||||
@override String get anonTL => 'Гостям нельзя играть в Тетра Лигу';
|
||||
@override String get quickPlay => 'Быстрая Игра';
|
||||
@override String get expert => 'Эксперт';
|
||||
@override String get withMods => 'С модами';
|
||||
@override String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n,
|
||||
zero: 'с ${n} модами',
|
||||
one: 'с ${n} модом',
|
||||
two: 'с ${n} модами',
|
||||
few: 'с ${n} модами',
|
||||
many: 'с ${n} модами',
|
||||
other: 'с ${n} модами',
|
||||
);
|
||||
@override String get exportDB => 'Экспортировать локальную базу данных';
|
||||
@override String get exportDBDescription => 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.';
|
||||
@override String get desktopExportAlertTitle => 'Экспорт на десктопе';
|
||||
|
@ -1624,7 +1646,7 @@ extension on Translations {
|
|||
case 'seasonStarts': return 'Season starts in:';
|
||||
case 'myMessadgeHeader': return 'A messadge from dan63';
|
||||
case 'myMessadgeBody': return 'TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.';
|
||||
case 'preSeasonMessage': return ({required Object n}) => 'Right now you can play unranked FT3 matches against absolutely random player.\nSeason ${n} rules applied';
|
||||
case 'preSeasonMessage': return ({required Object n}) => 'Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied';
|
||||
case 'nanow': return 'Not avaliable for now...';
|
||||
case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}';
|
||||
case 'seasonEnded': return 'Season has ended';
|
||||
|
@ -1640,6 +1662,17 @@ extension on Translations {
|
|||
case 'neverPlayedTL': return 'That user never played Tetra League';
|
||||
case 'botTL': return 'Bots are not allowed to play Tetra League';
|
||||
case 'anonTL': return 'Guests are not allowed to play Tetra League';
|
||||
case 'quickPlay': return 'Quick Play';
|
||||
case 'expert': return 'Expert';
|
||||
case 'withMods': return 'With mods';
|
||||
case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
|
||||
zero: 'with ${n} mods',
|
||||
one: 'with ${n} mod',
|
||||
two: 'with ${n} mods',
|
||||
few: 'with ${n} mods',
|
||||
many: 'with ${n} mods',
|
||||
other: 'with ${n} mods',
|
||||
);
|
||||
case 'exportDB': return 'Export local database';
|
||||
case 'exportDBDescription': return 'It contains states and Tetra League records of the tracked players and list of tracked players.';
|
||||
case 'desktopExportAlertTitle': return 'Desktop export';
|
||||
|
@ -2243,7 +2276,7 @@ extension on _StringsRu {
|
|||
case 'seasonStarts': return 'Сезон начнётся через:';
|
||||
case 'myMessadgeHeader': return 'Сообщение от dan63';
|
||||
case 'myMessadgeBody': return 'TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.';
|
||||
case 'preSeasonMessage': return ({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона';
|
||||
case 'preSeasonMessage': return ({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона';
|
||||
case 'nanow': return 'Пока недоступно...';
|
||||
case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}';
|
||||
case 'seasonEnded': return 'Сезон закончился';
|
||||
|
@ -2259,6 +2292,17 @@ extension on _StringsRu {
|
|||
case 'neverPlayedTL': return 'Этот игрок никогда не играл в Тетра Лигу';
|
||||
case 'botTL': return 'Ботам нельзя играть в Тетра Лигу';
|
||||
case 'anonTL': return 'Гостям нельзя играть в Тетра Лигу';
|
||||
case 'quickPlay': return 'Быстрая Игра';
|
||||
case 'expert': return 'Эксперт';
|
||||
case 'withMods': return 'С модами';
|
||||
case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n,
|
||||
zero: 'с ${n} модами',
|
||||
one: 'с ${n} модом',
|
||||
two: 'с ${n} модами',
|
||||
few: 'с ${n} модами',
|
||||
many: 'с ${n} модами',
|
||||
other: 'с ${n} модами',
|
||||
);
|
||||
case 'exportDB': return 'Экспортировать локальную базу данных';
|
||||
case 'exportDBDescription': return 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.';
|
||||
case 'desktopExportAlertTitle': return 'Экспорт на десктопе';
|
||||
|
|
|
@ -90,8 +90,6 @@ class CacheController {
|
|||
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:
|
||||
|
@ -99,8 +97,8 @@ class CacheController {
|
|||
}
|
||||
}
|
||||
|
||||
void store(dynamic object, int? cachedUntil) async {
|
||||
String key = _getObjectId(object) + cachedUntil!.toString();
|
||||
void store(dynamic object, int cachedUntil) async {
|
||||
String key = _getObjectId(object) + cachedUntil.toString();
|
||||
_cache[key] = object;
|
||||
}
|
||||
|
||||
|
@ -113,6 +111,8 @@ class CacheController {
|
|||
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;
|
||||
case SingleplayerStream:
|
||||
objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(id));
|
||||
default:
|
||||
objectEntry = _cache.entries.firstWhere((element) => element.key.startsWith(datatype.toString()+id));
|
||||
id = datatype.toString()+id;
|
||||
|
@ -309,15 +309,15 @@ 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<SingleplayerStream> fetchSingleplayerStream(String userID, String stream) async {
|
||||
SingleplayerStream? cached = _cache.get(userID, SingleplayerStream);
|
||||
Future<SingleplayerStream> fetchStream(String userID, String stream) async {
|
||||
SingleplayerStream? cached = _cache.get(stream+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()}');
|
||||
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/$stream');
|
||||
}
|
||||
try {
|
||||
final response = await client.get(url);
|
||||
|
@ -325,7 +325,7 @@ class TetrioService extends DB {
|
|||
switch (response.statusCode) {
|
||||
case 200:
|
||||
if (jsonDecode(response.body)['success']) {
|
||||
SingleplayerStream records = SingleplayerStream.fromJson(jsonDecode(response.body)['data']['records'], userID, stream);
|
||||
SingleplayerStream records = SingleplayerStream.fromJson(jsonDecode(response.body)['data']['entries'], 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;
|
||||
|
@ -709,7 +709,7 @@ class TetrioService extends DB {
|
|||
|
||||
/// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve.
|
||||
Future<News> fetchNews(String userID) async{
|
||||
News? cached = _cache.get(userID, News);
|
||||
News? cached = _cache.get("user_$userID", News);
|
||||
if (cached != null) return cached;
|
||||
|
||||
Uri url;
|
||||
|
@ -757,7 +757,7 @@ 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<TetraLeagueBetaStream> fetchTLStream(String userID) async {
|
||||
TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueAlphaStream);
|
||||
TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream);
|
||||
if (cached != null) return cached;
|
||||
|
||||
Uri url;
|
||||
|
@ -957,7 +957,9 @@ class TetrioService extends DB {
|
|||
case 200:
|
||||
if (jsonDecode(response.body)['success']) {
|
||||
developer.log("fetchSummaries: $id summaries retrieved and cached", name: "services/tetrio_crud");
|
||||
return Summaries.fromJson(jsonDecode(response.body)['data'], id);
|
||||
Summaries summaries = Summaries.fromJson(jsonDecode(response.body)['data'], id);
|
||||
_cache.store(summaries, jsonDecode(response.body)['cache']['cached_until']);
|
||||
return summaries;
|
||||
} else {
|
||||
developer.log("fetchSummaries: User dosen't exist", name: "services/tetrio_crud", error: response.body);
|
||||
throw TetrioPlayerNotExist();
|
||||
|
@ -1183,6 +1185,8 @@ class TetrioService extends DB {
|
|||
}
|
||||
case 403:
|
||||
throw TetrioForbidden();
|
||||
case 404:
|
||||
throw TetrioPlayerNotExist();
|
||||
case 429:
|
||||
throw TetrioTooManyRequests();
|
||||
case 418:
|
||||
|
|
|
@ -23,6 +23,7 @@ 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/views/tl_match_view.dart' show TlMatchResultView;
|
||||
import 'package:tetra_stats/views/zenith_record_view.dart';
|
||||
import 'package:tetra_stats/widgets/finesse_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/gauget_num.dart';
|
||||
import 'package:tetra_stats/widgets/graphs.dart';
|
||||
|
@ -36,6 +37,7 @@ 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:tetra_stats/widgets/zenith_thingy.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
@ -82,6 +84,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
bool _TLHistoryWasFetched = false;
|
||||
late TabController _tabController;
|
||||
late TabController _wideScreenTabController;
|
||||
bool zenithEX = false;
|
||||
|
||||
String get title => "Tetra Stats: $_titleNickname";
|
||||
|
||||
|
@ -89,8 +92,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
void initState() {
|
||||
initDB();
|
||||
_scrollController = ScrollController();
|
||||
_tabController = TabController(length: 7, vsync: this);
|
||||
_wideScreenTabController = TabController(length: 4, vsync: this);
|
||||
_tabController = TabController(length: 9, vsync: this);
|
||||
_wideScreenTabController = TabController(length: 5, vsync: this);
|
||||
_zoomPanBehavior = ZoomPanBehavior(
|
||||
enablePinching: true,
|
||||
enableSelectionZooming: true,
|
||||
|
@ -160,29 +163,33 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
late List<dynamic> requests;
|
||||
late Summaries summaries;
|
||||
late TetraLeagueBetaStream tlStream;
|
||||
late UserRecords records;
|
||||
late News news;
|
||||
late SingleplayerStream recent;
|
||||
late SingleplayerStream sprint;
|
||||
late SingleplayerStream blitz;
|
||||
late TetrioPlayerFromLeaderboard? topOne;
|
||||
late TopTr? topTR;
|
||||
requests = await Future.wait([ // all at once (7 requests to oskware lmao)
|
||||
// late SingleplayerStream recentSprint;
|
||||
// late SingleplayerStream recentBlitz;
|
||||
// late SingleplayerStream sprint;
|
||||
// late SingleplayerStream blitz;
|
||||
late SingleplayerStream recentZenith;
|
||||
late SingleplayerStream recentZenithEX;
|
||||
// late TetrioPlayerFromLeaderboard? topOne;
|
||||
// late TopTr? topTR;
|
||||
requests = await Future.wait([ // all at once (8 requests to oskware in total)
|
||||
teto.fetchSummaries(_searchFor),
|
||||
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),
|
||||
//(me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR
|
||||
teto.fetchStream(_searchFor, "zenith/recent"),
|
||||
teto.fetchStream(_searchFor, "zenithex/recent"),
|
||||
//teto.fetchStream(_searchFor, "40l/top"),
|
||||
//teto.fetchStream(_searchFor, "blitz/top"),
|
||||
]);
|
||||
//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),
|
||||
//(me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR
|
||||
summaries = requests[0] as Summaries;
|
||||
tlStream = requests[1] as TetraLeagueBetaStream;
|
||||
// records = requests[1] as UserRecords;
|
||||
news = requests[2] as News;
|
||||
recentZenith = requests[3] as SingleplayerStream;
|
||||
recentZenithEX = requests[4] as SingleplayerStream;
|
||||
// recent = requests[3] as SingleplayerStream;
|
||||
// sprint = requests[4] as SingleplayerStream;
|
||||
// blitz = requests[5] as SingleplayerStream;
|
||||
|
@ -194,7 +201,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
// Get tetra League leaderboard
|
||||
everyone = teto.getCachedLeaderboard();
|
||||
everyone ??= await teto.fetchTLLeaderboard();
|
||||
if (meAmongEveryone == null){
|
||||
if (meAmongEveryone == null && everyone!.leaderboard.isNotEmpty){
|
||||
meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me);
|
||||
if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!);
|
||||
}
|
||||
|
@ -308,7 +315,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
changePlayer(me.userId);
|
||||
});
|
||||
}
|
||||
return [me, summaries, news, tlStream];
|
||||
return [me, summaries, news, tlStream, recentZenith, recentZenithEX];
|
||||
//return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp];
|
||||
}
|
||||
|
||||
|
@ -317,6 +324,10 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
void toggleZenith(){
|
||||
setState(() {zenithEX = !zenithEX;});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
|
@ -433,14 +444,15 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
tabs: bigScreen ? [
|
||||
Tab(text: t.tetraLeague,),
|
||||
Tab(text: t.history),
|
||||
Tab(text: "Quick Play"),
|
||||
Tab(text: t.quickPlay),
|
||||
Tab(text: "${t.sprint} & ${t.blitz}"),
|
||||
Tab(text: t.other),
|
||||
] : [
|
||||
Tab(text: t.tetraLeague),
|
||||
Tab(text: t.tlRecords),
|
||||
Tab(text: t.history),
|
||||
Tab(text: "Quick Play"),
|
||||
Tab(text: t.quickPlay),
|
||||
Tab(text: "${t.quickPlay} ${t.recent}"),
|
||||
Tab(text: t.sprint),
|
||||
Tab(text: t.blitz),
|
||||
Tab(text: t.recentRuns),
|
||||
|
@ -483,7 +495,20 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
),
|
||||
],),
|
||||
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0),
|
||||
_ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width-450,
|
||||
constraints: const BoxConstraints(maxWidth: 1024),
|
||||
child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)
|
||||
),
|
||||
SizedBox(
|
||||
width: 450.0,
|
||||
child: _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true),
|
||||
)
|
||||
],
|
||||
),
|
||||
_TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, recent: SingleplayerStream(userId: "userId", records: [], type: "recent"), sprintStream: SingleplayerStream(userId: "userId", records: [], type: "40l"), blitzStream: SingleplayerStream(userId: "userId", records: [], type: "blitz")),
|
||||
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![2])
|
||||
] : [
|
||||
|
@ -506,7 +531,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
|
|||
),
|
||||
_TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true),
|
||||
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0),
|
||||
_ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx),
|
||||
ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX),
|
||||
_ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true),
|
||||
SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")),
|
||||
SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "Blitz")),
|
||||
_RecentSingleplayersThingy(SingleplayerStream(userId: "userId", records: [], type: "recent")),
|
||||
|
@ -768,6 +794,63 @@ class _TLRecords extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _ZenithRecords extends StatelessWidget {
|
||||
final String userID;
|
||||
final SingleplayerStream data;
|
||||
final bool separateScrollController;
|
||||
|
||||
/// Widget, that displays Quick Play records.
|
||||
/// Accepts list of TL records ([data]) and [userID] of player from the view
|
||||
const _ZenithRecords({required this.userID, required this.data, this.separateScrollController = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (data.records.isEmpty) {
|
||||
return Center(child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)),
|
||||
],
|
||||
));
|
||||
}
|
||||
bool bigScreen = MediaQuery.of(context).size.width >= 768;
|
||||
int length = data.records.length;
|
||||
return ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: separateScrollController ? ScrollController() : null,
|
||||
itemCount: length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == length) {
|
||||
return Center(child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)),
|
||||
],
|
||||
));
|
||||
}
|
||||
const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100, fontSize: 13);
|
||||
return Container(
|
||||
child: ListTile(
|
||||
leading: Text("QP",
|
||||
style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)),
|
||||
title: Text("${f2.format(data.records[index].stats.zenith!.altitude)} m${(data.records[index].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (data.records[index].extras as ZenithExtras).mods.length)})" : ""}"),
|
||||
subtitle: Text(timestamp(data.records[index].timestamp), style: const TextStyle(color: Colors.grey)),
|
||||
trailing: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text("${f2.format(data.records[index].aggregateStats.apm)} APM, ${f2.format(data.records[index].aggregateStats.pps)} PPS", style: style, textAlign: TextAlign.right),
|
||||
Text("${f2.format(data.records[index].stats.cps)} CSP (${f2.format(data.records[index].stats.zenith!.peakrank)} peak)", style: style, textAlign: TextAlign.right),
|
||||
Text("${data.records[index].stats.kills} KO's, ${getMoreNormalTime(data.records[index].stats.finalTime)}", style: style, textAlign: TextAlign.right)
|
||||
],
|
||||
),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => ZenithRecordView(record: data.records[index]))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _History extends StatelessWidget{
|
||||
final List<DropdownMenuItem<List<_HistoryChartSpot>>> chartsData;
|
||||
final String userID;
|
||||
|
@ -1227,230 +1310,6 @@ class _RecentSingleplayersThingy extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _ZenithThingy extends StatefulWidget{
|
||||
final RecordSingle? record;
|
||||
final RecordSingle? recordEX;
|
||||
|
||||
_ZenithThingy({this.record, this.recordEX});
|
||||
|
||||
@override
|
||||
State<_ZenithThingy> createState() => _ZenithThingyState();
|
||||
}
|
||||
|
||||
class _ZenithThingyState extends State<_ZenithThingy> {
|
||||
late RecordSingle? record;
|
||||
bool ex = false;
|
||||
|
||||
@override
|
||||
void initState(){
|
||||
super.initState();
|
||||
record = ex ? widget.recordEX : widget.record;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints){
|
||||
bool bigScreen = constraints.maxWidth > 768;
|
||||
if (record == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("Quick Play${ex ? " Expert" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
|
||||
RichText(text: TextSpan(
|
||||
text: "--- m",
|
||||
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
TextButton(onPressed: (){
|
||||
if (ex){
|
||||
ex = false;
|
||||
}else{
|
||||
ex = true;
|
||||
}
|
||||
setState(() {
|
||||
record = ex ? widget.recordEX : widget.record;
|
||||
});
|
||||
}, child: Text(ex ? "Switch to normal" : "Switch to Expert")),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Padding(padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("Quick Play${ex ? " Expert" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
|
||||
RichText(text: TextSpan(
|
||||
text: "${f2.format(record!.stats.zenith!.altitude)} m",
|
||||
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!.rank != -1) TextSpan(text: "№${record!.rank}"),
|
||||
if (record!.rank != -1) const TextSpan(text: " • "),
|
||||
if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"),
|
||||
if (record!.countryRank != -1) const TextSpan(text: " • "),
|
||||
TextSpan(text: timestamp(widget.record!.timestamp)),
|
||||
]
|
||||
),
|
||||
),
|
||||
TextButton(onPressed: (){
|
||||
if (ex){
|
||||
ex = false;
|
||||
}else{
|
||||
ex = true;
|
||||
}
|
||||
setState(() {
|
||||
record = ex ? widget.recordEX : widget.record;
|
||||
});
|
||||
}, child: Text(ex ? "Switch to normal" : "Switch to Expert")),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 20,
|
||||
children: [
|
||||
StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
|
||||
StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true),
|
||||
StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true)
|
||||
],
|
||||
),
|
||||
FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage),
|
||||
LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center),
|
||||
Table(
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
Text("Floor"),
|
||||
Text("Split"),
|
||||
Text("Total"),
|
||||
]
|
||||
),
|
||||
for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow(
|
||||
children: [
|
||||
Text((i+1).toString()),
|
||||
Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---"),
|
||||
Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---"),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 40, 0, 0),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 35,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [
|
||||
GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red),
|
||||
GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow),
|
||||
GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green),
|
||||
GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue),
|
||||
GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple),
|
||||
], alertWidgets: [
|
||||
Text(t.statCellNum.appDescription),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}")
|
||||
]),
|
||||
GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [
|
||||
GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green),
|
||||
GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue),
|
||||
GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple),
|
||||
], alertWidgets: [
|
||||
Text(t.statCellNum.vsapmDescription),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}")
|
||||
])
|
||||
]),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 25,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss,
|
||||
alertWidgets: [Text(t.statCellNum.dssDescription),
|
||||
Text("${t.formula}: (VS / 100) - (APM / 60)"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true,),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp,
|
||||
alertWidgets: [Text(t.statCellNum.dspDescription),
|
||||
Text("${t.formula}: DS/S / PPS"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp,
|
||||
alertWidgets: [Text(t.statCellNum.appdspDescription),
|
||||
Text("${t.formula}: APP + DS/P"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese,
|
||||
alertWidgets: [Text(t.statCellNum.cheeseDescription),
|
||||
Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: false),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe,
|
||||
alertWidgets: [Text(t.statCellNum.gbeDescription),
|
||||
Text("${t.formula}: APP * DS/P * 2"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp,
|
||||
alertWidgets: [Text(t.statCellNum.nyaappDescription),
|
||||
Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area,
|
||||
alertWidgets: [Text(t.statCellNum.areaDescription),
|
||||
Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true)
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _OtherThingy extends StatelessWidget {
|
||||
final TetrioZen? zen;
|
||||
final String? bio;
|
||||
|
|
|
@ -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/text_timestamp.dart';
|
||||
import 'package:tetra_stats/widgets/zenith_thingy.dart';
|
||||
|
||||
class ZenithRecordView extends StatelessWidget {
|
||||
final RecordSingle record;
|
||||
|
||||
const ZenithRecordView({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.gamemode){
|
||||
"zenith" => "Quick Play",
|
||||
"zenithex" => "Quick Play Expert",
|
||||
String() => "5000000 Blast",
|
||||
}
|
||||
} ${timestamp(record.timestamp)}"),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
ZenithThingy(record: record, switchable: false),
|
||||
// TODO: Insert replay link here
|
||||
]
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,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/utils/colors_functions.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';
|
||||
|
@ -22,15 +23,6 @@ class SingleplayerRecord extends StatelessWidget {
|
|||
/// 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)));
|
||||
|
@ -94,8 +86,8 @@ class SingleplayerRecord extends StatelessWidget {
|
|||
else if (record!.gamemode == "blitz" && (rank == null || rank == "z")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.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: " • "),
|
||||
if (record!.rank != -1) TextSpan(text: "№${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank))),
|
||||
if (record!.rank != -1) const TextSpan(text: " • "),
|
||||
TextSpan(text: timestamp(record!.timestamp)),
|
||||
]
|
||||
),
|
||||
|
|
|
@ -52,7 +52,7 @@ class StatCellNum extends StatelessWidget {
|
|||
RichText(
|
||||
text: TextSpan(text: splited[0],
|
||||
children: [
|
||||
if ((fractionDigits??0) > 0) TextSpan(text: f.symbols.DECIMAL_SEP+splited[1], style: smallDecimal ? const TextStyle(fontFamily: "Eurostile Round", fontSize: 16) : null)
|
||||
if ((fractionDigits??0) > 0 && splited.elementAtOrNull(1) != null) 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,259 @@
|
|||
import 'package:flutter/material.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/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/utils/relative_timestamps.dart';
|
||||
import 'package:tetra_stats/widgets/finesse_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/gauget_num.dart';
|
||||
import 'package:tetra_stats/widgets/graphs.dart';
|
||||
import 'package:tetra_stats/widgets/lineclears_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/stat_sell_num.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class ZenithThingy extends StatefulWidget{
|
||||
final RecordSingle? record;
|
||||
final bool switchable;
|
||||
final bool initEXvalue;
|
||||
final RecordSingle? recordEX;
|
||||
final Function? parentZenithToggle;
|
||||
|
||||
ZenithThingy({this.record, this.recordEX, this.switchable = true, this.parentZenithToggle, this.initEXvalue = false});
|
||||
|
||||
@override
|
||||
State<ZenithThingy> createState() => _ZenithThingyState();
|
||||
}
|
||||
|
||||
class _ZenithThingyState extends State<ZenithThingy> {
|
||||
late RecordSingle? record;
|
||||
bool ex = false;
|
||||
|
||||
@override
|
||||
void initState(){
|
||||
ex = widget.initEXvalue;
|
||||
|
||||
super.initState();
|
||||
if (widget.switchable){
|
||||
record = (ex ? widget.recordEX : widget.record);
|
||||
}else{
|
||||
record = widget.record;
|
||||
ex = widget.record!.gamemode == "zenithex";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints){
|
||||
bool bigScreen = constraints.maxWidth > 768;
|
||||
if (record == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
|
||||
RichText(text: TextSpan(
|
||||
text: "--- m",
|
||||
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
TextButton(onPressed: (){
|
||||
if (ex){
|
||||
ex = false;
|
||||
}else{
|
||||
ex = true;
|
||||
}
|
||||
setState(() {
|
||||
if (widget.parentZenithToggle != null) widget.parentZenithToggle!();
|
||||
record = ex ? widget.recordEX : widget.record;
|
||||
});
|
||||
}, child: Text(ex ? "Switch to normal" : "Switch to Expert")),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Padding(padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)),
|
||||
RichText(text: TextSpan(
|
||||
text: "${f2.format(record!.stats.zenith!.altitude)} m",
|
||||
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
),
|
||||
),
|
||||
if ((record!.extras as ZenithExtras).mods.isNotEmpty) RichText(
|
||||
text: TextSpan(
|
||||
text: "",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.white),
|
||||
children: [
|
||||
TextSpan(text: "${t.withMods}: "),
|
||||
for (String mod in (record!.extras as ZenithExtras).mods) TextSpan(text: "${mod.toUpperCase()} "),
|
||||
]
|
||||
),
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: "",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
|
||||
children: [
|
||||
if (record!.rank != -1) TextSpan(text: "№${record!.rank}"),
|
||||
if (record!.rank != -1) const TextSpan(text: " • "),
|
||||
if (record!.countryRank != -1) TextSpan(text: "№${record!.countryRank} local"),
|
||||
if (record!.countryRank != -1) const TextSpan(text: " • "),
|
||||
TextSpan(text: timestamp(widget.record!.timestamp)),
|
||||
]
|
||||
),
|
||||
),
|
||||
if (widget.switchable) TextButton(onPressed: (){
|
||||
if (ex){
|
||||
ex = false;
|
||||
}else{
|
||||
ex = true;
|
||||
}
|
||||
setState(() {
|
||||
if (widget.parentZenithToggle != null) widget.parentZenithToggle!();
|
||||
record = ex ? widget.recordEX : widget.record;
|
||||
});
|
||||
}, child: Text(ex ? "Switch to normal" : "Switch to Expert")),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 20,
|
||||
children: [
|
||||
StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false),
|
||||
StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true),
|
||||
StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "Kills", isScreenBig: bigScreen, higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "CPS\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true)
|
||||
],
|
||||
),
|
||||
FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage),
|
||||
LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Total time: ${getMoreNormalTime(record!.stats.finalTime)}", style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center),
|
||||
Table(
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
Text("Floor"),
|
||||
Text("Split"),
|
||||
Text("Total"),
|
||||
]
|
||||
),
|
||||
for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow(
|
||||
children: [
|
||||
Text((i+1).toString()),
|
||||
Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---"),
|
||||
Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---"),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 40, 0, 0),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 35,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [
|
||||
GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red),
|
||||
GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow),
|
||||
GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green),
|
||||
GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue),
|
||||
GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple),
|
||||
], alertWidgets: [
|
||||
Text(t.statCellNum.appDescription),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}")
|
||||
]),
|
||||
GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [
|
||||
GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green),
|
||||
GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue),
|
||||
GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple),
|
||||
], alertWidgets: [
|
||||
Text(t.statCellNum.vsapmDescription),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}")
|
||||
])
|
||||
]),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 25,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss,
|
||||
alertWidgets: [Text(t.statCellNum.dssDescription),
|
||||
Text("${t.formula}: (VS / 100) - (APM / 60)"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true,),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp,
|
||||
alertWidgets: [Text(t.statCellNum.dspDescription),
|
||||
Text("${t.formula}: DS/S / PPS"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp,
|
||||
alertWidgets: [Text(t.statCellNum.appdspDescription),
|
||||
Text("${t.formula}: APP + DS/P"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese,
|
||||
alertWidgets: [Text(t.statCellNum.cheeseDescription),
|
||||
Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: false),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe,
|
||||
alertWidgets: [Text(t.statCellNum.gbeDescription),
|
||||
Text("${t.formula}: APP * DS/P * 2"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp,
|
||||
alertWidgets: [Text(t.statCellNum.nyaappDescription),
|
||||
Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true),
|
||||
StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area,
|
||||
alertWidgets: [Text(t.statCellNum.areaDescription),
|
||||
Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"),
|
||||
Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),],
|
||||
okText: t.popupActions.ok,
|
||||
higherIsBetter: true)
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -91,7 +91,7 @@
|
|||
"seasonStarts": "Season starts in:",
|
||||
"myMessadgeHeader": "A messadge from dan63",
|
||||
"myMessadgeBody": "TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.",
|
||||
"preSeasonMessage": "Right now you can play unranked FT3 matches against absolutely random player.\nSeason ${n} rules applied",
|
||||
"preSeasonMessage": "Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied",
|
||||
"nanow": "Not avaliable for now...",
|
||||
"seasonEnds": "Season ends in ${countdown}",
|
||||
"seasonEnded": "Season has ended",
|
||||
|
@ -107,6 +107,17 @@
|
|||
"neverPlayedTL": "That user never played Tetra League",
|
||||
"botTL": "Bots are not allowed to play Tetra League",
|
||||
"anonTL": "Guests are not allowed to play Tetra League",
|
||||
"quickPlay": "Quick Play",
|
||||
"expert": "Expert",
|
||||
"withMods": "With mods",
|
||||
"withModsPlural":{
|
||||
"zero": "with $n mods",
|
||||
"one": "with $n mod",
|
||||
"two": "with $n mods",
|
||||
"few": "with $n mods",
|
||||
"many": "with $n mods",
|
||||
"other": "with $n mods"
|
||||
},
|
||||
"exportDB": "Export local database",
|
||||
"exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.",
|
||||
"desktopExportAlertTitle": "Desktop export",
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
"seasonStarts": "Сезон начнётся через:",
|
||||
"myMessadgeHeader": "Сообщение от dan63",
|
||||
"myMessadgeBody": "TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.",
|
||||
"preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед против абсолютно рандомного по скиллу игрока.\nПрименяются правила ${n} сезона",
|
||||
"preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона",
|
||||
"nanow": "Пока недоступно...",
|
||||
"seasonEnds": "Сезон закончится через ${countdown}",
|
||||
"seasonEnded": "Сезон закончился",
|
||||
|
@ -107,6 +107,17 @@
|
|||
"neverPlayedTL": "Этот игрок никогда не играл в Тетра Лигу",
|
||||
"botTL": "Ботам нельзя играть в Тетра Лигу",
|
||||
"anonTL": "Гостям нельзя играть в Тетра Лигу",
|
||||
"quickPlay": "Быстрая Игра",
|
||||
"expert": "Эксперт",
|
||||
"withMods": "С модами",
|
||||
"withModsPlural":{
|
||||
"zero": "с $n модами",
|
||||
"one": "с $n модом",
|
||||
"two": "с $n модами",
|
||||
"few": "с $n модами",
|
||||
"many": "с $n модами",
|
||||
"other": "с $n модами"
|
||||
},
|
||||
"exportDB": "Экспортировать локальную базу данных",
|
||||
"exportDBDescription": "Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.",
|
||||
"desktopExportAlertTitle": "Экспорт на десктопе",
|
||||
|
|
Loading…
Reference in New Issue