diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 6f815ff..ad026ce 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'package:vector_math/vector_math.dart'; +import 'dart:developer' as developer; import 'package:http/http.dart' as http; import 'dart:convert'; @@ -70,7 +71,8 @@ class TetrioPlayer { double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1; - TetrioPlayer.fromJson(Map json, DateTime stateTime) { + TetrioPlayer.fromJson(Map json, DateTime stateTime, bool fetchRecords) { + developer.log("TetrioPlayer.fromJson $stateTime: $json", name: "data_objects/tetrio"); userId = json['_id']; username = json['username']; state = stateTime; @@ -95,23 +97,26 @@ class TetrioPlayer { connections = Connections.fromJson(json['connections']); distinguishment = json['distinguishment'] != null ? Distinguishment.fromJson(json['distinguishment']) : null; friendCount = json['friend_count'] ?? 0; - var url = Uri.https('ch.tetr.io', 'api/users/$userId/records'); - Future response = http.get(url); - response.then((value) { - if (value.statusCode == 200) { - Map jsonRecords = jsonDecode(value.body); - sprint = jsonRecords['data']['records']['40l']['record'] != null - ? [RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'])] - : []; - blitz = jsonRecords['data']['records']['blitz']['record'] != null - ? [RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])] - : []; - zen = TetrioZen.fromJson(jsonRecords['data']['zen']); - } else { - throw Exception('Failed to fetch player'); - } - }); badstanding = json['badstanding']; + if (fetchRecords) { + var url = Uri.https('ch.tetr.io', 'api/users/$userId/records'); + Future response = http.get(url); + response.then((value) { + if (value.statusCode == 200) { + Map jsonRecords = jsonDecode(value.body); + sprint = jsonRecords['data']['records']['40l']['record'] != null + ? [RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'])] + : []; + blitz = jsonRecords['data']['records']['blitz']['record'] != null + ? [RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'])] + : []; + zen = TetrioZen.fromJson(jsonRecords['data']['zen']); + } else { + developer.log("TetrioPlayer.fromJson exception", name: "data_objects/tetrio", error: value.statusCode); + throw Exception('Failed to fetch player'); + } + }); + } } Map toJson() { @@ -137,6 +142,7 @@ class TetrioPlayer { data['friend_count'] = friendCount; data['badstanding'] = badstanding; data['bot'] = bot; + developer.log("TetrioPlayer.toJson: $bot", name: "data_objects/tetrio"); return data; } @@ -468,7 +474,6 @@ class EstTr { final double _apm; final double _pps; final double _vs; - final double _rating; final double _rd; final double _app; final double _dss; @@ -478,7 +483,7 @@ class EstTr { late double srarea; late double statrank; - EstTr(this._apm, this._pps, this._vs, this._rating, this._rd, this._app, this._dss, this._dsp, this._gbe) { + EstTr(this._apm, this._pps, this._vs, this._rd, this._app, this._dss, this._dsp, this._gbe) { srarea = (_apm * 0) + (_pps * 135) + (_vs * 0) + (_app * 290) + (_dss * 0) + (_dsp * 700) + (_gbe * 0); statrank = 11.2 * atan((srarea - 93) / 130) + 1; if (statrank <= 0) statrank = 0.001; @@ -493,11 +498,10 @@ class EstTr { class Playstyle { final double _apm; final double _pps; - final double _vs; - final double _rd; + //final double _vs; final double _app; final double _vsapm; - final double _dss; + //final double _dss; final double _dsp; final double _gbe; final double _srarea; @@ -507,12 +511,12 @@ class Playstyle { late double stride; late double infds; - Playstyle(this._apm, this._pps, this._vs, this._rd, this._app, this._vsapm, this._dss, this._dsp, this._gbe, this._srarea, this._statrank) { + Playstyle(this._apm, this._pps, this._app, this._vsapm, this._dsp, this._gbe, this._srarea, this._statrank) { double nmapm = ((_apm / _srarea) / ((0.069 * pow(1.0017, (pow(_statrank, 5) / 4700))) + _statrank / 360)) - 1; double nmpps = ((_pps / _srarea) / (0.0084264 * pow(2.14, (-2 * (_statrank / 2.7 + 1.03))) - _statrank / 5750 + 0.0067)) - 1; - double nmvs = ((_vs / _srarea) / (0.1333 * pow(1.0021, ((pow(_statrank, 7) * (_statrank / 16.5)) / 1400000)) + _statrank / 133)) - 1; + //double nmvs = ((_vs / _srarea) / (0.1333 * pow(1.0021, ((pow(_statrank, 7) * (_statrank / 16.5)) / 1400000)) + _statrank / 133)) - 1; double nmapp = (_app / (0.1368803292 * pow(1.0024, (pow(_statrank, 5) / 2800)) + _statrank / 54)) - 1; - double nmdss = (_dss / (0.01436466667 * pow(4.1, ((_statrank - 9.6) / 2.9)) + _statrank / 140 + 0.01)) - 1; + //double nmdss = (_dss / (0.01436466667 * pow(4.1, ((_statrank - 9.6) / 2.9)) + _statrank / 140 + 0.01)) - 1; double nmdsp = (_dsp / (0.02136327583 * pow(14, ((_statrank - 14.75) / 3.9)) + _statrank / 152 + 0.022)) - 1; double nmgbe = (_gbe / (_statrank / 350 + 0.005948424455 * pow(3.8, ((_statrank - 6.1) / 4)) + 0.006)) - 1; double nmvsapm = (_vsapm / (-pow(((_statrank - 16) / 36), 2) + 2.133)) - 1; @@ -662,12 +666,9 @@ class TetraLeagueAlpha { nextAt = json['next_at']; percentileRank = json['percentile_rank']; nerdStats = (apm != null && pps != null && apm != null) ? NerdStats(apm!, pps!, vs!) : null; - estTr = - (nerdStats != null) ? EstTr(apm!, pps!, vs!, rating, (rd != null) ? rd! : 69, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; - playstyle = (nerdStats != null) - ? Playstyle(apm!, pps!, vs!, (rd != null) ? rd! : 69, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, - estTr!.statrank) - : null; + estTr = (nerdStats != null) ? EstTr(apm!, pps!, vs!, (rd != null) ? rd! : 69, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe) : null; + playstyle = + (nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; } double? get esttracc => (estTr != null) ? estTr!.esttr - rating : null; @@ -701,6 +702,7 @@ class RecordSingle { late String userId; late String replayId; late String ownId; + late String stream; DateTime? timestamp; EndContextSingle? endContext; int? rank; @@ -708,9 +710,11 @@ class RecordSingle { RecordSingle({required this.userId, required this.replayId, required this.ownId, this.timestamp, this.endContext, this.rank}); RecordSingle.fromJson(Map json, int? ran) { + developer.log("RecordSingle.fromJson: $json", name: "data_objects/tetrio"); ownId = json['_id']; endContext = json['endcontext'] != null ? EndContextSingle.fromJson(json['endcontext']) : null; replayId = json['replayid']; + stream = json['stream']; timestamp = DateTime.parse(json['ts']); userId = json['user']['_id']; rank = ran; diff --git a/lib/main.dart b/lib/main.dart index e028348..3a7b3c6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:path/path.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/compare_view.dart'; @@ -10,8 +9,10 @@ void main() { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; runApp(MaterialApp( - home: const MainView(), - routes: {"/settings": (context) => const SettingsView(), "/compare": (context) => const CompareView(), "/states": (context) => const StatesView()}, - theme: ThemeData(fontFamily: 'Eurostile Round', colorScheme: const ColorScheme.dark(), scaffoldBackgroundColor: Colors.black), - )); + home: const MainView(), + routes: {"/settings": (context) => const SettingsView(), "/compare": (context) => const CompareView(), "/states": (context) => const StatesView()}, + theme: ThemeData( + fontFamily: 'Eurostile Round', + colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.purpleAccent), + scaffoldBackgroundColor: Colors.black))); } diff --git a/lib/services/settings_crud.dart b/lib/services/settings_crud.dart deleted file mode 100644 index 2404151..0000000 --- a/lib/services/settings_crud.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:sqflite/sqflite.dart'; -import 'package:path_provider/path_provider.dart' - show MissingPlatformDirectoryException, getApplicationDocumentsDirectory; -import 'package:tetra_stats/services/crud_exceptions.dart'; -import 'package:path/path.dart' show join; - -const String dbName = "TetraStats.db"; -const String tetrioUsersTable = "settings"; -const String userTetrioId = "userTetrioId"; -const String createSettingsTable = ''' - CREATE TABLE IF NOT EXISTS "settings" ( - "userTetrioId" TEXT - )'''; - -class SettingsService {} diff --git a/lib/services/sqlite_db_controller.dart b/lib/services/sqlite_db_controller.dart index 4758ff3..059fb28 100644 --- a/lib/services/sqlite_db_controller.dart +++ b/lib/services/sqlite_db_controller.dart @@ -1,11 +1,8 @@ import 'dart:async'; -import 'dart:convert'; import 'package:sqflite/sqflite.dart'; -import 'package:path_provider/path_provider.dart' - show MissingPlatformDirectoryException, getApplicationDocumentsDirectory; +import 'package:path_provider/path_provider.dart' show MissingPlatformDirectoryException, getApplicationDocumentsDirectory; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; -import 'package:tetra_stats/services/settings_crud.dart'; import 'package:path/path.dart' show join; const String dbName = "TetraStats.db"; @@ -22,7 +19,6 @@ class DB { final db = await openDatabase(dbPath); _db = db; await db.execute(createTetrioUsersTable); - await db.execute(createSettingsTable); } on MissingPlatformDirectoryException { throw UnableToGetDocuments(); } diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 3f602d9..b23b34d 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'package:sqflite/sqflite.dart'; -import 'package:path_provider/path_provider.dart' - show MissingPlatformDirectoryException, getApplicationDocumentsDirectory; -import 'package:path/path.dart' show join; +import 'dart:developer' as developer; +// import 'package:sqflite/sqflite.dart'; +// import 'package:path_provider/path_provider.dart' show MissingPlatformDirectoryException, getApplicationDocumentsDirectory; +// import 'package:path/path.dart' show join; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; @@ -23,19 +23,22 @@ const String createTetrioUsersTable = ''' class TetrioService { Map> _players = {}; - final _tetrioStreamController = - StreamController>>.broadcast(); + final _tetrioStreamController = StreamController>>.broadcast(); + + TetrioService(DB udb) { + _cachePlayers(udb); + } Future _cachePlayers(DB udb) async { final allPlayers = await getAllPlayers(udb: udb); _players = allPlayers.first; _tetrioStreamController.add(_players); + developer.log("_cachePlayers: $_players", name: "services/tetrio_crud"); } Future deletePlayer({required String id, required DB udb}) async { final db = udb.getDatabaseOrThrow(); - final deletedPlayer = await db.delete(tetrioUsersTable, - where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + final deletedPlayer = await db.delete(tetrioUsersTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); if (deletedPlayer != 1) { throw CouldNotDeletePlayer(); } else { @@ -54,25 +57,14 @@ class TetrioService { // } // } - Future createPlayer( - {required TetrioPlayer tetrioPlayer, required DB udb}) async { + Future createPlayer({required TetrioPlayer tetrioPlayer, required DB udb}) async { final db = udb.getDatabaseOrThrow(); - 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) { throw TetrioPlayerAlreadyExist(); } - final Map statesJson = { - tetrioPlayer.state.millisecondsSinceEpoch.toString(): - tetrioPlayer.toJson() - }; - db.insert(tetrioUsersTable, { - idCol: tetrioPlayer.userId, - nickCol: tetrioPlayer.username, - statesCol: jsonEncode(statesJson) - }); + final Map statesJson = {tetrioPlayer.state.millisecondsSinceEpoch.toString(): tetrioPlayer.toJson()}; + db.insert(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)}); _players.addEntries({ tetrioPlayer.userId: [tetrioPlayer] }.entries); @@ -81,40 +73,28 @@ class TetrioService { Future storeState(TetrioPlayer tetrioPlayer, DB udb) async { final db = udb.getDatabaseOrThrow(); - List states = - await getPlayer(id: tetrioPlayer.userId, udb: udb); + List states = await getPlayer(id: tetrioPlayer.userId, udb: udb); states.add(tetrioPlayer); final Map statesJson = {}; for (var e in states) { - statesJson.addEntries( - {e.state.millisecondsSinceEpoch.toString(): e.toJson()}.entries); + statesJson.addEntries({e.state.millisecondsSinceEpoch.toString(): e.toJson()}.entries); } - db.update( - tetrioUsersTable, - { - idCol: tetrioPlayer.userId, - nickCol: tetrioPlayer.username, - statesCol: jsonEncode(statesJson) - }, - where: '$idCol = ?', - whereArgs: [tetrioPlayer.userId]); + 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); } - Future> getPlayer( - {required String id, required DB udb}) async { + Future> getPlayer({required String id, required DB udb}) async { final db = udb.getDatabaseOrThrow(); List 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) { throw TetrioPlayerNotExist(); } else { dynamic rawStates = results.first['jsonStates'] as String; rawStates = json.decode(rawStates); - rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson( - v, DateTime.fromMillisecondsSinceEpoch(int.parse(k))))); + rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k)), false))); _players.removeWhere((key, value) => key == id); _players.addEntries({states.last.userId: states}.entries); _tetrioStreamController.add(_players); @@ -122,17 +102,24 @@ class TetrioService { } } - Future>>> getAllPlayers( - {required DB udb}) async { - //await _ensureDbIsOpen(); + Future _ensureDbIsOpen(DB udb) async { + try { + await udb.open(); + } on DatabaseAlreadyOpen { + // empty + } + } + + Future>>> getAllPlayers({required DB udb}) async { + await _ensureDbIsOpen(udb); final db = udb.getDatabaseOrThrow(); final players = await db.query(tetrioUsersTable); Map> data = {}; + //developer.log("getAllPlayers: $players", name: "services/tetrio_crud"); return players.map((row) { var test = json.decode(row['jsonStates'] as String); List states = []; - test.forEach( - (k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.now()))); + test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k)), false))); data.addEntries({states.last.userId: states}.entries); return data; }); diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 4517856..3dfad8f 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -12,12 +12,12 @@ class CompareState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("you vs someone"), + title: const Text("you vs someone"), ), backgroundColor: Colors.black, body: SafeArea( child: ListView( - children: [ + children: const [ ListTile( title: Center(child: Text("So thats gonna be the main purpose of the app")), subtitle: Center(child: Text("We gonna look who is the best")), diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 20d6ad4..83e5e11 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:path/path.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:developer' as developer; import 'dart:convert'; +import 'package:flutter/services.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; @@ -16,7 +18,8 @@ extension StringExtension on String { String _searchFor = "dan63047"; Future? me; DB db = DB(); -TetrioService teto = TetrioService(); +late TetrioService teto; +late SharedPreferences prefs; const allowedHeightForPlayerIdInPixels = 40.0; const allowedHeightForPlayerBioInPixels = 30.0; const givenTextHeightByScreenPercentage = 0.3; @@ -30,6 +33,10 @@ class MainView extends StatefulWidget { State createState() => _MainState(); } +Future copyToClipboard(String text) async { + await Clipboard.setData(ClipboardData(text: text)); +} + Future fetchTetrioPlayer(String user) async { var url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}'); final response = await http.get(url); @@ -37,11 +44,13 @@ Future fetchTetrioPlayer(String user) async { if (response.statusCode == 200) { if (jsonDecode(response.body)['success']) { return TetrioPlayer.fromJson( - jsonDecode(response.body)['data']['user'], DateTime.fromMillisecondsSinceEpoch(jsonDecode(response.body)['cache']['cached_at'], isUtc: true)); + jsonDecode(response.body)['data']['user'], DateTime.fromMillisecondsSinceEpoch(jsonDecode(response.body)['cache']['cached_at'], isUtc: true), true); } else { + developer.log("fetchTetrioPlayer User dosen't exist", name: "main_view", error: response.body); throw Exception("User doesn't exist"); } } else { + developer.log("fetchTetrioPlayer Failed to fetch player", name: "main_view", error: response.statusCode); throw Exception('Failed to fetch player'); } } @@ -62,7 +71,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { Widget _searchTextField() { return TextField( maxLength: 25, - decoration: InputDecoration(counter: Offstage()), + decoration: const InputDecoration(counter: Offstage()), style: const TextStyle( shadows: [ Shadow( @@ -78,7 +87,6 @@ class _MainState extends State with SingleTickerProviderStateMixin { ], ), onSubmitted: (String value) { - _tabController.animateTo(0, duration: Duration(milliseconds: 300)); changePlayer(value); }, ); @@ -86,10 +94,13 @@ class _MainState extends State with SingleTickerProviderStateMixin { @override void initState() { + db.open(); + teto = TetrioService(db); _scrollController = ScrollController(); _tabController = TabController(length: 4, vsync: this); - changePlayer("dan63047"); + _getPreferences().then((value) => changePlayer(prefs.getString("player") ?? "dan63047")); super.initState(); + developer.log("Main view initialized", name: "main_view"); } @override @@ -97,28 +108,34 @@ class _MainState extends State with SingleTickerProviderStateMixin { _tabController.dispose(); _scrollController.dispose(); super.dispose(); + developer.log("Main view disposed", name: "main_view"); + } + + Future _getPreferences() async { + prefs = await SharedPreferences.getInstance(); } void changePlayer(String player) { setState(() { + _tabController.animateTo(0, duration: const Duration(milliseconds: 300)); _searchFor = player; me = fetchTetrioPlayer(player); }); } - _scrollListener() { - if (fixedScroll) { - _scrollController.jumpTo(0); - } - } + // _scrollListener() { + // if (fixedScroll) { + // _scrollController.jumpTo(0); + // } + // } - _smoothScrollToTop() { - _scrollController.animateTo( - 0, - duration: const Duration(microseconds: 300), - curve: Curves.ease, - ); - } + // _smoothScrollToTop() { + // _scrollController.animateTo( + // 0, + // duration: const Duration(microseconds: 300), + // curve: Curves.ease, + // ); + // } @override Widget build(BuildContext context) { @@ -150,7 +167,6 @@ class _MainState extends State with SingleTickerProviderStateMixin { ? IconButton( onPressed: () { setState(() { - //add _searchBoolean = true; }); }, @@ -160,7 +176,6 @@ class _MainState extends State with SingleTickerProviderStateMixin { : IconButton( onPressed: () { setState(() { - //add _searchBoolean = false; }); }, @@ -192,6 +207,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { child: FutureBuilder( future: me, builder: (context, snapshot) { + developer.log("builder ($context): $snapshot", name: "main_view"); if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator( @@ -210,8 +226,10 @@ class _MainState extends State with SingleTickerProviderStateMixin { controller: _tabController, isScrollable: true, tabs: myTabs, - onTap: (int sus) { - setState(() {}); + onTap: (int tabId) { + setState(() { + developer.log("Tab changed to $tabId", name: "main_view"); + }); }, ), ), @@ -438,9 +456,9 @@ class _MainState extends State with SingleTickerProviderStateMixin { radarShape: RadarShape.polygon, tickCount: 4, ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), - radarBorderData: BorderSide(color: Colors.transparent, width: 1), - gridBorderData: BorderSide(color: Colors.white24, width: 1), - tickBorderData: BorderSide(color: Colors.transparent, width: 1), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), getTitle: (index, angle) { switch (index) { case 0: @@ -492,16 +510,16 @@ class _MainState extends State with SingleTickerProviderStateMixin { fillColor: Colors.transparent, borderColor: Colors.transparent, dataEntries: [ - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), ], ) ], @@ -521,9 +539,9 @@ class _MainState extends State with SingleTickerProviderStateMixin { radarShape: RadarShape.polygon, tickCount: 4, ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), - radarBorderData: BorderSide(color: Colors.transparent, width: 1), - gridBorderData: BorderSide(color: Colors.white24, width: 1), - tickBorderData: BorderSide(color: Colors.transparent, width: 1), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), getTitle: (index, angle) { switch (index) { case 0: @@ -557,20 +575,20 @@ class _MainState extends State with SingleTickerProviderStateMixin { fillColor: Colors.transparent, borderColor: Colors.transparent, dataEntries: [ - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), - RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), ], ), RadarDataSet( fillColor: Colors.transparent, borderColor: Colors.transparent, dataEntries: [ - RadarEntry(value: 1), - RadarEntry(value: 1), - RadarEntry(value: 1), - RadarEntry(value: 1), + const RadarEntry(value: 1), + const RadarEntry(value: 1), + const RadarEntry(value: 1), + const RadarEntry(value: 1), ], ) ], @@ -1087,7 +1105,8 @@ class _MainState extends State with SingleTickerProviderStateMixin { ), ); } else if (snapshot.hasError) { - return Center(child: Text('${snapshot.error}', style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))); + return Center( + child: Text('${snapshot.error}', style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center)); } return const Center( child: CircularProgressIndicator( @@ -1101,8 +1120,8 @@ class _MainState extends State with SingleTickerProviderStateMixin { } class NavDrawer extends StatelessWidget { - Function changePlayer; - NavDrawer(this.changePlayer, {super.key}); + final Function changePlayer; + const NavDrawer(this.changePlayer, {super.key}); @override Widget build(BuildContext context) { @@ -1116,10 +1135,11 @@ class NavDrawer extends StatelessWidget { style: TextStyle(color: Colors.white, fontSize: 25), )), ListTile( - leading: const Icon(Icons.verified_user), - title: const Text('dan63047'), + leading: const Icon(Icons.home), + title: Text(prefs.getString("player") ?? "dan63047"), onTap: () { - changePlayer('dan63047'); + developer.log("Navigator changed player", name: "main_view"); + changePlayer(prefs.getString("player") ?? "dan63047"); Navigator.of(context).pop(); }, ), @@ -1178,7 +1198,7 @@ class _StatCellNum extends StatelessWidget { class _UserThingy extends StatelessWidget { final TetrioPlayer player; - _UserThingy({Key? key, required this.player}) : super(key: key); + const _UserThingy({Key? key, required this.player}) : super(key: key); @override Widget build(BuildContext context) { @@ -1201,6 +1221,12 @@ class _UserThingy extends StatelessWidget { "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", fit: BoxFit.cover, height: bannerHeight, + errorBuilder: (context, error, stackTrace) { + developer.log("Error with building banner image", name: "main_view", error: error, stackTrace: stackTrace); + return const Placeholder( + color: Colors.black, + ); + }, ), Container( padding: EdgeInsets.fromLTRB(0, player.bannerRevision != null ? bannerHeight / 1.4 : pfpHeight, 0, 0), @@ -1212,16 +1238,21 @@ class _UserThingy extends StatelessWidget { fit: BoxFit.fitHeight, height: 128, ) - : Image.network( - "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", - fit: BoxFit.fitHeight, - height: 128, - errorBuilder: (context, error, stackTrace) => Image.asset( - "res/avatars/tetrio_anon.png", - fit: BoxFit.fitHeight, - height: 128, - ), - ), + : player.avatarRevision != null + ? Image.network("https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { + developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace); + return Image.asset( + "res/avatars/tetrio_anon.png", + fit: BoxFit.fitHeight, + height: 128, + ); + }) + : Image.asset( + "res/avatars/tetrio_anon.png", + fit: BoxFit.fitHeight, + height: 128, + ), ), ), ], @@ -1230,10 +1261,12 @@ class _UserThingy extends StatelessWidget { child: Column( children: [ Text(player.username, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Text( - player.userId, - style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14), - ), + TextButton( + child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)), + onPressed: () { + copyToClipboard(player.userId); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Copied to clipboard!"))); + }), ], ), ), @@ -1311,7 +1344,6 @@ class _UserThingy extends StatelessWidget { IconButton( onPressed: () => showDialog( context: context, - barrierDismissible: false, // user must tap button! builder: (BuildContext context) { return AlertDialog( title: Text( @@ -1350,6 +1382,10 @@ class _UserThingy extends StatelessWidget { "res/tetrio_badges/${badge.badgeId}.png", height: 64, width: 64, + errorBuilder: (context, error, stackTrace) { + developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace); + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, )) ], ), diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index a082f7a..e49845f 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class SettingsView extends StatefulWidget { const SettingsView({Key? key}) : super(key: key); @@ -10,11 +11,14 @@ class SettingsView extends StatefulWidget { class SettingsState extends State { PackageInfo _packageInfo = PackageInfo(appName: "TetraStats", packageName: "idk man", version: "some numbers", buildNumber: "anotherNumber"); + late SharedPreferences prefs; + final TextEditingController _playertext = TextEditingController(); @override void initState() { super.initState(); _initPackageInfo(); + _getPreferences(); } Future _initPackageInfo() async { @@ -24,6 +28,14 @@ class SettingsState extends State { }); } + Future _getPreferences() async { + prefs = await SharedPreferences.getInstance(); + } + + Future _setPlayer(String player) async { + await prefs.setString('player', player); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -37,22 +49,23 @@ class SettingsState extends State { ListTile( title: const Text("So there you gonna be able to change some settings"), subtitle: const Text( - "They are not implemented yet. But its gonna be possible to change player for main view init, save logs, as well as import and export app sqlite database."), + "Only \"Your TETR.IO account nickname or ID\" implemented yet. But its gonna be possible to change player for main view init, save logs, as well as import and export app sqlite database."), trailing: Switch( value: true, onChanged: (bool value) {}, ), ), ListTile( - title: const Text("Very egg"), - subtitle: const Text("very ass"), - trailing: const Text("dan63047"), + title: const Text("Your TETR.IO account nickname or ID"), + subtitle: + const Text("Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed."), + trailing: Text(prefs.getString("player") ?? "dan63047"), onTap: () => showDialog( context: context, builder: (BuildContext context) => AlertDialog( - title: const Text("Your username in TETR.IO", style: TextStyle(fontFamily: "Eurostile Round Extended")), + title: const Text("Your TETR.IO account nickname or ID", style: TextStyle(fontFamily: "Eurostile Round Extended")), content: SingleChildScrollView( - child: ListBody(children: [const TextField()]), + child: ListBody(children: [TextField(controller: _playertext)]), ), actions: [ TextButton( @@ -64,13 +77,15 @@ class SettingsState extends State { TextButton( child: const Text('Submit'), onPressed: () { + _setPlayer(_playertext.text.toLowerCase().trim()); Navigator.of(context).pop(); + setState(() {}); }, ) ], )), ), - Divider(), + const Divider(), ListTile( title: const Text("About app"), subtitle: Text("${_packageInfo.appName} (${_packageInfo.packageName}) Version ${_packageInfo.version} Build ${_packageInfo.buildNumber}"), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4d98e47..63c4df1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,12 @@ import Foundation import package_info_plus import path_provider_foundation +import shared_preferences_foundation import sqflite func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 85167f9..8192e10 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -264,6 +264,62 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + url: "https://pub.dev" + source: hosted + version: "2.2.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 357f6c3..11beba8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ publish_to: 'none' # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.0.1+1 +version: 0.0.2+2 environment: sdk: '>=2.19.6 <3.0.0' @@ -37,6 +37,7 @@ dependencies: path: ^1.8.2 fl_chart: ^0.62.0 package_info_plus: ^4.0.2 + shared_preferences: ^2.1.1 dev_dependencies: flutter_test: