Compare commits

..

No commits in common. "68784834fe5f05a3199f65e0e40faaa764cdc6cc" and "523038b9c8bd200a4473a38aa689df4bb161eb1f" have entirely different histories.

15 changed files with 209 additions and 255 deletions

View File

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

View File

@ -265,7 +265,6 @@ class TetrioPlayer {
List<RecordSingle?> blitz = []; List<RecordSingle?> blitz = [];
TetrioZen? zen; TetrioZen? zen;
Distinguishment? distinguishment; Distinguishment? distinguishment;
DateTime? cachedUntil;
TetrioPlayer({ TetrioPlayer({
required this.userId, required this.userId,
@ -293,12 +292,11 @@ class TetrioPlayer {
required this.blitz, required this.blitz,
this.zen, this.zen,
this.distinguishment, this.distinguishment,
this.cachedUntil
}); });
double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1; double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1;
TetrioPlayer.fromJson(Map<String, dynamic> json, DateTime stateTime, String id, String nick, [DateTime? cUntil]) { TetrioPlayer.fromJson(Map<String, dynamic> json, DateTime stateTime, String id, String nick) {
//developer.log("TetrioPlayer.fromJson $stateTime: $json", name: "data_objects/tetrio"); //developer.log("TetrioPlayer.fromJson $stateTime: $json", name: "data_objects/tetrio");
userId = id; userId = id;
username = nick; username = nick;
@ -326,7 +324,6 @@ class TetrioPlayer {
friendCount = json['friend_count'] ?? 0; friendCount = json['friend_count'] ?? 0;
badstanding = json['badstanding']; badstanding = json['badstanding'];
botmaster = json['botmaster']; botmaster = json['botmaster'];
cachedUntil = cUntil;
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -1167,15 +1164,6 @@ class TetrioZen {
} }
} }
class UserRecords{
String id;
RecordSingle? sprint;
RecordSingle? blitz;
TetrioZen zen;
UserRecords(this.id, this.sprint, this.blitz, this.zen);
}
class Distinguishment { class Distinguishment {
late String type; late String type;
String? detail; String? detail;
@ -1204,28 +1192,18 @@ class Distinguishment {
} }
} }
class News{ class News {
late String id; late String id;
late List<NewsEntry> news; late String stream;
News(this.id, this.news);
News.fromJson(Map<String, dynamic> json, String? userID){
id = userID != null ? "user_${userID}" : json['news'].first['stream'];
news = [for (var entry in json['news']) NewsEntry.fromJson(entry)];
}
}
class NewsEntry {
//late String id; do i need it?
late String type; late String type;
late Map<String, dynamic> data; late Map<String, dynamic> data;
late DateTime timestamp; late DateTime timestamp;
NewsEntry({required this.type, required this.data, required this.timestamp}); News({required this.type, required this.id, required this.stream, required this.data, required this.timestamp});
NewsEntry.fromJson(Map<String, dynamic> json){ News.fromJson(Map<String, dynamic> json){
//id = json["_id"]; id = json["_id"];
stream = json["stream"];
type = json["type"]; type = json["type"];
data = json["data"]; data = json["data"];
timestamp = DateTime.parse(json['ts']); timestamp = DateTime.parse(json['ts']);

View File

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

View File

@ -1,12 +1,9 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:developer' as developer;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/views/customization_view.dart'; import 'package:tetra_stats/views/customization_view.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
@ -21,7 +18,6 @@ import 'package:go_router/go_router.dart';
late final PackageInfo packageInfo; late final PackageInfo packageInfo;
late SharedPreferences prefs; late SharedPreferences prefs;
late TetrioService teto;
ColorScheme sheme = const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white); ColorScheme sheme = const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white);
void setAccentColor(Color color){ // does this thing work??? yes??? no??? void setAccentColor(Color color){ // does this thing work??? yes??? no???
@ -81,7 +77,6 @@ void main() async {
packageInfo = await PackageInfo.fromPlatform(); packageInfo = await PackageInfo.fromPlatform();
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
teto = TetrioService();
// Choosing the locale // Choosing the locale
String? locale = prefs.getString("locale"); String? locale = prefs.getString("locale");
@ -90,12 +85,6 @@ void main() async {
}else{ }else{
LocaleSettings.setLocaleRaw(locale); LocaleSettings.setLocaleRaw(locale);
} }
// I dont want to store old cache
Timer.periodic(Duration(minutes: 5), (Timer timer) {
teto.cacheRoutine();
developer.log("Cache routine complete", name: "main");
});
runApp(TranslationProvider( runApp(TranslationProvider(
child: const MyApp(), child: const MyApp(),

View File

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

View File

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

View File

@ -1,6 +1,5 @@
// ignore_for_file: type_literal_in_constant_pattern // ignore_for_file: type_literal_in_constant_pattern
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@ -11,10 +10,10 @@ import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:tetra_stats/data_objects/tetra_stats.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show prefs, teto; import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/main.dart' show prefs;
import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/utils/text_shadow.dart';
@ -33,6 +32,7 @@ import 'package:window_manager/window_manager.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
final TetrioService teto = TetrioService(); // thing, that manadge our local DB
int _chartsIndex = 0; int _chartsIndex = 0;
bool _gamesPlayedInsteadOfDateAndTime = false; bool _gamesPlayedInsteadOfDateAndTime = false;
late ZoomPanBehavior _zoomPanBehavior; late ZoomPanBehavior _zoomPanBehavior;
@ -94,7 +94,6 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
//var tableData = <TableRow>[]; //var tableData = <TableRow>[];
final bodyGlobalKey = GlobalKey(); final bodyGlobalKey = GlobalKey();
bool _showSearchBar = false; bool _showSearchBar = false;
Timer backgroundUpdate = Timer(Duration(days: 365), (){});
bool _TLHistoryWasFetched = false; bool _TLHistoryWasFetched = false;
late TabController _tabController; late TabController _tabController;
late TabController _wideScreenTabController; late TabController _wideScreenTabController;
@ -159,7 +158,6 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
Future<List> fetch(String nickOrID, {bool fetchHistory = false, bool fetchTLmatches = false}) async { Future<List> fetch(String nickOrID, {bool fetchHistory = false, bool fetchTLmatches = false}) async {
TetrioPlayer me; TetrioPlayer me;
_TLHistoryWasFetched = false; _TLHistoryWasFetched = false;
backgroundUpdate.cancel();
// If user trying to search with discord id // If user trying to search with discord id
if (nickOrID.startsWith("ds:")){ if (nickOrID.startsWith("ds:")){
@ -176,23 +174,23 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
// Requesting Tetra League (alpha), records, news and top TR of player // Requesting Tetra League (alpha), records, news and top TR of player
late List<dynamic> requests; late List<dynamic> requests;
late TetraLeagueAlphaStream tlStream; late TetraLeagueAlphaStream tlStream;
late UserRecords records; late Map<String, dynamic> records;
late News news; late List<News> news;
late TetrioPlayerFromLeaderboard? topOne; late TetrioPlayerFromLeaderboard? topOne;
late TopTr? topTR; late double? topTR;
requests = await Future.wait([ // all at once requests = await Future.wait([ // all at once
teto.fetchTLStream(_searchFor), teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor), teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor), teto.fetchNews(_searchFor),
prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=><Map<String, double>>[]), 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.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 if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR
]); ]);
tlStream = requests[0] as TetraLeagueAlphaStream; tlStream = requests[0] as TetraLeagueAlphaStream;
records = requests[1] as UserRecords; records = requests[1] as Map<String, dynamic>;
news = requests[2] as News; news = requests[2] as List<News>;
topOne = requests[4] as TetrioPlayerFromLeaderboard?; topOne = requests[4] as TetrioPlayerFromLeaderboard?;
topTR = requests[5] as TopTr?; // No TR - no Top TR topTR = requests.elementAtOrNull(5) as double?; // No TR - no Top TR
meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId);
if (prefs.getBool("showPositions") == true){ if (prefs.getBool("showPositions") == true){
@ -204,8 +202,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!);
} }
} }
Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as Cutoffs?)?.tr; Map<String, double>? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as List<Map<String, double>>).elementAtOrNull(0);
Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as Cutoffs?)?.glicko; Map<String, double>? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as List<Map<String, double>>).elementAtOrNull(1);
if (me.tlSeason1.gamesPlayed > 9) { if (me.tlSeason1.gamesPlayed > 9) {
thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank];
@ -310,11 +308,6 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
compareWith = null; compareWith = null;
chartsData = []; chartsData = [];
} }
backgroundUpdate = Timer(me.cachedUntil!.difference(DateTime.now()), () {
changePlayer(me.userId);
});
return [me, records, states, tlMatches, compareWith, isTracking, news, topTR]; return [me, records, states, tlMatches, compareWith, isTracking, news, topTR];
} }
@ -466,7 +459,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
tl: snapshot.data![0].tlSeason1, tl: snapshot.data![0].tlSeason1,
userID: snapshot.data![0].userId, userID: snapshot.data![0].userId,
states: snapshot.data![2], states: snapshot.data![2],
topTR: snapshot.data![7]?.tr, topTR: snapshot.data![7],
bot: snapshot.data![0].role == "bot", bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon", guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff, thatRankCutoff: thatRankCutoff,
@ -485,14 +478,14 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
), ),
],), ],),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank,), _TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,),
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
] : [ ] : [
TLThingy( TLThingy(
tl: snapshot.data![0].tlSeason1, tl: snapshot.data![0].tlSeason1,
userID: snapshot.data![0].userId, userID: snapshot.data![0].userId,
states: snapshot.data![2], states: snapshot.data![2],
topTR: snapshot.data![7]?.tr, topTR: snapshot.data![7],
bot: snapshot.data![0].role == "bot", bot: snapshot.data![0].role == "bot",
guest: snapshot.data![0].role == "anon", guest: snapshot.data![0].role == "anon",
thatRankCutoff: thatRankCutoff, thatRankCutoff: thatRankCutoff,
@ -506,9 +499,9 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
), ),
_TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched), _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched),
_History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0),
_RecordThingy(record: snapshot.data![1].sprint, rank: snapshot.data![0].tlSeason1.percentileRank), _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank),
_RecordThingy(record: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank), _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank),
_OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],)
], ],
), ),
), ),
@ -1295,7 +1288,7 @@ class _OtherThingy extends StatelessWidget {
final TetrioZen? zen; final TetrioZen? zen;
final String? bio; final String? bio;
final Distinguishment? distinguishment; final Distinguishment? distinguishment;
final News? newsletter; final List<News>? newsletter;
/// Widget, that shows players [distinguishment], [bio], [zen] and [newsletter] /// Widget, that shows players [distinguishment], [bio], [zen] and [newsletter]
const _OtherThingy({required this.zen, required this.bio, required this.distinguishment, this.newsletter}); const _OtherThingy({required this.zen, required this.bio, required this.distinguishment, this.newsletter});
@ -1344,7 +1337,7 @@ class _OtherThingy extends StatelessWidget {
} }
/// Handles [news] entry and returns widget that contains this entry /// Handles [news] entry and returns widget that contains this entry
ListTile getNewsTile(NewsEntry news){ ListTile getNewsTile(News news){
Map<String, String> gametypes = { Map<String, String> gametypes = {
"40l": t.sprint, "40l": t.sprint,
"blitz": t.blitz, "blitz": t.blitz,
@ -1526,7 +1519,7 @@ class _OtherThingy extends StatelessWidget {
], ],
), ),
), ),
if (newsletter != null && newsletter!.news.isNotEmpty && showNewsTitle) if (newsletter != null && newsletter!.isNotEmpty && showNewsTitle)
Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
], ],
); );
@ -1542,9 +1535,9 @@ class _OtherThingy extends StatelessWidget {
SizedBox(width: 450, child: getShit(context, true, false)), SizedBox(width: 450, child: getShit(context, true, false)),
SizedBox(width: constraints.maxWidth - 450, child: ListView.builder( SizedBox(width: constraints.maxWidth - 450, child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: newsletter!.news.length+1, itemCount: newsletter!.length+1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter!.news[index-1]); return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter![index-1]);
} }
)) ))
] ]
@ -1553,9 +1546,9 @@ class _OtherThingy extends StatelessWidget {
else { else {
return ListView.builder( return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: newsletter!.news.length+1, itemCount: newsletter!.length+1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter!.news[index-1]); return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter![index-1]);
}, },
); );
} }

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/main.dart' show packageInfo, teto; import 'package:tetra_stats/main.dart' show packageInfo;
import 'package:file_selector/file_selector.dart'; import 'package:file_selector/file_selector.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -10,6 +10,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/utils/open_in_browser.dart'; import 'package:tetra_stats/utils/open_in_browser.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -24,6 +25,7 @@ class SettingsView extends StatefulWidget {
class SettingsState extends State<SettingsView> { class SettingsState extends State<SettingsView> {
late SharedPreferences prefs; late SharedPreferences prefs;
final TetrioService teto = TetrioService();
String defaultNickname = "Checking..."; String defaultNickname = "Checking...";
late bool showPositions; late bool showPositions;
final TextEditingController _playertext = TextEditingController(); final TextEditingController _playertext = TextEditingController();

View File

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/views/mathes_view.dart'; import 'package:tetra_stats/views/mathes_view.dart';
import 'package:tetra_stats/views/state_view.dart'; import 'package:tetra_stats/views/state_view.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';

View File

@ -6,8 +6,7 @@ import 'package:tetra_stats/services/crud_exceptions.dart';
import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy; import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy;
import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart';
import 'package:tetra_stats/widgets/vs_graphs.dart'; import 'package:tetra_stats/widgets/vs_graphs.dart';
import 'main_view.dart' show secs; import 'main_view.dart' show teto, secs;
import 'package:tetra_stats/main.dart' show teto;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';

View File

@ -4,11 +4,12 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show teto; import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/utils/filesizes_converter.dart'; import 'package:tetra_stats/utils/filesizes_converter.dart';
import 'package:tetra_stats/views/states_view.dart'; import 'package:tetra_stats/views/states_view.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
final TetrioService teto = TetrioService();
late String oldWindowTitle; late String oldWindowTitle;
class TrackedPlayersView extends StatefulWidget { class TrackedPlayersView extends StatefulWidget {

View File

@ -70,7 +70,7 @@ class TLProgress extends StatelessWidget{
if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"),
if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"),
if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"),
if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null))
] ]
) )
), ),

View File

@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/main.dart' show teto;
import 'package:tetra_stats/views/compare_view.dart'; import 'package:tetra_stats/views/compare_view.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:tetra_stats/utils/text_shadow.dart';