A lot of things, 2.0.3 is ready to release

- Can fetch S2 history
- New averages
- Damage calculator fix
- Icon for linux runner
- We can build .deb now
This commit is contained in:
dan63047 2025-01-18 01:00:46 +03:00
parent 0ee2b83bd7
commit ee3bbe6369
25 changed files with 273 additions and 161 deletions

View File

@ -62,6 +62,8 @@ jobs:
type: 'zip'
filename: TetraStats-${{github.ref_name}}-linux.zip
directory: build/linux/x64/release/bundle
- name: Build .deb package
run: dart run flutter_to_debian
- name: Push to Releases
uses: ncipollo/release-action@v1
with:
@ -69,7 +71,7 @@ jobs:
allowUpdates: true
replacesArtifacts: false
discussionCategory: autobuilded-releases
artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip,build/linux/x64/release/debian/*"
tag: Auto-${{ github.run_number }}
body: Build with GitHub Action workflow
token: ${{ secrets.TOKEN }}

View File

@ -1,11 +1,11 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
# This file should be version controlled and should not be manually edited.
version:
revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
channel: stable
revision: "b0850beeb25f6d5b10426284f506557f66181b36"
channel: "stable"
project_type: app
@ -13,26 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: android
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: ios
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: linux
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: macos
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: web
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: windows
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
# User provided section

View File

@ -0,0 +1,5 @@
package com.dan63.tetra_stats
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

3
debian/debian.yaml vendored
View File

@ -2,10 +2,11 @@ flutter_app:
command: tetra_stats
arch: x64
parent: /usr/local/lib
nonInteractive: true
control:
Package: tetra-stats
Version: 0.2.0
Version: 2.0.3
Architecture: amd64
Essential: no
Priority: optional

View File

@ -1,5 +1,5 @@
[Desktop Entry]
Version=0.2.0
Version=2.0.3
Name=Tetra Stats
GenericName=Tetra Stats
Comment=Track your and other player stats in TETR.IO

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
const int currentSeason = 2;
final DateTime sprintAndBlitzRelevance = DateTime(2024, 8, 25);
final DateTime sprintAndBlitzRelevance = DateTime(2025, 1, 16);
const double noTrRd = 60.9;
const double apmWeight = 1;
const double ppsWeight = 45;
@ -210,46 +210,46 @@ const List<Color> achievementColors = [
];
const Map<String, Duration> sprintAverages = {
// based on https://discord.com/channels/673303546107658242/674421736162197515/1277367281264889908
'x+': Duration(seconds: 18, milliseconds: 867),
'x': Duration(seconds: 23, milliseconds: 277),
'u': Duration(seconds: 28, milliseconds: 853),
'ss': Duration(seconds: 35, milliseconds: 173),
's+': Duration(seconds: 39, milliseconds: 028),
's': Duration(seconds: 45, milliseconds: 807),
's-': Duration(seconds: 48, milliseconds: 840),
'a+': Duration(seconds: 54, milliseconds: 975),
'a': Duration(seconds: 60, milliseconds: 287),
'a-': Duration(seconds: 64, milliseconds: 019),
'b+': Duration(seconds: 76, milliseconds: 531),
'b': Duration(seconds: 77, milliseconds: 635),
'b-': Duration(seconds: 92, milliseconds: 279),
'c+': Duration(seconds: 97, milliseconds: 911),
'c': Duration(seconds: 104, milliseconds: 700),
'c-': Duration(seconds: 115, milliseconds: 173),
'd+': Duration(seconds: 131, milliseconds: 486),
'd': Duration(seconds: 158, milliseconds: 397),
// based on https://discord.com/channels/673303546107658242/1260605501754839060/1329448681539244094
'x+': Duration(seconds: 19, milliseconds: 223),
'x': Duration(seconds: 24, milliseconds: 832),
'u': Duration(seconds: 32, milliseconds: 586),
'ss': Duration(seconds: 40, milliseconds: 011),
's+': Duration(seconds: 47, milliseconds: 963),
's': Duration(seconds: 54, milliseconds: 413),
's-': Duration(seconds: 61, milliseconds: 740),
'a+': Duration(seconds: 70, milliseconds: 101),
'a': Duration(seconds: 73, milliseconds: 294),
'a-': Duration(seconds: 81, milliseconds: 773),
'b+': Duration(seconds: 88, milliseconds: 647),
'b': Duration(seconds: 97, milliseconds: 699),
'b-': Duration(seconds: 105, milliseconds: 721),
'c+': Duration(seconds: 113, milliseconds: 229),
'c': Duration(seconds: 124, milliseconds: 740),
'c-': Duration(seconds: 129, milliseconds: 382),
'd+': Duration(seconds: 138, milliseconds: 947),
'd': Duration(seconds: 155, milliseconds: 190),
};
const Map<String, int> blitzAverages = {
'x+': 879378,
'x': 677479,
'u': 485962,
'ss': 369043,
's+': 279242,
's': 245619,
's-': 199368,
'a+': 162035,
'a': 130949,
'a-': 111505,
'b+': 97251,
'b': 83580,
'b-': 70511,
'c+': 56747,
'c': 43002,
'c-': 38925,
'd+': 30483,
'd': 22513,
'x+': 886046,
'x': 631014,
'u': 428799,
'ss': 296430,
's+': 212237,
's': 157234,
's-': 122791,
'a+': 103031,
'a': 90174,
'a-': 73474,
'b+': 60655,
'b': 52463,
'b-': 43877,
'c+': 36594,
'c': 34014,
'c-': 29613,
'd+': 31521,
'd': 23437,
};
List<DateTime> seasonStarts = [

View File

@ -6,7 +6,7 @@
/// Locales: 3
/// Strings: 2295 (765 per locale)
///
/// Built on 2024-12-31 at 17:29 UTC
/// Built on 2025-01-14 at 21:20 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
@ -707,7 +707,7 @@ class _StringsGraphsDestinationEn {
final Translations _root; // ignore: unused_field
// Translations
String get fetchAndsaveTLHistory => 'Get player history';
String get fetchAndsaveTLHistory => 'Fetch History';
String get fetchAndSaveOldTLmatches => 'Get Tetra League matches history';
String fetchAndsaveTLHistoryResult({required Object number}) => '${number} states was found';
String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} matches was found';
@ -2247,7 +2247,7 @@ class _StringsGraphsDestinationRuRu implements _StringsGraphsDestinationEn {
@override final _StringsRuRu _root; // ignore: unused_field
// Translations
@override String get fetchAndsaveTLHistory => 'Получить историю игрока';
@override String get fetchAndsaveTLHistory => 'Получить историю';
@override String get fetchAndSaveOldTLmatches => 'Получить историю матчей Тетра Лиги';
@override String fetchAndsaveTLHistoryResult({required Object number}) => '${number} состояний было найдено';
@override String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} матчей было найдено';
@ -4925,7 +4925,7 @@ extension on Translations {
case 'actions.ok': return 'OK';
case 'actions.apply': return 'Apply';
case 'actions.refresh': return 'Refresh';
case 'graphsDestination.fetchAndsaveTLHistory': return 'Get player history';
case 'graphsDestination.fetchAndsaveTLHistory': return 'Fetch History';
case 'graphsDestination.fetchAndSaveOldTLmatches': return 'Get Tetra League matches history';
case 'graphsDestination.fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} states was found';
case 'graphsDestination.fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} matches was found';
@ -5739,7 +5739,7 @@ extension on _StringsRuRu {
case 'actions.ok': return 'ОК';
case 'actions.apply': return 'Применить';
case 'actions.refresh': return 'Обновить';
case 'graphsDestination.fetchAndsaveTLHistory': return 'Получить историю игрока';
case 'graphsDestination.fetchAndsaveTLHistory': return 'Получить историю';
case 'graphsDestination.fetchAndSaveOldTLmatches': return 'Получить историю матчей Тетра Лиги';
case 'graphsDestination.fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} состояний было найдено';
case 'graphsDestination.fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} матчей было найдено';

View File

@ -88,7 +88,7 @@ class DB {
}
Future<bool> checkImportingDB(File db) async {
final newDB = await openDatabase(db.path);
final newDB = await openDatabase(db.path); // TODO: Maybe i should use arguments, that this method provides?
var usersTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioUsersTable}`);");
List<String> usersTableRows = [for (Map<String, Object?> row in usersTable) row["name"] as String];
if (!listEquals(usersTableRows, tetrioUsersTableRows)) return false;

View File

@ -4,9 +4,11 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:io';
import 'dart:math';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:tetra_stats/data_objects/beta_record.dart';
import 'package:tetra_stats/data_objects/cutoff_tetrio.dart';
import 'package:tetra_stats/data_objects/end_context_multi.dart';
import 'package:tetra_stats/data_objects/news.dart';
@ -24,8 +26,6 @@ import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
import 'package:tetra_stats/data_objects/tetrio_player.dart';
import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart';
import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart';
import 'package:tetra_stats/data_objects/tetrio_zen.dart';
import 'package:tetra_stats/data_objects/user_records.dart';
import 'package:tetra_stats/main.dart' show packageInfo;
import 'package:flutter/foundation.dart';
import 'package:tetra_stats/services/custom_http_client.dart';
@ -372,7 +372,7 @@ class TetrioService extends DB {
dbPath = join(docsPath.path, dbName);
}
var dbFile = File(dbPath);
var dbSize = (await dbFile.stat()).size;
var dbSize = kIsWeb ? -1 : (await dbFile.stat()).size;
var dbTLRecordsQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetraLeagueMatchesTable}`')).first['COUNT(*)']! as int;
var dbTLStatesQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetrioLeagueTable}`')).first['COUNT(*)']! as int;
return (dbSize, dbTLRecordsQuery, dbTLStatesQuery);
@ -673,8 +673,7 @@ class TetrioService extends DB {
/// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states
/// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data.
Future<List<TetraLeague>> fetchAndsaveTLHistory(String id, int season) async {
// TODO: find le way to get season 2 history
Future<List<TetraLeague>> fetchAndsaveS1TLHistory(String id) async {
Uri url;
if (kIsWeb) {
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id});
@ -745,6 +744,70 @@ class TetrioService extends DB {
}
}
Future<List<TetraLeague>> fetchAndsaveS2TLHistory(String id) async {
final db = getDatabaseOrThrow();
List<BetaRecord> records = [];
int entries = 100;
String? prisecter;
while (entries > 0){
TetraLeagueBetaStream stream = await fetchTLStream(id, prisecter: prisecter);
if (stream.records.isEmpty) break;
records.addAll(stream.records);
prisecter = stream.records.last.prisecter.toString();
entries = stream.records.length;
}
//TetraLeague currentState = await fetchTLSummary(id);
List<TetraLeague> states = [];
//states.add(currentState);
int gp = 0;
int gw = 0;
List<double> last10apm = [];
List<double> last10pps = [];
List<double> last10vs = [];
int bestRankIndex = -1; // -1 - Z; 0 - D, 1 - D+ ... 18 - X+
Batch batch = db.batch();
for (BetaRecord match in records.reversed){
gp++;
if (match.extras.result.contains("victory")) gw++;
last10apm.add(match.results.leaderboard.firstWhere((e) => e.id == id).stats.apm);
if (last10apm.length > 10) last10apm.removeAt(0);
last10pps.add(match.results.leaderboard.firstWhere((e) => e.id == id).stats.pps);
if (last10pps.length > 10) last10pps.removeAt(0);
last10vs.add(match.results.leaderboard.firstWhere((e) => e.id == id).stats.vs);
if (last10vs.length > 10) last10vs.removeAt(0);
double apm = last10apm.reduce((v, e) => v + e) / last10apm.length;
double pps = last10pps.reduce((v, e) => v + e) / last10pps.length;
double vs = last10vs.reduce((v, e) => v + e) / last10vs.length;
TetraLeague state = TetraLeague(
id: id,
timestamp: match.ts,
gamesPlayed: gp,
gamesWon: gw,
bestRank: bestRankIndex != -1 ? ranks[bestRankIndex] : "z",
decaying: false,
tr: match.extras.league[id]?[1]?.tr??-1.0,
glicko: match.extras.league[id]?[1]?.glicko,
rd: match.extras.league[id]?[1]?.rd,
gxe: match.extras.league[id]?[1]?.glicko != null ? 10000 / (1 + pow(10, (((1500 - match.extras.league[id]![1]!.glicko) * pi / sqrt(3 * pow(ln10, 2) * pow(match.extras.league[id]![1]!.rd, 2) + 2500 * (64 * pow(pi, 2) + 147 * pow(ln10, 2))))))) / 100 : -1,
rank: match.extras.league[id]?[1]?.rank??"z",
percentileRank: match.extras.league[id]?[1]?.rank??"z",
percentile: match.extras.league[id]?[1]?.rank != null ? rankCutoffs[match.extras.league[id]![1]!.rank]! : -1,
standing: match.extras.league[id]?[1]?.placement??-1,
standingLocal: -1,
nextAt: -1,
prevAt: -1,
apm: apm,
pps: pps,
vs: vs,
season: currentSeason
);
states.add(state);
batch.insert(tetrioLeagueTable, state.toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
}
batch.commit();
return states;
}
/// Docs later
Future<TetraLeagueAlphaStream> fetchAndSaveOldTLmatches(String userID) async {
Uri url;
@ -1129,38 +1192,29 @@ 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 `UserRecords`.
/// Throws an exception if fails to retrieve.
Future<UserRecords> fetchRecords(String userID) async {
UserRecords? cached = _cache.get(userID, UserRecords);
Future<TetraLeague> fetchTLSummary(String id) async {
TetraLeague? cached = _cache.get(id, TetraLeague);
if (cached != null) return cached;
Uri url;
if (kIsWeb) {
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUserRecords", "user": userID.toLowerCase().trim()});
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "Summaries", "id": id});
} else {
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records');
url = Uri.https('ch.tetr.io', 'api/users/$id/summaries/league');
}
try{
final response = await client.get(url);
switch (response.statusCode) {
case 200:
if (jsonDecode(response.body)['success']) {
Map jsonRecords = jsonDecode(response.body);
var sprint = jsonRecords['data']['records']['40l']['record'] != null
? RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'], jsonRecords['data']['records']['40l']['rank_local'])
: null;
var blitz = jsonRecords['data']['records']['blitz']['record'] != null
? RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'], jsonRecords['data']['records']['blitz']['rank_local'])
: null;
var zen = TetrioZen.fromJson(jsonRecords['data']['zen']);
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 result;
developer.log("fetchTLSummary: $id TL state retrieved and cached", name: "services/tetrio_crud");
TetraLeague league = TetraLeague.fromJson(jsonDecode(response.body)['data'], DateTime.now(), currentSeason, id);
_cache.store(league, jsonDecode(response.body)['cache']['cached_until']);
return league;
} else {
developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body);
developer.log("fetchTLSummary: User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist();
}
case 403:
@ -1175,7 +1229,7 @@ class TetrioService extends DB {
case 504:
throw TetrioInternalProblem();
default:
developer.log("fetchRecords Failed to fetch records", name: "services/tetrio_crud", error: response.statusCode);
developer.log("fetchTLSummary Failed to fetch TL state", name: "services/tetrio_crud", error: response.statusCode);
throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason");
}
} on http.ClientException catch (e, s) {
@ -1427,15 +1481,4 @@ 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);
// TetraLeagueBetaStream matches = await fetchTLStream(userID);
// saveTLMatchesFromStream(matches);
// sleep(Durations.extralong4);
// }
// }
}

View File

@ -76,7 +76,7 @@ class ClearData{
if (rules.combo && rules.comboTable != ComboTables.none) {
if (combo >= 1){
if (lines == 1 && rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))];
if (rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))];
else damage *= (1 + COMBO_BONUS * (combo));
}
if (combo >= 2) {
@ -166,7 +166,7 @@ class _DestinationCalculatorState extends State<DestinationCalculator> {
List<ClearData> clears = [];
Map<String, int> customClearsChoice = {
t.calcDestination.noSpinClears: 5,
t.calcDestination.spins: 5
t.stats.spins: 5
};
int idCounter = 0;
Rules rules = Rules();
@ -400,7 +400,13 @@ class _DestinationCalculatorState extends State<DestinationCalculator> {
),
onTap: (){
setState((){
clears.add(ClearData("${key == t.calcDestination.spins ? "${t.stats.spin} " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} ${t.stats.lines})", key == t.calcDestination.spins ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, customClearsChoice[key]!, false, key == t.calcDestination.spins).cloneWith(idCounter));
clears.add(ClearData(
"${key == t.stats.spins ? "${t.stats.spin} " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} ${t.stats.lines})",
key == t.stats.spins ? Lineclears.TSPIN_PENTA : Lineclears.PENTA,
customClearsChoice[key]!,
false,
key == t.stats.spins).cloneWith(idCounter)
);
});
idCounter++;
},

View File

@ -51,6 +51,7 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
ValueNotifier<String> historyPlayerUsername = ValueNotifier("");
ValueNotifier<String> historyPlayerAvatarRevizion = ValueNotifier("");
List<String> excludeRanks = [];
late Future<Map<int, Map<Stats, List<_HistoryChartSpot>>>> playerHistory = getHistoryData(fetchData);
late Future<List<_MyScatterSpot>> futureLeague = getTetraLeagueData(_Xchart, Ychart);
String searchLeague = "";
int? TLstatePlayers;
@ -143,9 +144,11 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
}
Future<Map<int, Map<Stats, List<_HistoryChartSpot>>>> getHistoryData(bool fetchHistory) async {
var playerID = (await teto.fetchPlayer(widget.searchFor)).userId;
if(fetchHistory){
try{
var history = await teto.fetchAndsaveTLHistory(widget.searchFor, 1);
//var history = await Future.wait([teto.fetchAndsaveS1TLHistory(widget.searchFor), teto.fetchAndsaveS2TLHistory(widget.searchFor)]); // S1 history unavaliable because of certificate issue on p1nkl0bst3r side
var history = await teto.fetchAndsaveS2TLHistory(playerID);
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.graphsDestination.fetchAndsaveTLHistoryResult(number: history.length))));
}on TetrioHistoryNotExist{
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.noHistorySaved)));
@ -159,19 +162,19 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
}
List<List<TetraLeague>> states = await Future.wait<List<TetraLeague>>([
teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2),
teto.getStates(playerID, season: 1), teto.getStates(playerID, season: 2),
]);
Map<int, Map<Stats, List<_HistoryChartSpot>>> historyData = {}; // [season][metric][spot]
for (int season = 0; season < currentSeason; season++){
if (states[season].length >= 2){
Map<Stats, List<_HistoryChartSpot>> statsMap = {};
for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())];
for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null && tl.getStatByEnum(stat) != -1.00) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())];
historyData[season] = statsMap;
}
}
fetchData = false;
historyPlayerUsername.value = await teto.getNicknameByID(widget.searchFor);
historyPlayerUsername.value = await teto.getNicknameByID(playerID);
return historyData;
}
@ -234,7 +237,7 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
trendlines:<Trendline>[
Trendline(
isVisible: _smooth,
period: (selectedGraph.length/175).floor(),
period: (selectedGraph.length/100).floor(),
type: TrendlineType.movingAverage,
color: Theme.of(context).colorScheme.primary)
],
@ -250,7 +253,7 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
trendlines:<Trendline>[
Trendline(
isVisible: _smooth,
period: (selectedGraph.length/175).floor(),
period: (selectedGraph.length/100).floor(),
type: TrendlineType.movingAverage,
color: Theme.of(context).colorScheme.primary)
],
@ -532,7 +535,16 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
);
});
}, icon: Icon(Icons.filter_alt)),
IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,)
IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,),
if (graph == Graph.history) ElevatedButton.icon(
onPressed: (){
setState(() {
fetchData = true;
});
},
label: Text(t.graphsDestination.fetchAndsaveTLHistory),
icon: Icon(Icons.download),
)
],
),
),
@ -581,6 +593,11 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
),
);
}
void markNeedsBuild() {
}
}
class _HistoryChartSpot{

View File

@ -447,7 +447,7 @@ class _DestinationSettings extends State<DestinationSettings> with SingleTickerP
text: TextSpan(
style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white),
children: [
TextSpan(text: "${bytesToSize(snapshot.data!.$1)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)),
TextSpan(text: "${snapshot.data!.$1 == -1 ? "???" : bytesToSize(snapshot.data!.$1)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)),
TextSpan(text: "${t.settingsDestination.bytesOfDataStored}\n"),
TextSpan(text: "${intf.format(snapshot.data!.$2)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)),
TextSpan(text: "${t.settingsDestination.TLrecordsSaved}\n"),
@ -457,7 +457,7 @@ class _DestinationSettings extends State<DestinationSettings> with SingleTickerP
)
);
}
if (snapshot.hasError){ return FutureError(snapshot); }
if (snapshot.hasError){ return SizedBox(height: 500.0, child: FutureError(snapshot)); }
}
return Text("huh?");
}

View File

@ -41,7 +41,6 @@ Future<FetchResults> getData(String searchFor, {bool withHistory = false}) async
}else{
player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username
}
}on TetrioPlayerNotExist{
return FetchResults(false, null, [], null, null, null, null, null, false, TetrioPlayerNotExist());
}
@ -62,7 +61,7 @@ Future<FetchResults> getData(String searchFor, {bool withHistory = false}) async
cutoffs = requests.elementAtOrNull(2);
averages = requests.elementAtOrNull(3);
if(withHistory) await teto.fetchAndsaveTLHistory(player.userId, 1); // Retrieve if needed
if(withHistory) await teto.fetchAndsaveS1TLHistory(player.userId); // Retrieve if needed
} on Exception catch (e) {
return FetchResults(false, null, [], null, null, null, null, null, false, e);
}

View File

@ -57,44 +57,38 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
constraints: const BoxConstraints(maxWidth: 600),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
child: Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: TableBorder.all(color: Colors.grey.shade900),
columnWidths: const {0: FixedColumnWidth(48)},
children: [
Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: TableBorder.all(color: Colors.grey.shade900),
columnWidths: const {0: FixedColumnWidth(48)},
TableRow(
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.gamemodes["40l"]!, 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.gamemodes["blitz"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
]
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.gamemodes["40l"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
for (MapEntry<String, Duration> sprintEntry in sprintAverages.entries) TableRow(
decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[sprintEntry.key]!.withAlpha(100), rankColors[sprintEntry.key]!.withAlpha(200)])),
children: [
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(getALittleBitMoreNormalTime(sprintEntry.value), 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: 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(t.gamemodes["blitz"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
),
]
),
Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 8, 25))))
for (MapEntry<String, Duration> sprintEntry in sprintAverages.entries) TableRow(
decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[sprintEntry.key]!.withAlpha(100), rankColors[sprintEntry.key]!.withAlpha(200)])),
children: [
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(getALittleBitMoreNormalTime(sprintEntry.value), 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: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
),
]
)
],
),
),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/beta_record.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/utils/numers_formats.dart';
@ -9,8 +10,8 @@ import 'package:tetra_stats/widgets/text_timestamp.dart';
class BetaLeagueEntryThingy extends StatelessWidget{
final BetaRecord record;
final String userID;
// TODO: Rating delta string is too long for small screens
const BetaLeagueEntryThingy(this.record, this.userID);
final bool wide;
const BetaLeagueEntryThingy(this.record, this.userID, this.wide);
TextSpan matchResult(String result){
return switch(result){
@ -57,6 +58,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{
@override
Widget build(BuildContext context) {
NumberFormat diff = wide ? fDiff : comparef2;
double? deltaTR = (record.extras.league[userID]?[1]?.tr != null && record.extras.league[userID]?[0]?.tr != null) ? record.extras.league[userID]![1]!.tr - record.extras.league[userID]![0]!.tr : null;
double? deltaGlicko = (record.extras.league[userID]?[1]?.glicko != null && record.extras.league[userID]?[0]?.glicko != null) ? record.extras.league[userID]![1]!.glicko - record.extras.league[userID]![0]!.glicko : null;
double? deltaRD = (record.extras.league[userID]?[1]?.rd != null && record.extras.league[userID]?[0]?.rd != null) ? record.extras.league[userID]![1]!.rd - record.extras.league[userID]![0]!.rd : null;
@ -88,7 +90,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{
text: ", ${timestamp(record.ts)}\n"
),
TextSpan(
text: deltaTR != null ? "${fDiff.format(deltaTR)} TR" : "??? TR",
text: deltaTR != null ? "${diff.format(deltaTR)} TR" : "??? TR",
style: TextStyle(
color: deltaColor(deltaTR)
)
@ -97,7 +99,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{
text: ", "
),
TextSpan(
text: deltaGlicko != null ? "${fDiff.format(deltaGlicko)} Glicko" : "??? Glicko",
text: deltaGlicko != null ? "${diff.format(deltaGlicko)} Glicko" : "??? Glicko",
style: TextStyle(
color: deltaColor(deltaGlicko)
)
@ -106,7 +108,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{
text: ", "
),
TextSpan(
text: deltaRD != null ? "${fDiff.format(deltaRD)} RD" : "??? RD",
text: deltaRD != null ? "${diff.format(deltaRD)} RD" : "??? RD",
style: TextStyle(
color: Colors.grey
)

View File

@ -65,7 +65,7 @@ class TLRatingThingy extends StatelessWidget{
} : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),]
)
),
if (oldTl != null) RichText(
if (oldTl != null && oldTl!.tr != -1.0) RichText(
textAlign: TextAlign.center,
softWrap: true,
text: TextSpan(

View File

@ -103,7 +103,7 @@ class _TLRecordsState extends State<TLRecords> {
),
),
itemBuilder: (BuildContext context, int index){
return BetaLeagueEntryThingy(records[index], widget.userID);
return BetaLeagueEntryThingy(records[index], widget.userID, MediaQuery.of(context).size.width >= 768.0);
}
),
);

View File

@ -47,7 +47,7 @@ class TetraLeagueThingy extends StatelessWidget{
message: "${t.stats.glixare.full}",
child: Tooltip(child: Text(" ${t.stats.glixare.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.gxe.isNegative ? Colors.grey : Colors.white)), message: "Glixare")
),
if (toCompare != null) Text(" (${comparef.format(league.gxe-toCompare!.gxe)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: getDifferenceColor(league.gxe-toCompare!.gxe))),
if (toCompare != null) Text(toCompare!.gxe != -1 ? " (${comparef.format(league.gxe-toCompare!.gxe)})" : "(---)", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: toCompare!.gxe != -1 ? getDifferenceColor(league.gxe-toCompare!.gxe) : Colors.grey)),
if (lbPos != null) Text(lbPos?.glixare != null ? (lbPos!.glixare!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.glixare!.percentage*100)}%)" : " (№ ${lbPos!.glixare!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.glixare != null ? getColorOfRank(lbPos!.glixare!.position) : null))
]),
];
@ -60,7 +60,7 @@ class TetraLeagueThingy extends StatelessWidget{
child: Column(
children: [
TLRatingThingy(userID: league.id, tlData: league, oldTl: toCompare, showPositions: true),
if (league.gamesPlayed > 9) TLProgress(
if (league.gamesPlayed > 9 && league.percentileRank != "z") TLProgress(
tlData: league,
previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null,
nextRankTRcutoff: cutoffs != null ? cutoffs!.tr[ranks2[ranks2.indexOf(league.rank != "z" ? league.rank : league.percentileRank)-1]] : null,

View File

@ -20,6 +20,8 @@ static void my_application_activate(GApplication* application) {
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
gtk_window_set_icon_from_file(GTK_WINDOW(window),"res/icons/app.png",NULL);
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).

View File

@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@ -328,6 +328,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_to_debian:
dependency: "direct main"
description:
name: flutter_to_debian
sha256: d23534407334b331ce20fbaa8395b9ecc255d0c047136b8998715f36933ee696
url: "https://pub.dev"
source: hosted
version: "2.0.2"
flutter_web_plugins:
dependency: transitive
description: flutter
@ -509,6 +517,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
mime_type:
dependency: transitive
description:
name: mime_type
sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb
url: "https://pub.dev"
source: hosted
version: "1.0.1"
node_preamble:
dependency: transitive
description:

View File

@ -2,7 +2,7 @@ name: tetra_stats
description: Track your and other player stats in TETR.IO
publish_to: 'none'
version: 2.0.2+43
version: 2.0.3+44
environment:
sdk: '>=3.0.0'
@ -46,6 +46,7 @@ dependencies:
flutter_layout_grid: ^2.0.0
go_router: ^13.0.0
syncfusion_flutter_charts: ^24.2.9
flutter_to_debian: ^2.0.2
dev_dependencies:
flutter_test:

View File

@ -172,7 +172,7 @@
"refresh": "Обновить"
},
"graphsDestination": {
"fetchAndsaveTLHistory": "Получить историю игрока",
"fetchAndsaveTLHistory": "Получить историю",
"fetchAndSaveOldTLmatches": "Получить историю матчей Тетра Лиги",
"fetchAndsaveTLHistoryResult": "${number} состояний было найдено",
"fetchAndSaveOldTLmatchesResult": "${number} матчей было найдено",

View File

@ -131,7 +131,7 @@
}
</style>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js?version=2.0.2" defer></script>
<script src="flutter.js?version=2.0.3" defer></script>
</head>
<body>
<div id="preloader">