Merge pull request #62 from dan63047/stable

Sync stable with master
This commit is contained in:
dan63047 2024-02-01 17:41:24 +03:00 committed by GitHub
commit 769a53bd69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 195 additions and 195 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# My shit
lib/views/tl_match_view web.dart

View File

@ -175,6 +175,7 @@ class TetrioService extends DB {
developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode); developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode);
_replaysCache[replayID] = [response.body, response.bodyBytes]; // Puts results into the cache _replaysCache[replayID] = [response.body, response.bodyBytes]; // Puts results into the cache
return [response.body, response.bodyBytes]; return [response.body, response.bodyBytes];
// if not 200 - throw a unique for each code exception
case 404: case 404:
throw SzyNotFound(); throw SzyNotFound();
case 403: case 403:
@ -192,12 +193,14 @@ class TetrioService extends DB {
developer.log("szyDownload: Failed to download a replay", 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) { } on http.ClientException catch (e, s) { // If local http client fails
developer.log("$e, $s"); developer.log("$e, $s");
throw http.ClientException(e.message, e.uri); throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet
} }
} }
/// Saves replay with given [replayID] to Download or Documents directory as [replayID].ttrm. Throws an exception,
/// if file with name [replayID].ttrm exist, if it fails to get replay or unable to save replay
Future<String> saveReplay(String replayID) async { Future<String> saveReplay(String replayID) async {
var downloadPath = await getDownloadsDirectory(); var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
@ -208,49 +211,58 @@ class TetrioService extends DB {
return replayFile.path; return replayFile.path;
} }
/// Gets replay with given [replayID] and returns some stats about it. If [isAvailable] is false
/// or unable to get replay, it will throw an exception
Future<ReplayData> analyzeReplay(String replayID, bool isAvailable) async{ Future<ReplayData> analyzeReplay(String replayID, bool isAvailable) async{
// trying retirieve existing stats from DB first
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
final results = await db.query(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [replayID]); final results = await db.query(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [replayID]);
if (results.isNotEmpty) return ReplayData.fromJson(jsonDecode(results.first["data"].toString())); if (results.isNotEmpty) return ReplayData.fromJson(jsonDecode(results.first["data"].toString())); // if success
if (!isAvailable) throw ReplayNotAvalable(); if (!isAvailable) throw ReplayNotAvalable(); // if replay too old
Map<String, dynamic> toAnalyze = jsonDecode((await szyGetReplay(replayID))[0]);
// otherwise, actually going to download a replay and analyze it
String replay = (await szyGetReplay(replayID))[0];
Map<String, dynamic> toAnalyze = jsonDecode(replay);
ReplayData data = ReplayData.fromJson(toAnalyze); ReplayData data = ReplayData.fromJson(toAnalyze);
saveReplayStats(data); saveReplayStats(data); // saving to DB for later
return data; return data;
} }
/// Gets and returns Top TR for a player with given [id]. May return null if player top tr is unknown
/// or api is unavaliable (404). May throw an exception, if something else happens.
Future<double?> fetchTopTR(String id) async { Future<double?> fetchTopTR(String id) async {
try{ try{ // read from cache
var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id); 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 (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"); 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; return cached.value.values.first;
}else{ }else{ // if cache expired
_topTRcache.remove(cached.key); _topTRcache.remove(cached.key);
developer.log("fetchTopTR: Top TR expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); developer.log("fetchTopTR: Top TR expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
} }
}catch(e){ }catch(e){ // actually going to obtain
developer.log("fetchTopTR: Trying to retrieve Top TR", name: "services/tetrio_crud"); developer.log("fetchTopTR: Trying to retrieve Top TR", name: "services/tetrio_crud");
} }
Uri url; Uri url;
if (kIsWeb) { 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": "PeakTR", "user": id}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "PeakTR", "user": id});
} else { } else { // Actually going to hit p1nkl0bst3r api
url = Uri.https('api.p1nkl0bst3r.xyz', 'toptr/$id'); url = Uri.https('api.p1nkl0bst3r.xyz', 'toptr/$id');
} }
try{ try{
final response = await client.get(url); final response = await client.get(url);
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200: // ok - return the value
_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: double.tryParse(response.body)}; _topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: double.tryParse(response.body)};
return double.tryParse(response.body); return double.tryParse(response.body);
case 404: case 404: // not found - return 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);
_topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: null}; _topTRcache[(DateTime.now().millisecondsSinceEpoch + 300000).toString()] = {id: null};
return null; return null;
// if not 200 or 404 - throw a unique for each code exception
case 403: case 403:
throw P1nkl0bst3rForbidden(); throw P1nkl0bst3rForbidden();
case 429: case 429:
@ -266,12 +278,17 @@ class TetrioService extends DB {
developer.log("fetchTopTR: Failed to fetch top TR", name: "services/tetrio_crud", error: response.statusCode); developer.log("fetchTopTR: Failed to fetch top TR", 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) { } on http.ClientException catch (e, s) { // If local http client fails
developer.log("$e, $s"); developer.log("$e, $s");
throw http.ClientException(e.message, e.uri); throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet
} }
} }
// 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
/// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states
/// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data.
Future<List<TetrioPlayer>> fetchAndsaveTLHistory(String id) async { Future<List<TetrioPlayer>> fetchAndsaveTLHistory(String id) async {
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
@ -284,10 +301,12 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
// that one api returns csv instead of json
List<List<dynamic>> csv = const CsvToListConverter().convert(response.body)..removeAt(0); List<List<dynamic>> csv = const CsvToListConverter().convert(response.body)..removeAt(0);
List<TetrioPlayer> history = []; List<TetrioPlayer> history = [];
// doesn't return nickname, need to retrieve it separately
String nick = await getNicknameByID(id); String nick = await getNicknameByID(id);
for (List<dynamic> entry in csv){ for (List<dynamic> entry in csv){ // each entry is one state
TetrioPlayer state = TetrioPlayer( TetrioPlayer state = TetrioPlayer(
userId: id, userId: id,
username: nick, username: nick,
@ -302,27 +321,51 @@ class TetrioService extends DB {
supporterTier: 0, supporterTier: 0,
verified: false, verified: false,
connections: null, connections: null,
tlSeason1: TetraLeagueAlpha(timestamp: DateTime.parse(entry[9]), apm: entry[6] != '' ? entry[6] : null, pps: entry[7] != '' ? entry[7] : null, vs: entry[8] != '' ? entry[8] : null, glicko: entry[4], rd: noTrRd, gamesPlayed: entry[1], gamesWon: entry[2], bestRank: "z", decaying: false, rating: entry[3], rank: entry[5], percentileRank: entry[5], percentile: rankCutoffs[entry[5]]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1), tlSeason1: TetraLeagueAlpha(
timestamp: DateTime.parse(entry[9]),
apm: entry[6] != '' ? entry[6] : null,
pps: entry[7] != '' ? entry[7] : null,
vs: entry[8] != '' ? entry[8] : null,
glicko: entry[4],
rd: noTrRd,
gamesPlayed: entry[1],
gamesWon: entry[2],
bestRank: "z",
decaying: false,
rating: entry[3],
rank: entry[5],
percentileRank: entry[5],
percentile: rankCutoffs[entry[5]]!,
standing: -1,
standingLocal: -1,
nextAt: -1,
prevAt: -1
),
sprint: [], sprint: [],
blitz: [] blitz: []
); );
history.add(state); history.add(state);
} }
// trying to dump it to local DB
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
late List<TetrioPlayer> states; late List<TetrioPlayer> states;
try{ try{
// checking if tetra stats aware about that player TODO: is it necessary?
states = _players[id]!; states = _players[id]!;
}catch(e){ }catch(e){
// if somehow not - create it
var player = await fetchPlayer(id); var player = await fetchPlayer(id);
await createPlayer(player); await createPlayer(player);
states = _players[id]!; states = _players[id]!;
} }
states.insertAll(0, history.reversed); states.insertAll(0, history.reversed);
final Map<String, dynamic> statesJson = {}; final Map<String, dynamic> statesJson = {};
for (var e in states) { for (var e in states) { // making one big json out of this list
statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
} }
// and putting it to local DB
await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]); await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]);
_tetrioStreamController.add(_players); _tetrioStreamController.add(_players);
return history; return history;
@ -350,6 +393,7 @@ class TetrioService extends DB {
} }
} }
/// Retrieves full Tetra League leaderboard from Tetra Channel api. Returns a leaderboard object. Throws an exception if fails to retrieve.
Future<TetrioPlayersLeaderboard> fetchTLLeaderboard() async { Future<TetrioPlayersLeaderboard> fetchTLLeaderboard() async {
try{ try{
var cached = _leaderboardsCache.entries.firstWhere((element) => element.value.type == "league"); var cached = _leaderboardsCache.entries.firstWhere((element) => element.value.type == "league");
@ -375,14 +419,14 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
var rawJson = jsonDecode(response.body); var rawJson = jsonDecode(response.body);
if (rawJson['success']) { 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;
return leaderboard; return leaderboard;
} else { } 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);
throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side?
} }
case 403: case 403:
throw TetrioForbidden(); throw TetrioForbidden();
@ -405,6 +449,7 @@ class TetrioService extends DB {
} }
} }
/// Retrieves and returns 100 latest news entries from Tetra Channel api for given [userID]. Throws an exception if fails to retrieve.
Future<List<News>> fetchNews(String userID) async{ Future<List<News>> fetchNews(String userID) async{
try{ try{
var cached = _newsCache.entries.firstWhere((element) => element.value[0].stream == "user_$userID"); var cached = _newsCache.entries.firstWhere((element) => element.value[0].stream == "user_$userID");
@ -431,10 +476,10 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
var payload = jsonDecode(response.body); var payload = jsonDecode(response.body);
if (payload['success']) { if (payload['success']) { // if api confirmed that everything ok
List<News> news = [for (var entry in payload['data']['news']) News.fromJson(entry)]; List<News> news = [for (var entry in payload['data']['news']) News.fromJson(entry)];
developer.log("fetchNews: $userID news retrieved and cached", name: "services/tetrio_crud");
_newsCache[payload['cache']['cached_until'].toString()] = news; _newsCache[payload['cache']['cached_until'].toString()] = news;
developer.log("fetchNews: $userID news retrieved and cached", name: "services/tetrio_crud");
return news; return news;
} else { } else {
developer.log("fetchNews: User dosen't exist", name: "services/tetrio_crud", error: response.body); developer.log("fetchNews: User dosen't exist", name: "services/tetrio_crud", error: response.body);
@ -461,18 +506,20 @@ class TetrioService extends DB {
} }
} }
Future<TetraLeagueAlphaStream> getTLStream(String userID) async { /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
/// Throws an exception if fails to retrieve.
Future<TetraLeagueAlphaStream> fetchTLStream(String userID) async {
try{ try{
var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID); var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID);
if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){
developer.log("getTLStream: Stream $userID retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); 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; return cached.value;
}else{ }else{
_tlStreamsCache.remove(cached.key); _tlStreamsCache.remove(cached.key);
developer.log("getTLStream: Cached stream $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); developer.log("fetchTLStream: Cached stream $userID expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud");
} }
}catch(e){ }catch(e){
developer.log("getTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud"); developer.log("fetchTLStream: Trying to retrieve stream $userID", name: "services/tetrio_crud");
} }
Uri url; Uri url;
@ -488,11 +535,11 @@ 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);
developer.log("getTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud");
_tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream; _tlStreamsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = stream;
developer.log("fetchTLStream: $userID stream retrieved and cached", name: "services/tetrio_crud");
return stream; return stream;
} else { } else {
developer.log("getTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body); developer.log("fetchTLStream User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist(); throw TetrioPlayerNotExist();
} }
case 403: case 403:
@ -507,7 +554,7 @@ class TetrioService extends DB {
case 504: case 504:
throw TetrioInternalProblem(); throw TetrioInternalProblem();
default: default:
developer.log("getTLStream Failed to fetch stream", name: "services/tetrio_crud", error: response.statusCode); developer.log("fetchTLStream Failed to fetch stream", 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) { } on http.ClientException catch (e, s) {
@ -516,16 +563,26 @@ class TetrioService extends DB {
} }
} }
/// Saves Tetra League Matches from [stream] to the local DB.
Future<void> saveTLMatchesFromStream(TetraLeagueAlphaStream stream) async { Future<void> saveTLMatchesFromStream(TetraLeagueAlphaStream stream) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
for (TetraLeagueAlphaRecord match in stream.records) { for (TetraLeagueAlphaRecord match in stream.records) { // putting then one by one
final results = await db.query(tetraLeagueMatchesTable, where: '$replayID = ?', whereArgs: [match.replayId]); final results = await db.query(tetraLeagueMatchesTable, where: '$replayID = ?', whereArgs: [match.replayId]);
if (results.isNotEmpty) continue; if (results.isNotEmpty) continue; // if match alreay exist - skip
db.insert(tetraLeagueMatchesTable, {idCol: match.ownId, replayID: match.replayId, timestamp: match.timestamp.toString(), player1id: match.endContext.first.userId, player2id: match.endContext.last.userId, endContext1: jsonEncode(match.endContext.first.toJson()), endContext2: jsonEncode(match.endContext.last.toJson())}); db.insert(tetraLeagueMatchesTable, {
idCol: match.ownId,
replayID: match.replayId,
timestamp: match.timestamp.toString(),
player1id: match.endContext.first.userId,
player2id: match.endContext.last.userId,
endContext1: jsonEncode(match.endContext.first.toJson()),
endContext2: jsonEncode(match.endContext.last.toJson())
});
} }
} }
/// Deletes duplicate entries of Tetra League matches from local DB.
Future<void> removeDuplicatesFromTLMatches() async{ Future<void> removeDuplicatesFromTLMatches() async{
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
@ -548,17 +605,28 @@ class TetrioService extends DB {
"""); """);
} }
/// Gets and returns a list of matches from local DB for a given [playerID].
Future<List<TetraLeagueAlphaRecord>> getTLMatchesbyPlayerID(String playerID) async { Future<List<TetraLeagueAlphaRecord>> getTLMatchesbyPlayerID(String playerID) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
List<TetraLeagueAlphaRecord> matches = []; List<TetraLeagueAlphaRecord> matches = [];
final results = await db.query(tetraLeagueMatchesTable, where: '($player1id = ?) OR ($player2id = ?)', whereArgs: [playerID, playerID]); final results = await db.query(tetraLeagueMatchesTable, where: '($player1id = ?) OR ($player2id = ?)', whereArgs: [playerID, playerID]);
for (var match in results){ for (var match in results){
matches.add(TetraLeagueAlphaRecord(ownId: match[idCol].toString(), replayId: match[replayID].toString(), timestamp: DateTime.parse(match[timestamp].toString()), endContext:[EndContextMulti.fromJson(jsonDecode(match[endContext1].toString())), EndContextMulti.fromJson(jsonDecode(match[endContext2].toString()))], replayAvalable: false)); matches.add(TetraLeagueAlphaRecord(
ownId: match[idCol].toString(),
replayId: match[replayID].toString(),
timestamp: DateTime.parse(match[timestamp].toString()),
endContext:[
EndContextMulti.fromJson(jsonDecode(match[endContext1].toString())),
EndContextMulti.fromJson(jsonDecode(match[endContext2].toString()))
],
replayAvalable: false
));
} }
return matches; return matches;
} }
/// Deletes match and stats of that match with given [matchID] from local DB. Throws an exception if fails.
Future<void> deleteTLMatch(String matchID) async { Future<void> deleteTLMatch(String matchID) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
@ -570,6 +638,8 @@ 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 [playerID] from Tetra Channel api. Returns Map, which contains user id (`user`),
/// Blitz (`blitz`) and 40 Lines (`sprint`) record objects and Zen object (`zen`). Throws an exception if fails to retrieve.
Future<Map<String, dynamic>> fetchRecords(String userID) async { Future<Map<String, dynamic>> fetchRecords(String userID) async {
try{ try{
var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID); var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID);
@ -598,15 +668,15 @@ class TetrioService extends DB {
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;
var blitz = jsonRecords['data']['records']['blitz']['record'] != null var blitz = jsonRecords['data']['records']['blitz']['record'] != null
? [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;
var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); var zen = TetrioZen.fromJson(jsonRecords['data']['zen']);
Map<String, dynamic> map = {"user": userID.toLowerCase().trim(), "sprint": sprint, "blitz": blitz, "zen": zen}; Map<String, dynamic> map = {"user": userID.toLowerCase().trim(), "sprint": sprint, "blitz": blitz, "zen": zen};
developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud");
_recordsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = map; _recordsCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = map;
developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud");
return map; 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);
@ -633,13 +703,18 @@ class TetrioService extends DB {
} }
} }
/// Creates an entry in local DB for [tetrioPlayer]. Throws an exception if that player already here.
Future<void> createPlayer(TetrioPlayer tetrioPlayer) async { Future<void> createPlayer(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
// checking if its already here
final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId.toLowerCase()]); final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId.toLowerCase()]);
if (results.isNotEmpty) { if (results.isNotEmpty) {
throw TetrioPlayerAlreadyExist(); throw TetrioPlayerAlreadyExist();
} }
// converting to json and store
final Map<String, dynamic> statesJson = {(tetrioPlayer.state.millisecondsSinceEpoch ~/ 1000).toString(): tetrioPlayer.toJson()}; final Map<String, dynamic> statesJson = {(tetrioPlayer.state.millisecondsSinceEpoch ~/ 1000).toString(): tetrioPlayer.toJson()};
db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}); db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)});
_players.addEntries({ _players.addEntries({
@ -648,6 +723,7 @@ class TetrioService extends DB {
_tetrioStreamController.add(_players); _tetrioStreamController.add(_players);
} }
/// Adds user id of [tetrioPlayer] to the [tetrioUsersToTrackTable] of database.
Future<void> addPlayerToTrack(TetrioPlayer tetrioPlayer) async { Future<void> addPlayerToTrack(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
@ -658,6 +734,7 @@ class TetrioService extends DB {
db.insert(tetrioUsersToTrackTable, {idCol: tetrioPlayer.userId}); db.insert(tetrioUsersToTrackTable, {idCol: tetrioPlayer.userId});
} }
/// Returns bool, which tells whether is given [id] is in [tetrioUsersToTrackTable].
Future<bool> isPlayerTracking(String id) async { Future<bool> isPlayerTracking(String id) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
@ -665,6 +742,7 @@ class TetrioService extends DB {
return results.isNotEmpty; return results.isNotEmpty;
} }
/// Returns Iterable with user ids of players who is tracked.
Future<Iterable<String>> getAllPlayerToTrack() async { Future<Iterable<String>> getAllPlayerToTrack() async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
@ -673,6 +751,7 @@ class TetrioService extends DB {
return players.map((noteRow) => noteRow["id"].toString()); return players.map((noteRow) => noteRow["id"].toString());
} }
/// Removes user with given [id] from the [tetrioUsersToTrackTable] of database.
Future<void> deletePlayerToTrack(String id) async { Future<void> deletePlayerToTrack(String id) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
@ -685,56 +764,71 @@ class TetrioService extends DB {
} }
} }
/// Saves state (which is [tetrioPlayer]) to the local database.
Future<void> storeState(TetrioPlayer tetrioPlayer) async { Future<void> storeState(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
late List<TetrioPlayer> states; late List<TetrioPlayer> states;
try { try { // retrieveing previous states
states = _players[tetrioPlayer.userId]!; states = _players[tetrioPlayer.userId]!;
} catch (e) { } catch (e) { // nothing found - player not exist - create them
await createPlayer(tetrioPlayer); await createPlayer(tetrioPlayer);
states = await getPlayer(tetrioPlayer.userId); states = await getPlayer(tetrioPlayer.userId);
} }
// we not going to add state, that is same, as the previous
bool test = _players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer); bool test = _players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer);
if (test == false) states.add(tetrioPlayer); if (test == false) states.add(tetrioPlayer);
// Making map of the states
final Map<String, dynamic> statesJson = {}; final Map<String, dynamic> statesJson = {};
for (var e in states) { for (var e in states) {
// Saving in format: {"unix_seconds": json_of_state}
statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
} }
// Rewrite our database
await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)},
where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]);
_players[tetrioPlayer.userId]!.add(tetrioPlayer); _players[tetrioPlayer.userId]!.add(tetrioPlayer);
_tetrioStreamController.add(_players); _tetrioStreamController.add(_players);
} }
/// Remove state (which is [tetrioPlayer]) from the local database
Future<void> deleteState(TetrioPlayer tetrioPlayer) async { Future<void> deleteState(TetrioPlayer tetrioPlayer) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
late List<TetrioPlayer> states; late List<TetrioPlayer> states;
states = await getPlayer(tetrioPlayer.userId); // removing state from map that contain every state of each user
_players[tetrioPlayer.userId]!.removeWhere((element) => element.state == tetrioPlayer.state); _players[tetrioPlayer.userId]!.removeWhere((element) => element.state == tetrioPlayer.state);
states = _players[tetrioPlayer.userId]!; states = _players[tetrioPlayer.userId]!;
// Making map of the states (without deleted one)
final Map<String, dynamic> statesJson = {}; final Map<String, dynamic> statesJson = {};
for (var e in states) { for (var e in states) {
statesJson.addEntries({e.state.millisecondsSinceEpoch.toString(): e.toJson()}.entries); statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
} }
// Rewriting database entry with new json
await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)},
where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]);
_players[tetrioPlayer.userId]!.add(tetrioPlayer); _players[tetrioPlayer.userId]!.add(tetrioPlayer);
_tetrioStreamController.add(_players); _tetrioStreamController.add(_players);
} }
/// Returns list of all states of player with given [id] from database. Can return empty list if player
/// was not found.
Future<List<TetrioPlayer>> getPlayer(String id) async { Future<List<TetrioPlayer>> getPlayer(String id) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();
List<TetrioPlayer> states = []; List<TetrioPlayer> states = [];
final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]);
if (results.isEmpty) { if (results.isEmpty) {
return states; return states; // it empty
} else { } else {
dynamic rawStates = results.first['jsonStates'] as String; dynamic rawStates = results.first['jsonStates'] as String;
rawStates = json.decode(rawStates); rawStates = json.decode(rawStates);
// recreating objects of states
rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String))); rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String)));
// updating the stream
_players.removeWhere((key, value) => key == id); _players.removeWhere((key, value) => key == id);
_players.addEntries({states.last.userId: states}.entries); _players.addEntries({states.last.userId: states}.entries);
_tetrioStreamController.add(_players); _tetrioStreamController.add(_players);
@ -742,6 +836,8 @@ class TetrioService extends DB {
} }
} }
/// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user.
/// If [isItDiscordID] is true, function expects [user] to be a discord user id. Throws an exception if fails to retrieve.
Future<TetrioPlayer> fetchPlayer(String user, {bool isItDiscordID = false}) async { Future<TetrioPlayer> fetchPlayer(String user, {bool isItDiscordID = false}) async {
try{ try{
var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user); var cached = _playersCache.entries.firstWhere((element) => element.value.userId == user || element.value.username == user);
@ -757,6 +853,7 @@ class TetrioService extends DB {
} }
if (isItDiscordID){ if (isItDiscordID){
// trying to find player with given discord id
Uri dUrl; Uri dUrl;
if (kIsWeb) { if (kIsWeb) {
dUrl = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()}); dUrl = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()});
@ -770,12 +867,14 @@ class TetrioService extends DB {
case 200: case 200:
var json = jsonDecode(response.body); var json = jsonDecode(response.body);
if (json['success'] && json['data'] != null) { if (json['success'] && json['data'] != null) {
// success - rewrite user with tetrio user id and going to obtain data about him
user = json['data']['user']['_id']; user = json['data']['user']['_id'];
} else { } else { // fail - throw an exception
developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body);
throw TetrioPlayerNotExist(); throw TetrioPlayerNotExist();
} }
break; break;
// more exceptions to god of exceptions
case 403: case 403:
throw TetrioForbidden(); throw TetrioForbidden();
case 429: case 429:
@ -797,6 +896,7 @@ class TetrioService extends DB {
} }
} }
// finally going to obtain
Uri url; Uri url;
if (kIsWeb) { if (kIsWeb) {
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()}); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()});
@ -810,9 +910,10 @@ class TetrioService extends DB {
case 200: case 200:
var json = jsonDecode(response.body); var json = jsonDecode(response.body);
if (json['success']) { 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']); TetrioPlayer player = TetrioPlayer.fromJson(json['data']['user'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['user']['_id'], json['data']['user']['username']);
developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud");
_playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player; _playersCache[jsonDecode(response.body)['cache']['cached_until'].toString()] = player;
developer.log("fetchPlayer: $user retrieved and cached", name: "services/tetrio_crud");
return player; return player;
} else { } else {
developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body); developer.log("fetchPlayer User dosen't exist", name: "services/tetrio_crud", error: response.body);
@ -839,6 +940,8 @@ class TetrioService extends DB {
} }
} }
/// Basucally, retrieves whole [tetrioUsersTable] and do stupud things idk
/// Returns god knows what. TODO: Rewrite this shit
Future<Iterable<Map<String, List<TetrioPlayer>>>> getAllPlayers() async { Future<Iterable<Map<String, List<TetrioPlayer>>>> getAllPlayers() async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); final db = getDatabaseOrThrow();

