diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 1e105df..c2eb46b 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 2 /// Strings: 1016 (508 per locale) /// -/// Built on 2024-02-03 at 12:49 UTC +/// Built on 2024-02-06 at 20:25 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -224,8 +224,8 @@ class Translations implements BaseTranslations { String get importCancelled => 'Operation was cancelled'; String get importSuccess => 'Import successful'; String get yourID => 'Your TETR.IO account'; - String get yourIDAlertTitle => 'Your TETR.IO account nickname or ID'; - String get yourIDText => 'Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed.'; + String get yourIDAlertTitle => 'Your nickname in TETR.IO'; + String get yourIDText => 'When app loads, it will retrieve data for this account'; String get language => 'Language'; String get aboutApp => 'About app'; String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; @@ -816,8 +816,8 @@ class _StringsRu implements Translations { @override String get importCancelled => 'Операция была отменена'; @override String get importSuccess => 'Успешно импортировано'; @override String get yourID => 'Ваш аккаунт в TETR.IO'; - @override String get yourIDAlertTitle => 'Никнейм или ID вашего аккаунта в TETR.IO'; - @override String get yourIDText => 'Каждый раз, когда приложение запускается, приложение будет получать статистику этого игрока. Пожалуйста, отдайте предпочтение ID, так как никнейм можно изменить.'; + @override String get yourIDAlertTitle => 'Ваш ник в TETR.IO'; + @override String get yourIDText => 'При запуске приложения оно будет получать статистику этого игрока.'; @override String get language => 'Язык (Language)'; @override String get aboutApp => 'О приложении'; @override String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; @@ -1400,8 +1400,8 @@ extension on Translations { case 'importCancelled': return 'Operation was cancelled'; case 'importSuccess': return 'Import successful'; case 'yourID': return 'Your TETR.IO account'; - case 'yourIDAlertTitle': return 'Your TETR.IO account nickname or ID'; - case 'yourIDText': return 'Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed.'; + case 'yourIDAlertTitle': return 'Your nickname in TETR.IO'; + case 'yourIDText': return 'When app loads, it will retrieve data for this account'; case 'language': return 'Language'; case 'aboutApp': return 'About app'; case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; @@ -1918,8 +1918,8 @@ extension on _StringsRu { case 'importCancelled': return 'Операция была отменена'; case 'importSuccess': return 'Успешно импортировано'; case 'yourID': return 'Ваш аккаунт в TETR.IO'; - case 'yourIDAlertTitle': return 'Никнейм или ID вашего аккаунта в TETR.IO'; - case 'yourIDText': return 'Каждый раз, когда приложение запускается, приложение будет получать статистику этого игрока. Пожалуйста, отдайте предпочтение ID, так как никнейм можно изменить.'; + case 'yourIDAlertTitle': return 'Ваш ник в TETR.IO'; + case 'yourIDText': return 'При запуске приложения оно будет получать статистику этого игрока.'; case 'language': return 'Язык (Language)'; case 'aboutApp': return 'О приложении'; case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 7f8843b..b5bf668 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -27,7 +27,7 @@ const String endContext2 = "endContext2"; const String statesCol = "jsonStates"; const String player1id = "player1id"; const String player2id = "player2id"; -/// Table, that store players data, their stats and some moments of time +/// Table, that store players data, their stats at some moments of time const String createTetrioUsersTable = ''' CREATE TABLE IF NOT EXISTS "tetrioUsers" ( "id" TEXT UNIQUE, @@ -66,7 +66,7 @@ const String createTetrioTLReplayStats = ''' '''; class TetrioService extends DB { - Map> _players = {}; + 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} @@ -82,9 +82,9 @@ class TetrioService extends DB { /// We should have only one instanse of this service static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; - late final StreamController>> _tetrioStreamController; + late final StreamController> _tetrioStreamController; TetrioService._sharedInstance() { - _tetrioStreamController = StreamController>>.broadcast(onListen: () { + _tetrioStreamController = StreamController>.broadcast(onListen: () { _tetrioStreamController.sink.add(_players); }); } @@ -95,17 +95,15 @@ class TetrioService extends DB { await _loadPlayers(); } - Stream>> get allPlayers => _tetrioStreamController.stream; + Stream> get allPlayers => _tetrioStreamController.stream; /// Loading and sending to the stream everyone. Future _loadPlayers() async { - final allPlayers = await getAllPlayers(); - try{ - _players = allPlayers.toList().first; // ??? - }catch (e){ - developer.log("_loadPlayers: allPlayers.toList().first did oopsie", name: "services/tetrio_crud", error: e); - _players = {}; + final allPlayers = await getAllPlayerToTrack(); + for (var element in allPlayers) { + _players[element] = await getNicknameByID(element); } + developer.log("_loadPlayers: $_players", name: "services/tetrio_crud"); _tetrioStreamController.add(_players); } @@ -128,7 +126,10 @@ class TetrioService extends DB { Future getNicknameByID(String id) async { if (id.length <= 16) return id; // nicknames can be up to 16 symbols in length, that's how i'm differentiate nickname from ids try{ - return await getPlayer(id).then((value) => value.last.username); + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + var request = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + return request.first[nickCol] as String; } catch (e){ return await fetchPlayer(id).then((value) => value.username); } @@ -350,16 +351,8 @@ class TetrioService extends DB { // trying to dump it to local DB await ensureDbIsOpen(); final db = getDatabaseOrThrow(); - late List states; - try{ - // checking if tetra stats aware about that player TODO: is it necessary? - states = _players[id]!; - }catch(e){ - // if somehow not - create it - var player = await fetchPlayer(id); - await createPlayer(player); - states = _players[id]!; - } + List states = await getPlayer(id); + if (states.isEmpty) await createPlayer(history.first); states.insertAll(0, history.reversed); final Map statesJson = {}; for (var e in states) { // making one big json out of this list @@ -367,7 +360,6 @@ class TetrioService extends DB { } // and putting it to local DB await db.update(tetrioUsersTable, {idCol: id, nickCol: nick, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [id]); - _tetrioStreamController.add(_players); return history; case 404: developer.log("fetchTLHistory: Probably, history doesn't exist", name: "services/tetrio_crud", error: response.statusCode); @@ -732,7 +724,7 @@ class TetrioService extends DB { 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`), + /// 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{ @@ -811,9 +803,7 @@ class TetrioService extends DB { // converting to json and store final Map statesJson = {(tetrioPlayer.state.millisecondsSinceEpoch ~/ 1000).toString(): tetrioPlayer.toJson()}; db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}); - _players.addEntries({ - tetrioPlayer.userId: [tetrioPlayer] - }.entries); + _players.addEntries({tetrioPlayer.userId: tetrioPlayer.username}.entries); _tetrioStreamController.add(_players); } @@ -841,7 +831,6 @@ class TetrioService extends DB { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersToTrackTable); - developer.log("getAllPlayerToTrack: $players", name: "services/tetrio_crud"); return players.map((noteRow) => noteRow["id"].toString()); } @@ -853,25 +842,22 @@ class TetrioService extends DB { if (deletedPlayer != 1) { throw CouldNotDeletePlayer(); } else { - // _players.removeWhere((key, value) => key == id); - // _tetrioStreamController.add(_players); + _players.removeWhere((key, value) => key == id); + _tetrioStreamController.add(_players); } } /// Saves state (which is [tetrioPlayer]) to the local database. Future storeState(TetrioPlayer tetrioPlayer) async { - await ensureDbIsOpen(); - final db = getDatabaseOrThrow(); - late List states; - try { // retrieveing previous states - states = _players[tetrioPlayer.userId]!; - } catch (e) { // nothing found - player not exist - create them - await createPlayer(tetrioPlayer); - states = await getPlayer(tetrioPlayer.userId); + // if tetrio player doesn't have entry in database - just calling different function + List states = await getPlayer(tetrioPlayer.userId); + if (states.isEmpty) { + await createPlayer(tetrioPlayer); + return; } // we not going to add state, that is same, as the previous - bool test = _players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer); + bool test = states.last.isSameState(tetrioPlayer); if (test == false) states.add(tetrioPlayer); // Making map of the states @@ -880,21 +866,21 @@ class TetrioService extends DB { // Saving in format: {"unix_seconds": json_of_state} statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries); } + // Rewrite our database + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); - _players[tetrioPlayer.userId]!.add(tetrioPlayer); - _tetrioStreamController.add(_players); } /// Remove state (which is [tetrioPlayer]) from the local database Future deleteState(TetrioPlayer tetrioPlayer) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); - late List states; + List 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); - states = _players[tetrioPlayer.userId]!; + states.removeWhere((element) => element.state == tetrioPlayer.state); // Making map of the states (without deleted one) final Map statesJson = {}; @@ -904,7 +890,6 @@ class TetrioService extends DB { // Rewriting database entry with new json await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]); - _players[tetrioPlayer.userId]!.add(tetrioPlayer); _tetrioStreamController.add(_players); } @@ -924,7 +909,7 @@ class TetrioService extends DB { 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.addEntries({states.last.userId: states}.entries); + _players.addEntries({states.last.userId: states.last.username}.entries); _tetrioStreamController.add(_players); return states; } @@ -1034,20 +1019,18 @@ class TetrioService extends DB { } } - /// Basucally, retrieves whole [tetrioUsersTable] and do stupud things idk - /// Returns god knows what. TODO: Rewrite this shit - Future>>> getAllPlayers() async { + /// Retrieves whole [tetrioUsersTable] and returns Map with [TetrioPlayer] objects of everyone in database + Future>> getAllPlayers() async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersTable); Map> data = {}; - return players.map((row) { - // what the fuck am i doing here? - var test = json.decode(row['jsonStates'] as String); + for (var entry in players){ + var test = json.decode(entry['jsonStates'] as String); List states = []; - test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), row[idCol] as String, row[nickCol] as String))); + test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), entry[idCol] as String, entry[nickCol] as String))); data.addEntries({states.last.userId: states}.entries); - return data; - }); + } + return data; } } diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index af5eb19..66d0d54 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -494,8 +494,9 @@ class _NavDrawerState extends State { case ConnectionState.waiting: case ConnectionState.active: final allPlayers = (snapshot.data != null) - ? snapshot.data as Map> - : >{}; + ? snapshot.data as Map + : {}; + allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted List keys = allPlayers.keys.toList(); return NestedScrollView( headerSliverBuilder: (context, value) { @@ -550,7 +551,7 @@ class _NavDrawerState extends State { itemBuilder: (context, index) { var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. return ListTile( - title: Text(allPlayers[keys[i]]?.last.username as String), // Takes last known username from list of states + title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states onTap: () { widget.changePlayer(keys[i]); // changes to chosen player Navigator.of(context).pop(); // and closes itself. diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index a0a0807..6edb8b9 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/main.dart' show packageInfo; import 'package:file_selector/file_selector.dart'; import 'package:file_picker/file_picker.dart'; @@ -66,6 +67,11 @@ class SettingsState extends State { await _setDefaultNickname(player); } + Future _removePlayer() async { + await prefs.remove('player'); + await _setDefaultNickname("dan63047"); + } + @override Widget build(BuildContext context) { final t = Translations.of(context); @@ -212,9 +218,21 @@ class SettingsState extends State { ), TextButton( child: Text(t.popupActions.submit), - onPressed: () { - _setPlayer(_playertext.text.toLowerCase().trim()); - Navigator.of(context).pop(); + onPressed: () async { + if (_playertext.text.isEmpty) { + _removePlayer(); + Navigator.of(context).pop(); + return; + } + late TetrioPlayer user; + try{ + user = await teto.fetchPlayer(_playertext.text.toLowerCase().trim()); + }on Exception{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.noSuchUser))); + return; + } + _setPlayer(user.userId); + if (context.mounted) Navigator.of(context).pop(); setState(() {}); }, ) diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart index 1916dbb..2b11976 100644 --- a/lib/views/tracked_players_view.dart +++ b/lib/views/tracked_players_view.dart @@ -70,14 +70,15 @@ class TrackedPlayersState extends State { ), backgroundColor: Colors.black, body: SafeArea( - child: StreamBuilder( - stream: teto.allPlayers, + child: FutureBuilder( + future: teto.getAllPlayers(), builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: - return const Center(child: Text('none case of StreamBuilder')); case ConnectionState.waiting: case ConnectionState.active: + return const Center(child: CircularProgressIndicator(color: Colors.white)); + case ConnectionState.done: final allPlayers = (snapshot.data != null) ? snapshot.data as Map> : >{}; List keys = allPlayers.keys.toList(); return NestedScrollView( @@ -114,7 +115,7 @@ class TrackedPlayersState extends State { icon: const Icon(Icons.delete_forever), onPressed: () { String nn = allPlayers[keys[index]]!.last.username; - teto.deletePlayer(keys[index]); + setState(() {teto.deletePlayer(keys[index]);}); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: nn)))); }, ), @@ -128,10 +129,6 @@ class TrackedPlayersState extends State { }, ); })); - case ConnectionState.done: - return const Center( - child: Text('done case of StreamBuilder', - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center)); } })), ); diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 92042ac..d063400 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -89,8 +89,8 @@ "importCancelled": "Operation was cancelled", "importSuccess": "Import successful", "yourID": "Your TETR.IO account", - "yourIDAlertTitle": "Your TETR.IO account nickname or ID", - "yourIDText": "Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed.", + "yourIDAlertTitle": "Your nickname in TETR.IO", + "yourIDText": "When app loads, it will retrieve data for this account", "language": "Language", "aboutApp": "About app", "aboutAppText": "${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index ebed9c7..054c6cd 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -89,8 +89,8 @@ "importCancelled": "Операция была отменена", "importSuccess": "Успешно импортировано", "yourID": "Ваш аккаунт в TETR.IO", - "yourIDAlertTitle": "Никнейм или ID вашего аккаунта в TETR.IO", - "yourIDText": "Каждый раз, когда приложение запускается, приложение будет получать статистику этого игрока. Пожалуйста, отдайте предпочтение ID, так как никнейм можно изменить.", + "yourIDAlertTitle": "Ваш ник в TETR.IO", + "yourIDText": "При запуске приложения оно будет получать статистику этого игрока.", "language": "Язык (Language)", "aboutApp": "О приложении", "aboutAppText": "${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy", diff --git a/res/tetrio_badges/twc23_honorary.png b/res/tetrio_badges/twc23_honorary.png new file mode 100644 index 0000000..469ce26 Binary files /dev/null and b/res/tetrio_badges/twc23_honorary.png differ