diff --git a/lib/data_objects/tetra_stats.dart b/lib/data_objects/tetra_stats.dart new file mode 100644 index 0000000..e5af9cf --- /dev/null +++ b/lib/data_objects/tetra_stats.dart @@ -0,0 +1,15 @@ +// p1nkl0bst3r data objects + +class Cutoffs{ + Map tr; + Map glicko; + + Cutoffs(this.tr, this.glicko); +} + +class TopTr{ + String id; + double? tr; + + TopTr(this.id, this.tr); +} \ No newline at end of file diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 0e300a6..98e8290 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -1164,6 +1164,15 @@ class TetrioZen { } } +class UserRecords{ + String id; + RecordSingle? sprint; + RecordSingle? blitz; + TetrioZen zen; + + UserRecords(this.id, this.sprint, this.blitz, this.zen); +} + class Distinguishment { late String type; String? detail; @@ -1192,18 +1201,28 @@ class Distinguishment { } } -class News { +class News{ late String id; - late String stream; + late List news; + + News(this.id, this.news); + + News.fromJson(Map json, String? userID){ + id = userID != null ? "user_${userID}" : json['news'].first['stream']; + news = [for (var entry in json['news']) NewsEntry.fromJson(entry)]; + } +} + +class NewsEntry { + //late String id; do i need it? late String type; late Map data; late DateTime timestamp; - News({required this.type, required this.id, required this.stream, required this.data, required this.timestamp}); + NewsEntry({required this.type, required this.data, required this.timestamp}); - News.fromJson(Map json){ - id = json["_id"]; - stream = json["stream"]; + NewsEntry.fromJson(Map json){ + //id = json["_id"]; type = json["type"]; data = json["data"]; timestamp = DateTime.parse(json['ts']); diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 406675b..9e5ecbd 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,4 +1,5 @@ import 'dart:math'; +import 'dart:typed_data'; import 'tetrio.dart'; @@ -57,6 +58,14 @@ class Garbage{ // charsys where??? } } +class RawReplay{ + String id; + Uint8List asBytes; + String asString; + + RawReplay(this.id, this.asBytes, this.asString); +} + class ReplayStats{ late int seed; late int linesCleared; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 8c118ae..61bd772 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; import 'package:path_provider/path_provider.dart'; +import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/main.dart' show packageInfo; import 'package:flutter/foundation.dart'; @@ -65,22 +66,77 @@ const String createTetrioTLReplayStats = ''' ) '''; +class CacheController { + late Map _cache; + late Map _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? 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) => DateTime.fromMillisecondsSinceEpoch(int.parse(key.substring(_getObjectId(value).length)), isUtc: true).isAfter(DateTime.now())); + } + + void reset(){ + _cache.clear(); + } +} + class TetrioService extends DB { final Map _players = {}; - - // I'm trying to send as less requests, as possible, so i'm caching the results of those requests. - // Usually those maps looks like this: {"cached_until_unix_milliseconds": Object} - // TODO: Make a proper caching system - final Map _playersCache = {}; - final Map> _recordsCache = {}; - final Map _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]} - final Map _leaderboardsCache = {}; - final Map _lbPositions = {}; - final Map> _newsCache = {}; - final Map> _topTRcache = {}; - final Map>> _cutoffsCache = {}; - final Map _topOneFromLB = {}; - final Map _tlStreamsCache = {}; + final _cache = CacheController.init(); // I'm trying to send as less requests, as possible, so i'm caching the results of those requests. + final Map _lbPositions = {}; // separate one because attached to the leaderboard /// Thing, that sends every request to the API endpoints final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); /// We should have only one instanse of this service @@ -157,14 +213,20 @@ class TetrioService extends DB { /// Downloads replay from inoue (szy API). Requiers [replayID]. If request have /// different from 200 statusCode, it will throw an excepction. Returns list, that contains same replay /// as string and as binary. - Future> szyGetReplay(String replayID) async { - try{ // read from cache - var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID); - return cached.value; - }catch (e){ - // actually going to obtain + Future szyGetReplay(String replayID) async { + // Trying to get it from cache first + RawReplay? cached = _cache.get(replayID, RawReplay); + if (cached != null) return cached; + + // If failed, trying to obtain replay from download directory + if (!kIsWeb){ // can't obtain download directory on web + var downloadPath = await getDownloadsDirectory(); + downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); + var replayFile = File("${downloadPath.path}/$replayID.ttrm"); + if (replayFile.existsSync()) return RawReplay(replayID, replayFile.readAsBytesSync(), replayFile.readAsStringSync()); } + // If failed, actually trying to retrieve Uri url; if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID}); @@ -172,22 +234,16 @@ class TetrioService extends DB { url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); } - // Trying to obtain replay from download directory first - if (!kIsWeb){ // can't obtain download directory on web - var downloadPath = await getDownloadsDirectory(); - downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); - var replayFile = File("${downloadPath.path}/$replayID.ttrm"); - if (replayFile.existsSync()) return [replayFile.readAsStringSync(), replayFile.readAsBytesSync()]; - } - try{ final response = await client.get(url); switch (response.statusCode) { case 200: - developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode); - _replaysCache[replayID] = [response.body, response.bodyBytes]; // Puts results into the cache - return [response.body, response.bodyBytes]; + developer.log("szyDownload: Replay $replayID downloaded", name: "services/tetrio_crud"); + RawReplay replay = RawReplay(replayID, response.bodyBytes, response.body); + DateTime now = DateTime.now(); + _cache.store(replay, now.millisecondsSinceEpoch + 3600000); + return replay; // if not 200 - throw a unique for each code exception case 404: throw SzyNotFound(); @@ -203,7 +259,7 @@ class TetrioService extends DB { case 504: throw SzyInternalProblem(); default: - developer.log("szyDownload: Failed to download a replay", name: "services/tetrio_crud", error: response.statusCode); + developer.log("szyDownload: Failed to download a replay $replayID", name: "services/tetrio_crud", error: response.statusCode); throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } } on http.ClientException catch (e, s) { // If local http client fails @@ -219,8 +275,8 @@ class TetrioService extends DB { downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); var replayFile = File("${downloadPath.path}/$replayID.ttrm"); if (replayFile.existsSync()) throw TetrioReplayAlreadyExist(); - var replay = await szyGetReplay(replayID); - await replayFile.writeAsBytes(replay[1]); + RawReplay replay = await szyGetReplay(replayID); + await replayFile.writeAsBytes(replay.asBytes); return replayFile.path; } @@ -235,7 +291,7 @@ class TetrioService extends DB { if (!isAvailable) throw ReplayNotAvalable(); // if replay too old // otherwise, actually going to download a replay and analyze it - String replay = (await szyGetReplay(replayID))[0]; + String replay = (await szyGetReplay(replayID)).asString; Map toAnalyze = jsonDecode(replay); ReplayData data = ReplayData.fromJson(toAnalyze); saveReplayStats(data); // saving to DB for later @@ -244,19 +300,10 @@ class TetrioService extends DB { /// Gets and returns Top TR for a player with given [id]. May return null if player top tr is unknown /// or api is unavaliable (404). May throw an exception, if something else happens. - Future fetchTopTR(String id) async { - try{ // read from cache - var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id); - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired - developer.log("fetchTopTR: Top TR retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value.values.first; - }else{ // if cache expired - _topTRcache.remove(cached.key); - developer.log("fetchTopTR: Top TR expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ // actually going to obtain - developer.log("fetchTopTR: Trying to retrieve Top TR", name: "services/tetrio_crud"); - } + Future fetchTopTR(String id) async { + // Trying to get it from cache first + TopTr? cached = _cache.get(id, TopTr); + if (cached != null) return cached; Uri url; if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS @@ -269,12 +316,15 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: // ok - return the value - _topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: double.tryParse(response.body)}; - return double.tryParse(response.body); + TopTr result = TopTr(id, double.tryParse(response.body)); + _cache.store(result, DateTime.now().millisecondsSinceEpoch + 300000); + return result; case 404: // not found - return null + TopTr result = TopTr(id, null); developer.log("fetchTopTR: Probably, player doesn't have top TR", name: "services/tetrio_crud", error: response.statusCode); - _topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: null}; - return null; + _cache.store(result, DateTime.now().millisecondsSinceEpoch + 300000); + //_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: null}; + return result; // if not 200 or 404 - throw a unique for each code exception case 403: throw P1nkl0bst3rForbidden(); @@ -300,19 +350,9 @@ class TetrioService extends DB { // Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above, // so i'm going to document only unique differences between them - Future>> fetchCutoffs() async { - try{ - var cached = _cutoffsCache.entries.first; - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired - developer.log("fetchCutoffs: Cutoffs retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value; - }else{ // if cache expired - _topTRcache.remove(cached.key); - developer.log("fetchCutoffs: Cutoffs expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ // actually going to obtain - developer.log("fetchCutoffs: Trying to retrieve Cutoffs", name: "services/tetrio_crud"); - } + Future fetchCutoffs() async { + Cutoffs? cached = _cache.get("", Cutoffs); + if (cached != null) return cached; Uri url; if (kIsWeb) { @@ -328,17 +368,16 @@ class TetrioService extends DB { case 200: Map rawData = jsonDecode(response.body); Map data = rawData["cutoffs"] as Map; - Map trCutoffs = {}; - Map glickoCutoffs = {}; + Cutoffs result = Cutoffs({}, {}); for (String rank in data.keys){ - trCutoffs[rank] = data[rank]["rating"]; - glickoCutoffs[rank] = data[rank]["glicko"]; + result.tr[rank] = data[rank]["rating"]; + result.glicko[rank] = data[rank]["glicko"]; } - _cutoffsCache[(rawData["ts"] + 300000).toString()] = [trCutoffs, glickoCutoffs]; - return [trCutoffs, glickoCutoffs]; + _cache.store(result, rawData["ts"] + 300000); + return result; case 404: developer.log("fetchCutoffs: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); - return []; + return null; // if not 200 or 404 - throw a unique for each code exception case 403: throw P1nkl0bst3rForbidden(); @@ -362,18 +401,8 @@ class TetrioService extends DB { } Future fetchTopOneFromTheLeaderboard() async { - try{ - var cached = _topOneFromLB.entries.first; - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired - developer.log("fetchTopOneFromTheLeaderboard: Leader retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value; - }else{ // if cache expired - _topTRcache.remove(cached.key); - developer.log("fetchTopOneFromTheLeaderboard: Leader expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ // actually going to obtain - developer.log("fetchTopOneFromTheLeaderboard: Trying to retrieve leader", name: "services/tetrio_crud"); - } + TetrioPlayerFromLeaderboard? cached = _cache.get("topone", TetrioPlayerFromLeaderboard); + if (cached != null) return cached; Uri url; if (kIsWeb) { @@ -388,7 +417,9 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: var rawJson = jsonDecode(response.body); - return TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["users"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"])); + TetrioPlayerFromLeaderboard result = TetrioPlayerFromLeaderboard.fromJson(rawJson["data"]["users"][0], DateTime.fromMillisecondsSinceEpoch(rawJson["cache"]["cached_at"])); + _cache.store(result, rawJson["cache"]["cached_until"]); + return result; case 404: throw TetrioPlayerNotExist(); // if not 200 or 404 - throw a unique for each code exception @@ -606,18 +637,9 @@ class TetrioService extends DB { /// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve. Future fetchTLLeaderboard() async { - try{ - var cached = _leaderboardsCache.entries.firstWhere((element) => element.value.type == "league"); - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ - developer.log("fetchTLLeaderboard: Leaderboard retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value; - }else{ - _leaderboardsCache.remove(cached.key); - developer.log("fetchTLLeaderboard: Leaderboard expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ - developer.log("fetchTLLeaderboard: Trying to retrieve leaderboard", name: "services/tetrio_crud"); - } + TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); + if (cached != null) return cached; + Uri url; if (kIsWeb) { url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); @@ -634,7 +656,8 @@ class TetrioService extends DB { if (rawJson['success']) { // if api confirmed that everything ok TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at'])); developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); - _leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + _cache.store(leaderboard, rawJson['cache']['cached_until']); return leaderboard; } else { // idk how to hit that one developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); @@ -662,25 +685,13 @@ class TetrioService extends DB { } TetrioPlayersLeaderboard? getCachedLeaderboard(){ - return _leaderboardsCache.entries.firstOrNull?.value; - // That function will break if i decide to recive other leaderboards - // TODO: Think about better solution + return _cache.get("league", TetrioPlayersLeaderboard); } /// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve. - Future> fetchNews(String userID) async{ - try{ - var cached = _newsCache.entries.firstWhere((element) => element.value[0].stream == "user_$userID"); - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ - developer.log("fetchNews: News for $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value; - }else{ - _newsCache.remove(cached.key); - developer.log("fetchNews: Cached news for $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ - developer.log("fetchNews: Trying to retrieve news for $userID", name: "services/tetrio_crud"); - } + Future fetchNews(String userID) async{ + News? cached = _cache.get(userID, News); + if (cached != null) return cached; Uri url; if (kIsWeb) { @@ -695,8 +706,8 @@ class TetrioService extends DB { case 200: var payload = jsonDecode(response.body); if (payload['success']) { // if api confirmed that everything ok - List news = [for (var entry in payload['data']['news']) News.fromJson(entry)]; - _newsCache[payload['cache']['cached_until'].toString()] = news; + News news = News.fromJson(payload['data'], userID); + _cache.store(news, payload['cache']['cached_until']); developer.log("fetchNews: $userID news retrieved and cached", name: "services/tetrio_crud"); return news; } else { @@ -727,18 +738,8 @@ class TetrioService extends DB { /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream). /// Throws an exception if fails to retrieve. Future fetchTLStream(String userID) async { - try{ - var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID); - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ - developer.log("fetchTLStream: Stream $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value; - }else{ - _tlStreamsCache.remove(cached.key); - developer.log("fetchTLStream: Cached stream $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ - developer.log("fetchTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud"); - } + TetraLeagueAlphaStream? cached = _cache.get(userID, TetraLeagueAlphaStream); + if (cached != null) return cached; Uri url; if (kIsWeb) { @@ -753,7 +754,7 @@ class TetrioService extends DB { case 200: if (jsonDecode(response.body)['success']) { TetraLeagueAlphaStream stream = TetraLeagueAlphaStream.fromJson(jsonDecode(response.body)['data']['records'], userID); - _tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream; + _cache.store(stream, jsonDecode(response.body)['cache']['cached_until']); developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud"); return stream; } else { @@ -864,21 +865,11 @@ class TetrioService extends DB { await db.delete(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [rID]); } - /// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns Map, which contains user id (`user`), - /// Blitz (`blitz`) and 40 Lines (`sprint`) record objects and Zen object (`zen`). Throws an exception if fails to retrieve. - Future> fetchRecords(String userID) async { - try{ - var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID); - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ - developer.log("fetchRecords: $userID records retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value; - }else{ - _recordsCache.remove(cached.key); - developer.log("fetchRecords: $userID records expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ - developer.log("fetchRecords: Trying to retrieve $userID records", name: "services/tetrio_crud"); - } + /// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns `UserRecords`. + /// Throws an exception if fails to retrieve. + Future fetchRecords(String userID) async { + UserRecords? cached = _cache.get(userID, UserRecords); + if (cached != null) return cached; Uri url; if (kIsWeb) { @@ -892,7 +883,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: if (jsonDecode(response.body)['success']) { - Map jsonRecords = jsonDecode(response.body); + Map jsonRecords = jsonDecode(response.body); var sprint = jsonRecords['data']['records']['40l']['record'] != null ? RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank']) : null; @@ -900,10 +891,10 @@ class TetrioService extends DB { ? RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank']) : null; var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); - Map map = {"user": userID.toLowerCase().trim(), "sprint": sprint, "blitz": blitz, "zen": zen}; - _recordsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = map; + UserRecords result = UserRecords(userID, sprint, blitz, zen); + _cache.store(result, jsonDecode(response.body)['cache']['cached_until']); developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud"); - return map; + return result; } else { developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body); throw TetrioPlayerNotExist(); @@ -997,8 +988,7 @@ class TetrioService extends DB { } // we not going to add state, that is same, as the previous - bool test = states.last.isSameState(tetrioPlayer); - if (test == false) states.add(tetrioPlayer); + if (!states.last.isSameState(tetrioPlayer)) states.add(tetrioPlayer); // Making map of the states final Map statesJson = {}; @@ -1058,18 +1048,8 @@ class TetrioService extends DB { /// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user. /// If [isItDiscordID] is true, function expects [user] to be a discord user id. Throws an exception if fails to retrieve. Future fetchPlayer(String user, {bool isItDiscordID = false}) async { - try{ - var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user); - if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ - developer.log("fetchPlayer: User $user retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); - return cached.value; - }else{ - _playersCache.remove(cached.key); - developer.log("fetchPlayer: Cached user $user expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); - } - }catch(e){ - developer.log("fetchPlayer: Trying to retrieve $user", name: "services/tetrio_crud"); - } + TetrioPlayer? cached = _cache.get(user, TetrioPlayer); + if (cached != null) return cached; if (isItDiscordID){ // trying to find player with given discord id @@ -1131,7 +1111,7 @@ class TetrioService extends DB { if (json['success']) { // parse and count stats TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username']); - _playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player; + _cache.store(player, json['cache']['cached_until']); developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud"); return player; } else { diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 5cb42e1..2df9b71 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -10,6 +10,7 @@ import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:tetra_stats/data_objects/tetra_stats.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; @@ -174,23 +175,23 @@ class _MainState extends State with TickerProviderStateMixin { // Requesting Tetra League (alpha), records, news and top TR of player late List requests; late TetraLeagueAlphaStream tlStream; - late Map records; - late List news; + late UserRecords records; + late News news; late TetrioPlayerFromLeaderboard? topOne; - late double? topTR; + late TopTr? topTR; requests = await Future.wait([ // all at once teto.fetchTLStream(_searchFor), teto.fetchRecords(_searchFor), teto.fetchNews(_searchFor), prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>>[]), (me.tlSeason1.rank != "z" ? me.tlSeason1.rank == "x" : me.tlSeason1.percentileRank == "x") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), - if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR + (me.tlSeason1.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR ]); tlStream = requests[0] as TetraLeagueAlphaStream; - records = requests[1] as Map; - news = requests[2] as List; + records = requests[1] as UserRecords; + news = requests[2] as News; topOne = requests[4] as TetrioPlayerFromLeaderboard?; - topTR = requests.elementAtOrNull(5) as double?; // No TR - no Top TR + topTR = requests[5] as TopTr?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); if (prefs.getBool("showPositions") == true){ @@ -202,8 +203,8 @@ class _MainState extends State with TickerProviderStateMixin { if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } } - Map? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as List>).elementAtOrNull(0); - Map? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as List>).elementAtOrNull(1); + Map? cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : (requests[3] as Cutoffs?)?.tr; + Map? cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : (requests[3] as Cutoffs?)?.glicko; if (me.tlSeason1.gamesPlayed > 9) { thatRankCutoff = cutoffs?[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; @@ -459,7 +460,7 @@ class _MainState extends State with TickerProviderStateMixin { tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, states: snapshot.data![2], - topTR: snapshot.data![7], + topTR: snapshot.data![7]?.tr, bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon", thatRankCutoff: thatRankCutoff, @@ -478,14 +479,14 @@ class _MainState extends State with TickerProviderStateMixin { ), ],), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), - _TwoRecordsThingy(sprint: snapshot.data![1]['sprint'], blitz: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank,), - _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) + _TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank,), + _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) ] : [ TLThingy( tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, states: snapshot.data![2], - topTR: snapshot.data![7], + topTR: snapshot.data![7]?.tr, bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon", thatRankCutoff: thatRankCutoff, @@ -499,9 +500,9 @@ class _MainState extends State with TickerProviderStateMixin { ), _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3], wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0, oldMathcesHere: _TLHistoryWasFetched), _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![0].tlSeason1.gamesPlayed > 0), - _RecordThingy(record: snapshot.data![1]['sprint'], rank: snapshot.data![0].tlSeason1.percentileRank), - _RecordThingy(record: snapshot.data![1]['blitz'], rank: snapshot.data![0].tlSeason1.percentileRank), - _OtherThingy(zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) + _RecordThingy(record: snapshot.data![1].sprint, rank: snapshot.data![0].tlSeason1.percentileRank), + _RecordThingy(record: snapshot.data![1].blitz, rank: snapshot.data![0].tlSeason1.percentileRank), + _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![6],) ], ), ), @@ -1288,7 +1289,7 @@ class _OtherThingy extends StatelessWidget { final TetrioZen? zen; final String? bio; final Distinguishment? distinguishment; - final List? newsletter; + final News? newsletter; /// Widget, that shows players [distinguishment], [bio], [zen] and [newsletter] const _OtherThingy({required this.zen, required this.bio, required this.distinguishment, this.newsletter}); @@ -1337,7 +1338,7 @@ class _OtherThingy extends StatelessWidget { } /// Handles [news] entry and returns widget that contains this entry - ListTile getNewsTile(News news){ + ListTile getNewsTile(NewsEntry news){ Map gametypes = { "40l": t.sprint, "blitz": t.blitz, @@ -1519,7 +1520,7 @@ class _OtherThingy extends StatelessWidget { ], ), ), - if (newsletter != null && newsletter!.isNotEmpty && showNewsTitle) + if (newsletter != null && newsletter!.news.isNotEmpty && showNewsTitle) Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), ], ); @@ -1535,9 +1536,9 @@ class _OtherThingy extends StatelessWidget { SizedBox(width: 450, child: getShit(context, true, false)), SizedBox(width: constraints.maxWidth - 450, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), - itemCount: newsletter!.length+1, + itemCount: newsletter!.news.length+1, itemBuilder: (BuildContext context, int index) { - return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter![index-1]); + return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter!.news[index-1]); } )) ] @@ -1546,9 +1547,9 @@ class _OtherThingy extends StatelessWidget { else { return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), - itemCount: newsletter!.length+1, + itemCount: newsletter!.news.length+1, itemBuilder: (BuildContext context, int index) { - return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter![index-1]); + return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter!.news[index-1]); }, ); } diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index 9359629..b13a7d4 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -70,7 +70,7 @@ class TLProgress extends StatelessWidget{ if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), - if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) + if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x" && tlData.rank != "z") || tlData.percentileRank != "x"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x")) ? Colors.greenAccent : null)) ] ) ),