View File

@ -156,7 +156,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
late List<News> news; late List<News> news;
late double? topTR; late double? topTR;
requests = await Future.wait([ // all at once requests = await Future.wait([ // all at once
teto.getTLStream(_searchFor), teto.fetchTLStream(_searchFor),
teto.fetchRecords(_searchFor), teto.fetchRecords(_searchFor),
teto.fetchNews(_searchFor), teto.fetchNews(_searchFor),
if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR
@ -173,10 +173,13 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
TetraLeagueAlpha? compareWith; TetraLeagueAlpha? compareWith;
Set<TetraLeagueAlpha> uniqueTL = {}; Set<TetraLeagueAlpha> uniqueTL = {};
tlMatches = tlStream.records; tlMatches = tlStream.records;
var storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches
if (isTracking){ // if tracked - save data to local DB if (isTracking){ // if tracked - save data to local DB
await teto.storeState(me); await teto.storeState(me);
await teto.saveTLMatchesFromStream(tlStream); await teto.saveTLMatchesFromStream(tlStream);
var storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches }
// building list of TL matches
for (var match in storedRecords) { for (var match in storedRecords) {
// add stored match to list only if it missing from retrived ones // add stored match to list only if it missing from retrived ones
if (!tlMatches.contains(match)) tlMatches.add(match); if (!tlMatches.contains(match)) tlMatches.add(match);
@ -186,8 +189,7 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
if(a.timestamp.isAtSameMomentAs(b.timestamp)) return 0; if(a.timestamp.isAtSameMomentAs(b.timestamp)) return 0;
if(a.timestamp.isAfter(b.timestamp)) return -1; if(a.timestamp.isAfter(b.timestamp)) return -1;
return 0; return 0;
}); });
}
// Handling history // Handling history
if(fetchHistory) await teto.fetchAndsaveTLHistory(_searchFor); // Retrieve if needed if(fetchHistory) await teto.fetchAndsaveTLHistory(_searchFor); // Retrieve if needed
@ -309,8 +311,6 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
case ConnectionState.done: case ConnectionState.done:
//bool bigScreen = MediaQuery.of(context).size.width > 1024; //bool bigScreen = MediaQuery.of(context).size.width > 1024;
if (snapshot.hasData) { if (snapshot.hasData) {
List<dynamic> sprintRuns = snapshot.data![1]['sprint'];
List<dynamic> blitzRuns = snapshot.data![1]['blitz'];
return RefreshIndicator( return RefreshIndicator(
onRefresh: () { onRefresh: () {
return Future(() => changePlayer(snapshot.data![0].userId)); return Future(() => changePlayer(snapshot.data![0].userId));
@ -356,8 +356,8 @@ class _MainState extends State<MainView> with TickerProviderStateMixin {
TLThingy(tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, states: snapshot.data![2], topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon"), TLThingy(tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, states: snapshot.data![2], topTR: snapshot.data![7], bot: snapshot.data![0].role == "bot", guest: snapshot.data![0].role == "anon"),
_TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]), _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]),
_History(states: snapshot.data![2], update: _justUpdate), _History(states: snapshot.data![2], update: _justUpdate),
_RecordThingy(record: sprintRuns.elementAtOrNull(0)), _RecordThingy(record: snapshot.data![1]['sprint']),
_RecordThingy(record: blitzRuns.elementAtOrNull(0)), _RecordThingy(record: snapshot.data![1]['blitz']),
_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],)
], ],
), ),

View File

@ -392,7 +392,14 @@ class UserThingy extends StatelessWidget {
width: 32, width: 32,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace); developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace);
return Image.asset("res/icons/kagari.png", height: 32, width: 32); return Image.network(
"https://tetr.io/res/badges/${badge.badgeId}.png",
height: 32,
width: 32,
errorBuilder:(context, error, stackTrace) {
return Image.asset("res/icons/kagari.png", height: 32, width: 32);
}
);
}, },
)) ))
], ],

View File

@ -82,129 +82,10 @@ targets:
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- res/avatars/tetrio_anon.png - res/avatars/
- res/avatars/tetrio_banned.png - res/icons/
- res/icons/kagari.png - res/tetrio_tl_alpha_ranks/
- res/icons/osk.svg - res/tetrio_badges/
- res/icons/tetrio-logo.svg
- res/icons/improvement-local.png
- res/icons/supporter-tag.png
- res/tetrio_tl_alpha_ranks/x.png
- res/tetrio_tl_alpha_ranks/u.png
- res/tetrio_tl_alpha_ranks/ss.png
- res/tetrio_tl_alpha_ranks/s+.png
- res/tetrio_tl_alpha_ranks/s.png
- res/tetrio_tl_alpha_ranks/s-.png
- res/tetrio_tl_alpha_ranks/a+.png
- res/tetrio_tl_alpha_ranks/a.png
- res/tetrio_tl_alpha_ranks/a-.png
- res/tetrio_tl_alpha_ranks/b+.png
- res/tetrio_tl_alpha_ranks/b.png
- res/tetrio_tl_alpha_ranks/b-.png
- res/tetrio_tl_alpha_ranks/c+.png
- res/tetrio_tl_alpha_ranks/c.png
- res/tetrio_tl_alpha_ranks/c-.png
- res/tetrio_tl_alpha_ranks/d+.png
- res/tetrio_tl_alpha_ranks/d.png
- res/tetrio_tl_alpha_ranks/z.png
- res/tetrio_badges/5mblast_1.png
- res/tetrio_badges/5mblast_10.png
- res/tetrio_badges/5mblast_100.png
- res/tetrio_badges/5mblast_1000.png
- res/tetrio_badges/20tsd.png
- res/tetrio_badges/100player.png
- res/tetrio_badges/allclear.png
- res/tetrio_badges/bugbounty.png
- res/tetrio_badges/cometopen_1.png
- res/tetrio_badges/cometopen_2.png
- res/tetrio_badges/cometopen_3.png
- res/tetrio_badges/early-supporter.png
- res/tetrio_badges/founder.png
- res/tetrio_badges/galactic2x2_1.png
- res/tetrio_badges/galactic2x2_2.png
- res/tetrio_badges/galactic2x2_3.png
- res/tetrio_badges/ggc_1.png
- res/tetrio_badges/ggc_2.png
- res/tetrio_badges/ggc_3.png
- res/tetrio_badges/hdoxii_1.png
- res/tetrio_badges/hdoxii_2.png
- res/tetrio_badges/hdoxii_3.png
- res/tetrio_badges/heart.png
- res/tetrio_badges/hnprism_1.png
- res/tetrio_badges/hnprism_2.png
- res/tetrio_badges/hnprism_3.png
- res/tetrio_badges/hnstratosphere50_1.png
- res/tetrio_badges/hnstratosphere50_2.png
- res/tetrio_badges/hnstratosphere50_3.png
- res/tetrio_badges/ift_1.png
- res/tetrio_badges/ift_2.png
- res/tetrio_badges/ift_3.png
- res/tetrio_badges/infdev.png
- res/tetrio_badges/kod_by_founder.png
- res/tetrio_badges/kod_founder.png
- res/tetrio_badges/leaderboard1.png
- res/tetrio_badges/mmc_tabi_1.png
- res/tetrio_badges/mmc_tabi_2.png
- res/tetrio_badges/mmc_tabi_3.png
- res/tetrio_badges/mmc_tabi_superlobby.png
- res/tetrio_badges/mmc_tabi_superlobby2.png
- res/tetrio_badges/mmc_tabi_superlobby3.png
- res/tetrio_badges/mmc_tabi_superlobby4.png
- res/tetrio_badges/redgevo_1.png
- res/tetrio_badges/redgevo_2.png
- res/tetrio_badges/redgevo_3.png
- res/tetrio_badges/rengervl_1.png
- res/tetrio_badges/rengervl_2.png
- res/tetrio_badges/rengervl_3.png
- res/tetrio_badges/sakurablend_1.png
- res/tetrio_badges/sakurablend_2.png
- res/tetrio_badges/sakurablend_3.png
- res/tetrio_badges/scuncapped_1.png
- res/tetrio_badges/scuncapped_2.png
- res/tetrio_badges/scuncapped_3.png
- res/tetrio_badges/secretgrade.png
- res/tetrio_badges/sfu_raccoon_1.png
- res/tetrio_badges/sfu_raccoon_2.png
- res/tetrio_badges/sfu_raccoon_3.png
- res/tetrio_badges/streamersuperlobby.png
- res/tetrio_badges/superlobby.png
- res/tetrio_badges/superlobby2.png
- res/tetrio_badges/taws_u50_1.png
- res/tetrio_badges/taws_u50_2.png
- res/tetrio_badges/taws_u50_3.png
- res/tetrio_badges/tawshdsl_capped.png
- res/tetrio_badges/tawshdsl_uncapped.png
- res/tetrio_badges/tawsignite_expert.png
- res/tetrio_badges/tawslg.png
- res/tetrio_badges/tetralympic_masters.png
- res/tetrio_badges/thaitour_1.png
- res/tetrio_badges/thaitour_2.png
- res/tetrio_badges/thaitour_3.png
- res/tetrio_badges/ttsdpf_1.png
- res/tetrio_badges/ttsdpf_2.png
- res/tetrio_badges/ttsdpf_3.png
- res/tetrio_badges/ttsdtc_1.png
- res/tetrio_badges/ttsdtc_2.png
- res/tetrio_badges/ttsdtc_3.png
- res/tetrio_badges/twc23_1.png
- res/tetrio_badges/twc23_2.png
- res/tetrio_badges/twc23_3.png
- res/tetrio_badges/twc23_4.png
- res/tetrio_badges/ubcea_1.png
- res/tetrio_badges/ubcea_2.png
- res/tetrio_badges/ubcea_3.png
- res/tetrio_badges/underdog_1.png
- res/tetrio_badges/underdog_2.png
- res/tetrio_badges/underdog_3.png
- res/tetrio_badges/underdog_predict.png
- res/tetrio_badges/wpl_1.png
- res/tetrio_badges/wpl_2.png
- res/tetrio_badges/wpl_3.png
- res/tetrio_badges/wplc_1.png
- res/tetrio_badges/wplc_2.png
- res/tetrio_badges/wplc_3.png
- res/tetrio_badges/wplc_participation.png
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
res/tetrio_badges/sii_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
res/tetrio_badges/sii_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
res/tetrio_badges/sii_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -13,21 +13,27 @@ import 'package:tetra_stats/services/tetrio_crud.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized(); DartPluginRegistrant.ensureInitialized();
if (kIsWeb) { late TetrioService teto;
sqfliteFfiInit(); setUp(() {
databaseFactory = databaseFactoryFfiWeb; if (kIsWeb) {
} else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { sqfliteFfiInit();
sqfliteFfiInit(); databaseFactory = databaseFactoryFfiWeb;
databaseFactory = databaseFactoryFfi; } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
} sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
teto = TetrioService();
});
test("Initialize TetrioServise", () async { test("Initialize TetrioServise", () async {
await TetrioService().open(); teto.open();
}, skip: true); // a fucking MissingPluginException how does that even happening? }); // a fucking MissingPluginException how does that even happening?
// i guess i will be unable to test iteractions with DB // i guess i will be unable to test iteractions with DB
group("Test fetchPlayer with different players", () { group("Test fetchPlayer with different players", () {
// those tests exist in order to detect a tiny little change in Tetra Channel API in case of some update.
test("dan63047 (user who have activity in tetra league)", () async { test("dan63047 (user who have activity in tetra league)", () async {
TetrioPlayer dan63047 = await TetrioService().fetchPlayer("6098518e3d5155e6ec429cdc"); TetrioPlayer dan63047 = await teto.fetchPlayer("6098518e3d5155e6ec429cdc");
expect(dan63047.userId, "6098518e3d5155e6ec429cdc"); expect(dan63047.userId, "6098518e3d5155e6ec429cdc");
expect(dan63047.registrationTime != null, true); expect(dan63047.registrationTime != null, true);
expect(dan63047.avatarRevision != null, true); expect(dan63047.avatarRevision != null, true);
@ -51,7 +57,7 @@ void main() {
expect(dan63047.tlSeason1.playstyle != null, true); expect(dan63047.tlSeason1.playstyle != null, true);
}); });
test("osk (sysop who have activity in tetra league)", () async { test("osk (sysop who have activity in tetra league)", () async {
TetrioPlayer osk = await TetrioService().fetchPlayer("5e32fc85ab319c2ab1beb07c"); TetrioPlayer osk = await teto.fetchPlayer("5e32fc85ab319c2ab1beb07c");
expect(osk.userId, "5e32fc85ab319c2ab1beb07c"); expect(osk.userId, "5e32fc85ab319c2ab1beb07c");
expect(osk.registrationTime, null); expect(osk.registrationTime, null);
expect(osk.country, "XM"); expect(osk.country, "XM");
@ -79,7 +85,7 @@ void main() {
expect(osk.tlSeason1.playstyle != null, true); expect(osk.tlSeason1.playstyle != null, true);
}); });
test("kagari (sysop who have zero activity)", () async { test("kagari (sysop who have zero activity)", () async {
TetrioPlayer kagari = await TetrioService().fetchPlayer("5e331c3ce24a5a3e258f7a1b"); TetrioPlayer kagari = await teto.fetchPlayer("5e331c3ce24a5a3e258f7a1b");
expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b"); expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b");
expect(kagari.registrationTime, null); expect(kagari.registrationTime, null);
expect(kagari.country, "XM"); expect(kagari.country, "XM");
@ -112,7 +118,7 @@ void main() {
expect(kagari.tlSeason1.playstyle, null); expect(kagari.tlSeason1.playstyle, null);
}); });
test("furry (banned account)", () async { test("furry (banned account)", () async {
TetrioPlayer furry = await TetrioService().fetchPlayer("5eea0ff69a1ba76c20347086"); TetrioPlayer furry = await teto.fetchPlayer("5eea0ff69a1ba76c20347086");
expect(furry.userId, "5eea0ff69a1ba76c20347086"); expect(furry.userId, "5eea0ff69a1ba76c20347086");
expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z")); expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z"));
expect(furry.role, "banned"); expect(furry.role, "banned");
@ -143,7 +149,7 @@ void main() {
expect(furry.tlSeason1.playstyle, null); expect(furry.tlSeason1.playstyle, null);
}); });
test("oskwarefan (anon account)", () async { test("oskwarefan (anon account)", () async {
TetrioPlayer oskwarefan = await TetrioService().fetchPlayer("646cb8273e887a054d64febe"); TetrioPlayer oskwarefan = await teto.fetchPlayer("646cb8273e887a054d64febe");
expect(oskwarefan.userId, "646cb8273e887a054d64febe"); expect(oskwarefan.userId, "646cb8273e887a054d64febe");
expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z")); expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z"));
expect(oskwarefan.role, "anon"); expect(oskwarefan.role, "anon");
@ -174,7 +180,7 @@ void main() {
}); });
test("not existing account", () async { test("not existing account", () async {
var future = TetrioService().fetchPlayer("hasdbashdbs"); var future = teto.fetchPlayer("hasdbashdbs");
await expectLater(future, throwsA(isA<TetrioPlayerNotExist>())); await expectLater(future, throwsA(isA<TetrioPlayerNotExist>()));
}); });
}); });