diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index b3c9c3f..47ef9b6 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -1,4 +1,5 @@ import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math.dart'; import 'dart:developer' as developer; import 'package:http/http.dart' as http; @@ -151,8 +152,8 @@ class TetrioPlayer { if (userId != other.userId) return false; if (username != other.username) return false; if (role != other.role) return false; - if (badges != other.badges) return false; - if (bio != other.bio) return false; + if (listEquals(badges, other.badges) == false) return false; + //if (bio != other.bio) return false; if (country != other.country) return false; if (friendCount != other.friendCount) return false; if (gamesPlayed != other.gamesPlayed) return false; @@ -222,6 +223,8 @@ class Connections { Connections.fromJson(Map json) { discord = json['discord'] != null ? Discord.fromJson(json['discord']) : null; } + @override + bool operator ==(covariant Connections other) => discord == other.discord; Map toJson() { final Map data = {}; @@ -308,6 +311,9 @@ class Discord { username = json['username']; } + @override + bool operator ==(covariant Discord other) => id == other.id; + Map toJson() { final Map data = {}; data['id'] = id; @@ -672,6 +678,9 @@ class TetraLeagueAlpha { (nerdStats != null) ? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank) : null; } + @override + bool operator ==(covariant TetraLeagueAlpha other) => gamesPlayed == other.gamesPlayed && rd == other.rd; + double? get esttracc => (estTr != null) ? estTr!.esttr - rating : null; Map toJson() { @@ -769,6 +778,9 @@ class Distinguishment { footer = json['footer']; } + @override + bool operator ==(covariant Distinguishment other) => type == other.type && detail == other.detail && header == other.header && footer == other.footer; + Map toJson() { final Map data = {}; data['type'] = type; diff --git a/lib/main.dart b/lib/main.dart index ffcb2fa..f3039ab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,7 +14,7 @@ void main() { routes: { "/settings": (context) => const SettingsView(), "/compare": (context) => const CompareView(), - "/states": (context) => const StatesView(), + "/states": (context) => const TrackedPlayersView(), "/calc": (context) => const CalcView() }, theme: ThemeData( diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 600ae52..d894046 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -64,6 +64,12 @@ class TetrioService extends DB { } } + Future getNicknameByID(String id) async { + if (id.length <= 16) return id; + TetrioPlayer player = await getPlayer(id).then((value) => value.last); + return player.username; + } + Future createPlayer(TetrioPlayer tetrioPlayer) async { ensureDbIsOpen(); final db = getDatabaseOrThrow(); @@ -89,6 +95,17 @@ class TetrioService extends DB { db.insert(tetrioUsersToTrackTable, {idCol: tetrioPlayer.userId}); } + Future isPlayerTracking(String id) async { + ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + final results = await db.query(tetrioUsersToTrackTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + if (results.isEmpty) { + return false; + } else { + return true; + } + } + Future> getAllPlayerToTrack() async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); @@ -119,7 +136,8 @@ class TetrioService extends DB { await createPlayer(tetrioPlayer); states = await getPlayer(tetrioPlayer.userId); } - if (!_players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer)) states.add(tetrioPlayer); + bool test = _players[tetrioPlayer.userId]!.last.isSameState(tetrioPlayer); + if (test == false) states.add(tetrioPlayer); final Map statesJson = {}; for (var e in states) { statesJson.addEntries({e.state.millisecondsSinceEpoch.toString(): e.toJson()}.entries); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index a797b95..7864864 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,19 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; 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'; -import 'package:fl_chart/fl_chart.dart'; - -extension StringExtension on String { - String capitalize() { - return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; - } -} +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/widgets/stat_sell_num.dart'; +import 'package:tetra_stats/widgets/tl_thingy.dart'; +import 'package:tetra_stats/widgets/user_thingy.dart'; String _searchFor = "dan63047"; Future? me; @@ -103,6 +97,10 @@ class _MainState extends State with SingleTickerProviderStateMixin { }); } + void _justUpdate() { + setState(() {}); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -150,13 +148,13 @@ class _MainState extends State with SingleTickerProviderStateMixin { ), PopupMenuButton( itemBuilder: (BuildContext context) => [ - const PopupMenuItem( - value: "/compare", - child: Text('Compare'), - ), + // const PopupMenuItem( + // value: "/compare", + // child: Text('Compare'), + // ), const PopupMenuItem( value: "/states", - child: Text('Players you track'), + child: Text('Show stored data'), ), const PopupMenuItem( value: "/calc", @@ -192,11 +190,20 @@ class _MainState extends State with SingleTickerProviderStateMixin { case ConnectionState.done: bool bigScreen = MediaQuery.of(context).size.width > 1024; if (snapshot.hasData) { + if (_searchFor.length > 16) _searchFor = snapshot.data!.username; + teto.isPlayerTracking(snapshot.data!.userId).then((value) { + if (value) teto.storeState(snapshot.data!); + }); return NestedScrollView( controller: _scrollController, headerSliverBuilder: (context, value) { return [ - SliverToBoxAdapter(child: _UserThingy(player: snapshot.data!)), + SliverToBoxAdapter( + child: UserThingy( + player: snapshot.data!, + showStateTimestamp: false, + setState: _justUpdate, + )), SliverToBoxAdapter( child: TabBar( controller: _tabController, @@ -214,7 +221,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { body: TabBarView( controller: _tabController, children: [ - _TLThingy(tl: snapshot.data!.tlSeason1, userID: snapshot.data!.userId), + TLThingy(tl: snapshot.data!.tlSeason1, userID: snapshot.data!.userId), _RecordThingy(record: (snapshot.data!.sprint.isNotEmpty) ? snapshot.data!.sprint[0] : null), _RecordThingy(record: (snapshot.data!.blitz.isNotEmpty) ? snapshot.data!.blitz[0] : null), _OtherThingy(zen: snapshot.data!.zen, bio: snapshot.data!.bio) @@ -252,9 +259,11 @@ class NavDrawer extends StatefulWidget { class _NavDrawerState extends State { late ScrollController _scrollController; + String homePlayerNickname = "Checking..."; @override void initState() { super.initState(); + _setHomePlayerNickname(prefs.getString("player")); _scrollController = ScrollController(); } @@ -264,6 +273,19 @@ class _NavDrawerState extends State { super.dispose(); } + Future _setHomePlayerNickname(String? n) async { + if (n != null) { + try { + homePlayerNickname = await teto.getNicknameByID(n); + } on TetrioPlayerNotExist { + homePlayerNickname = n; + } + } else { + homePlayerNickname = "dan63047"; + } + setState(() {}); + } + @override Widget build(BuildContext context) { return Drawer( @@ -289,7 +311,7 @@ class _NavDrawerState extends State { SliverToBoxAdapter( child: ListTile( leading: const Icon(Icons.home), - title: Text(prefs.getString("player") ?? "dan63047"), + title: Text(homePlayerNickname), onTap: () { developer.log("Navigator changed player", name: "main_view"); widget.changePlayer(prefs.getString("player") ?? "dan63047"); @@ -320,575 +342,6 @@ class _NavDrawerState extends State { } } -class _StatCellNum extends StatelessWidget { - const _StatCellNum({required this.playerStat, required this.playerStatLabel, required this.isScreenBig, this.snackBar, this.fractionDigits}); - - final num playerStat; - final String playerStatLabel; - final bool isScreenBig; - final String? snackBar; - final int? fractionDigits; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text( - fractionDigits != null ? playerStat.toStringAsFixed(fractionDigits!) : playerStat.floor().toString(), - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: isScreenBig ? 32 : 24, - ), - ), - snackBar == null - ? Text( - playerStatLabel, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - ), - ) - : TextButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(snackBar!))); - }, - style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), - child: Text( - playerStatLabel, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - ), - )), - ], - ); - } -} - -class _UserThingy extends StatelessWidget { - final TetrioPlayer player; - const _UserThingy({Key? key, required this.player}) : super(key: key); - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - double bannerHeight = bigScreen ? 240 : 120; - double pfpHeight = 128; - return Column( - children: [ - Flex( - direction: Axis.vertical, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - alignment: Alignment.topCenter, - children: [ - if (player.bannerRevision != null) - Image.network( - "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), - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: player.role == "banned" - ? Image.asset( - "res/avatars/tetrio_banned.png", - fit: BoxFit.fitHeight, - height: pfpHeight, - ) - : 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: pfpHeight, - ); - }) - : Image.asset( - "res/avatars/tetrio_anon.png", - fit: BoxFit.fitHeight, - height: pfpHeight, - ), - ), - ), - if (player.verified) - Padding( - padding: EdgeInsets.fromLTRB( - pfpHeight - 22, - bigScreen // verified icon top padding: - ? (player.bannerRevision != null ? bannerHeight + pfpHeight - 96 : pfpHeight + pfpHeight - 32) // for big screen - : (player.bannerRevision != null ? bannerHeight + pfpHeight - 58 : pfpHeight + pfpHeight - 32), // for small screen - 0, - 0), - child: const Icon(Icons.verified), - ) - ], - ), - Flexible( - child: Column( - children: [ - Text(player.username, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - 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!"))); - }), - IconButton( - icon: Icon(Icons.addchart), - style: ButtonStyle(), - onPressed: () {}, - ), - Text("Track") - ], - )), - ], - ), - (player.role != "banned") - ? Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, // hard WHAT??? - children: [ - _StatCellNum( - playerStat: player.level, - playerStatLabel: "XP Level", - isScreenBig: bigScreen, - snackBar: "${player.xp.floor().toString()} XP, ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} % until next level", - ), - if (player.gameTime >= Duration.zero) - _StatCellNum( - playerStat: player.gameTime.inHours, playerStatLabel: "Hours\nPlayed", isScreenBig: bigScreen, snackBar: player.gameTime.toString()), - if (player.gamesPlayed >= 0) _StatCellNum(playerStat: player.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: "Online\nGames"), - if (player.gamesWon >= 0) _StatCellNum(playerStat: player.gamesWon, isScreenBig: bigScreen, playerStatLabel: "Games\nWon"), - if (player.friendCount > 0) _StatCellNum(playerStat: player.friendCount, isScreenBig: bigScreen, playerStatLabel: "Friends"), - ], - ) - : Text( - "BANNED", - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontWeight: FontWeight.w900, - color: Colors.red, - fontSize: bigScreen ? 60 : 45, - ), - ), - if (player.badstanding != null && player.badstanding!) - Text( - "BAD STANDING", - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontWeight: FontWeight.w900, - color: Colors.red, - fontSize: bigScreen ? 60 : 45, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Text( - "${player.country != null ? "${player.country?.toUpperCase()} • " : ""}${player.role.capitalize()} account ${player.registrationTime == null ? "that was from very beginning" : 'created ${player.registrationTime}'}${player.botmaster != null ? " by ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? "Not a supporter" : "Supporter tier ${player.supporterTier}"}", - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - )), - ) - ], - ), - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - for (var badge in player.badges) - IconButton( - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text( - badge.label, - style: const TextStyle(fontFamily: "Eurostile Round Extended"), - ), - content: SingleChildScrollView( - child: ListBody( - children: [ - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 25, - children: [ - Image.asset("res/tetrio_badges/${badge.badgeId}.png"), - Text(badge.ts != null ? "Obtained ${badge.ts}" : "That badge was assigned manualy by TETR.IO admins"), - ], - ) - ], - ), - ), - actions: [ - TextButton( - child: const Text('OK'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ), - tooltip: badge.label, - icon: Image.asset( - "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); - }, - )) - ], - ), - ], - ); - }); - } -} - -class _TLThingy extends StatelessWidget { - final TetraLeagueAlpha tl; - final String userID; - const _TLThingy({Key? key, required this.tl, required this.userID}) : super(key: key); - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - return ListView.builder( - physics: const ClampingScrollPhysics(), - itemCount: 1, - itemBuilder: (BuildContext context, int index) { - return Column( - children: (tl.gamesPlayed > 0) - ? [ - Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (tl.gamesPlayed >= 10) - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceAround, - crossAxisAlignment: WrapCrossAlignment.center, - clipBehavior: Clip.hardEdge, - children: [ - userID == "5e32fc85ab319c2ab1beb07c" // he love her so much, you can't even imagine - ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? - : Image.asset("res/tetrio_tl_alpha_ranks/${tl.rank}.png", height: 128), - Column( - children: [ - Text("${tl.rating.toStringAsFixed(2)} TR", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Text( - "Top ${(tl.percentile * 100).toStringAsFixed(2)}% (${tl.percentileRank.toUpperCase()}) • Top Rank: ${tl.bestRank.toUpperCase()} • Glicko: ${tl.glicko?.toStringAsFixed(2)}±${tl.rd?.toStringAsFixed(2)}${tl.decaying ? ' • Decaying' : ''}", - textAlign: TextAlign.center, - ), - ], - ), - ], - ) - else - Text("${10 - tl.gamesPlayed} games until being ranked", - softWrap: true, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28, - overflow: TextOverflow.visible, - )), - Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - if (tl.apm != null) - _StatCellNum(playerStat: tl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Attack\nPer Minute"), - if (tl.pps != null) - _StatCellNum(playerStat: tl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Pieces\nPer Second"), - if (tl.apm != null) _StatCellNum(playerStat: tl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Versus\nScore"), - if (tl.standing > 0) _StatCellNum(playerStat: tl.standing, isScreenBig: bigScreen, playerStatLabel: "Leaderboard\nplacement"), - if (tl.standingLocal > 0) - _StatCellNum(playerStat: tl.standingLocal, isScreenBig: bigScreen, playerStatLabel: "Country LB\nplacement"), - _StatCellNum(playerStat: tl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: "Games\nplayed"), - _StatCellNum(playerStat: tl.gamesWon, isScreenBig: bigScreen, playerStatLabel: "Games\nwon"), - _StatCellNum(playerStat: tl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Winrate\nprecentage"), - ], - ), - ), - if (tl.nerdStats != null) - Column( - children: [ - Text("Nerd Stats", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - _StatCellNum(playerStat: tl.nerdStats!.app, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Attack\nPer Piece"), - _StatCellNum(playerStat: tl.nerdStats!.vsapm, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "VS/APM"), - _StatCellNum( - playerStat: tl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Downstack\nPer Second"), - _StatCellNum( - playerStat: tl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Downstack\nPer Piece"), - _StatCellNum(playerStat: tl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "APP + DS/P"), - _StatCellNum(playerStat: tl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Cheese\nIndex"), - _StatCellNum( - playerStat: tl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Garbage\nEfficiency"), - _StatCellNum(playerStat: tl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Weighted\nAPP"), - _StatCellNum(playerStat: tl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: "Area") - ]), - ) - ], - ), - if (tl.estTr != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), - child: SizedBox( - width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Est. of TR:", - style: TextStyle(fontSize: 24), - ), - Text( - tl.estTr!.esttr.toStringAsFixed(2), - style: const TextStyle(fontSize: 24), - ), - ], - ), - if (tl.rating >= 0) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Accuracy:", - style: TextStyle(fontSize: 24), - ), - Text( - tl.esttracc!.toStringAsFixed(2), - style: const TextStyle(fontSize: 24), - ), - ], - ), - ], - ), - ), - ), - if (tl.nerdStats != null) - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceAround, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 48), - child: SizedBox( - height: 300, - width: 300, - child: RadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), - 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: - return RadarChartTitle( - text: 'APM', - angle: angle, - ); - case 1: - return RadarChartTitle( - text: 'PPS', - angle: angle, - ); - case 2: - return RadarChartTitle(text: 'VS', angle: angle); - case 3: - return RadarChartTitle(text: 'APP', angle: angle + 180); - case 4: - return RadarChartTitle(text: 'DS/S', angle: angle + 180); - case 5: - return RadarChartTitle(text: 'DS/P', angle: angle + 180); - case 6: - return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180); - case 7: - return RadarChartTitle(text: 'VS/APM', angle: angle + 180); - case 8: - return RadarChartTitle(text: 'Cheese', angle: angle); - case 9: - return RadarChartTitle(text: 'Gb Eff.', angle: angle); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - dataEntries: [ - RadarEntry(value: tl.apm! * 1), - RadarEntry(value: tl.pps! * 45), - RadarEntry(value: tl.vs! * 0.444), - RadarEntry(value: tl.nerdStats!.app * 185), - RadarEntry(value: tl.nerdStats!.dss * 175), - RadarEntry(value: tl.nerdStats!.dsp * 450), - RadarEntry(value: tl.nerdStats!.appdsp * 140), - RadarEntry(value: tl.nerdStats!.vsapm * 60), - RadarEntry(value: tl.nerdStats!.cheese * 1.25), - RadarEntry(value: tl.nerdStats!.gbe * 315), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - 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), - ], - ) - ], - ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional - ), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 48), - child: SizedBox( - height: 300, - width: 300, - child: RadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), - 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: - return RadarChartTitle( - text: 'Opener', - angle: angle, - ); - case 1: - return RadarChartTitle( - text: 'Stride', - angle: angle, - ); - case 2: - return RadarChartTitle(text: 'Inf Ds', angle: angle + 180); - case 3: - return RadarChartTitle(text: 'Plonk', angle: angle); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - dataEntries: [ - RadarEntry(value: tl.playstyle!.opener), - RadarEntry(value: tl.playstyle!.stride), - RadarEntry(value: tl.playstyle!.infds), - RadarEntry(value: tl.playstyle!.plonk), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 1), - const RadarEntry(value: 1), - const RadarEntry(value: 1), - const RadarEntry(value: 1), - ], - ) - ], - ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional - ), - ), - ), - ], - ) - ] - : [ - Text("That user never played Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ], - ); - }, - ); - }); - } -} - class _RecordThingy extends StatelessWidget { final RecordSingle? record; const _RecordThingy({Key? key, required this.record}) : super(key: key); @@ -912,7 +365,7 @@ class _RecordThingy extends StatelessWidget { Text(record!.endContext!.finalTime.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) else if (record!.stream.contains("blitz")) Text(record!.endContext!.score.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (record!.rank != null) _StatCellNum(playerStat: record!.rank!, playerStatLabel: "Leaderboard Placement", isScreenBig: bigScreen), + if (record!.rank != null) StatCellNum(playerStat: record!.rank!, playerStatLabel: "Leaderboard Placement", isScreenBig: bigScreen), Padding( padding: const EdgeInsets.fromLTRB(0, 48, 0, 48), child: Wrap( @@ -923,20 +376,20 @@ class _RecordThingy extends StatelessWidget { spacing: 25, children: [ if (record!.stream.contains("blitz")) - _StatCellNum(playerStat: record!.endContext!.level, playerStatLabel: "Level", isScreenBig: bigScreen), + StatCellNum(playerStat: record!.endContext!.level, playerStatLabel: "Level", isScreenBig: bigScreen), if (record!.stream.contains("blitz")) - _StatCellNum(playerStat: record!.endContext!.spp, playerStatLabel: "Score\nPer Piece", fractionDigits: 2, isScreenBig: bigScreen), - _StatCellNum(playerStat: record!.endContext!.piecesPlaced, playerStatLabel: "Pieces\nPlaced", isScreenBig: bigScreen), - _StatCellNum(playerStat: record!.endContext!.pps, playerStatLabel: "Pieces\nPer Second", fractionDigits: 2, isScreenBig: bigScreen), - _StatCellNum(playerStat: record!.endContext!.finesse.faults, playerStatLabel: "Finesse\nFaults", isScreenBig: bigScreen), - _StatCellNum( + StatCellNum(playerStat: record!.endContext!.spp, playerStatLabel: "Score\nPer Piece", fractionDigits: 2, isScreenBig: bigScreen), + StatCellNum(playerStat: record!.endContext!.piecesPlaced, playerStatLabel: "Pieces\nPlaced", isScreenBig: bigScreen), + StatCellNum(playerStat: record!.endContext!.pps, playerStatLabel: "Pieces\nPer Second", fractionDigits: 2, isScreenBig: bigScreen), + StatCellNum(playerStat: record!.endContext!.finesse.faults, playerStatLabel: "Finesse\nFaults", isScreenBig: bigScreen), + StatCellNum( playerStat: record!.endContext!.finessePercentage * 100, playerStatLabel: "Finesse\nPercentage", fractionDigits: 2, isScreenBig: bigScreen), - _StatCellNum(playerStat: record!.endContext!.inputs, playerStatLabel: "Key\nPresses", isScreenBig: bigScreen), - _StatCellNum(playerStat: record!.endContext!.kpp, playerStatLabel: "KP Per\nPiece", fractionDigits: 2, isScreenBig: bigScreen), - _StatCellNum(playerStat: record!.endContext!.kps, playerStatLabel: "KP Per\nSecond", fractionDigits: 2, isScreenBig: bigScreen), + StatCellNum(playerStat: record!.endContext!.inputs, playerStatLabel: "Key\nPresses", isScreenBig: bigScreen), + StatCellNum(playerStat: record!.endContext!.kpp, playerStatLabel: "KP Per\nPiece", fractionDigits: 2, isScreenBig: bigScreen), + StatCellNum(playerStat: record!.endContext!.kps, playerStatLabel: "KP Per\nSecond", fractionDigits: 2, isScreenBig: bigScreen), ], ), ), diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 9d953ac..4b52a21 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/services/tetrio_crud.dart'; class SettingsView extends StatefulWidget { const SettingsView({Key? key}) : super(key: key); @@ -12,13 +14,15 @@ 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 TetrioService teto = TetrioService(); + String defaultNickname = "Checking..."; final TextEditingController _playertext = TextEditingController(); @override void initState() { - super.initState(); _initPackageInfo(); _getPreferences(); + super.initState(); } Future _initPackageInfo() async { @@ -30,10 +34,25 @@ class SettingsState extends State { Future _getPreferences() async { prefs = await SharedPreferences.getInstance(); + _setDefaultNickname(prefs.getString("player")); + } + + Future _setDefaultNickname(String? n) async { + if (n != null) { + try { + defaultNickname = await teto.getNicknameByID(n); + } on TetrioPlayerNotExist { + defaultNickname = n; + } + } else { + defaultNickname = "dan63047"; + } + setState(() {}); } Future _setPlayer(String player) async { await prefs.setString('player', player); + await _setDefaultNickname(player); } @override @@ -48,24 +67,25 @@ class SettingsState extends State { children: [ ListTile( title: const Text("So there you gonna be able to change some settings"), - subtitle: const Text( - "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."), + subtitle: const Text("Only \"Your TETR.IO account\" implemented yet. In the future you will able to import and export app sqlite database."), trailing: Switch( value: true, onChanged: (bool value) {}, ), ), ListTile( - 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"), + title: const Text("Your TETR.IO account"), + trailing: Text(defaultNickname), onTap: () => showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: const Text("Your TETR.IO account nickname or ID", style: TextStyle(fontFamily: "Eurostile Round Extended")), content: SingleChildScrollView( - child: ListBody(children: [TextField(controller: _playertext, maxLength: 25)]), + child: ListBody(children: [ + const Text( + "Every time when app loads, stats of that player will be fetched. Please prefer ID over nickname because nickname can be changed."), + TextField(controller: _playertext, maxLength: 25) + ]), ), actions: [ TextButton( diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart new file mode 100644 index 0000000..7804372 --- /dev/null +++ b/lib/views/state_view.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/widgets/tl_thingy.dart'; +import 'package:tetra_stats/widgets/user_thingy.dart'; + +class StateView extends StatefulWidget { + final TetrioPlayer state; + const StateView({Key? key, required this.state}) : super(key: key); + + @override + State createState() => StateState(); +} + +class StateState extends State { + late ScrollController _scrollController; + + @override + void initState() { + _scrollController = ScrollController(); + super.initState(); + } + + void _justUpdate() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("${widget.state.username.toUpperCase()} account on ${widget.state.state}"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: UserThingy( + player: widget.state, + showStateTimestamp: true, + setState: _justUpdate, + )) + ]; + }, + body: TLThingy(tl: widget.state.tlSeason1, userID: widget.state.userId)))); + } +} diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart new file mode 100644 index 0000000..be48b39 --- /dev/null +++ b/lib/views/states_view.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/views/state_view.dart'; + +class StatesView extends StatefulWidget { + final List states; + const StatesView({Key? key, required this.states}) : super(key: key); + + @override + State createState() => StatesState(); +} + +class StatesState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("${widget.states.length} states of ${widget.states.last.username.toUpperCase()} account"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: ListView.builder( + itemCount: widget.states.length, + itemBuilder: (context, index) { + return ListTile( + title: Text("On ${widget.states[index].state}"), + subtitle: Text("Level ${widget.states[index].level.toStringAsFixed(2)} level, ${widget.states[index].gameTime} of gametime"), + trailing: IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () {}, + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StateView(state: widget.states[index]), + ), + ); + }, + ); + }))); + } +} diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart index eb710cb..130a760 100644 --- a/lib/views/tracked_players_view.dart +++ b/lib/views/tracked_players_view.dart @@ -1,22 +1,23 @@ import 'package:flutter/material.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; +import 'package:tetra_stats/views/states_view.dart'; final TetrioService teto = TetrioService(); -class StatesView extends StatefulWidget { - const StatesView({Key? key}) : super(key: key); +class TrackedPlayersView extends StatefulWidget { + const TrackedPlayersView({Key? key}) : super(key: key); @override - State createState() => StatesState(); + State createState() => TrackedPlayersState(); } -class StatesState extends State { +class TrackedPlayersState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("Players you track"), + title: const Text("Stored data"), ), backgroundColor: Colors.black, body: SafeArea( @@ -38,7 +39,7 @@ class StatesState extends State { child: Padding( padding: const EdgeInsets.only(left: 16), child: Text( - 'There is ${allPlayers.length} players', + 'There are ${allPlayers.length} players', style: TextStyle(color: Colors.white, fontSize: 25), ), )), @@ -51,7 +52,18 @@ class StatesState extends State { return ListTile( title: Text("${allPlayers[keys[index]]?.last.username}: ${allPlayers[keys[index]]?.length} states"), subtitle: Text("From ${allPlayers[keys[index]]?.first.state} until ${allPlayers[keys[index]]?.last.state}"), - onTap: () {}, + trailing: IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () {}, + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StatesView(states: allPlayers[keys[index]]!), + ), + ); + }, ); })); case ConnectionState.done: diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart new file mode 100644 index 0000000..6489211 --- /dev/null +++ b/lib/widgets/stat_sell_num.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class StatCellNum extends StatelessWidget { + const StatCellNum({required this.playerStat, required this.playerStatLabel, required this.isScreenBig, this.snackBar, this.fractionDigits}); + + final num playerStat; + final String playerStatLabel; + final bool isScreenBig; + final String? snackBar; + final int? fractionDigits; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + fractionDigits != null ? playerStat.toStringAsFixed(fractionDigits!) : playerStat.floor().toString(), + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: isScreenBig ? 32 : 24, + ), + ), + snackBar == null + ? Text( + playerStatLabel, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: "Eurostile Round", + fontSize: 16, + ), + ) + : TextButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(snackBar!))); + }, + style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), + child: Text( + playerStatLabel, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: "Eurostile Round", + fontSize: 16, + ), + )), + ], + ); + } +} diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart new file mode 100644 index 0000000..a030ca8 --- /dev/null +++ b/lib/widgets/tl_thingy.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'dart:developer' as developer; +import 'package:fl_chart/fl_chart.dart'; +import 'package:tetra_stats/widgets/stat_sell_num.dart'; + +class TLThingy extends StatelessWidget { + final TetraLeagueAlpha tl; + final String userID; + const TLThingy({Key? key, required this.tl, required this.userID}) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + bool bigScreen = constraints.maxWidth > 768; + return ListView.builder( + physics: const ClampingScrollPhysics(), + itemCount: 1, + itemBuilder: (BuildContext context, int index) { + return Column( + children: (tl.gamesPlayed > 0) + ? [ + Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (tl.gamesPlayed >= 10) + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + clipBehavior: Clip.hardEdge, + children: [ + userID == "5e32fc85ab319c2ab1beb07c" // he love her so much, you can't even imagine + ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? + : Image.asset("res/tetrio_tl_alpha_ranks/${tl.rank}.png", height: 128), + Column( + children: [ + Text("${tl.rating.toStringAsFixed(2)} TR", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Text( + "Top ${(tl.percentile * 100).toStringAsFixed(2)}% (${tl.percentileRank.toUpperCase()}) • Top Rank: ${tl.bestRank.toUpperCase()} • Glicko: ${tl.glicko?.toStringAsFixed(2)}±${tl.rd?.toStringAsFixed(2)}${tl.decaying ? ' • Decaying' : ''}", + textAlign: TextAlign.center, + ), + ], + ), + ], + ) + else + Text("${10 - tl.gamesPlayed} games until being ranked", + softWrap: true, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: bigScreen ? 42 : 28, + overflow: TextOverflow.visible, + )), + Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + if (tl.apm != null) + StatCellNum(playerStat: tl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Attack\nPer Minute"), + if (tl.pps != null) + StatCellNum(playerStat: tl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Pieces\nPer Second"), + if (tl.apm != null) StatCellNum(playerStat: tl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Versus\nScore"), + if (tl.standing > 0) StatCellNum(playerStat: tl.standing, isScreenBig: bigScreen, playerStatLabel: "Leaderboard\nplacement"), + if (tl.standingLocal > 0) StatCellNum(playerStat: tl.standingLocal, isScreenBig: bigScreen, playerStatLabel: "Country LB\nplacement"), + StatCellNum(playerStat: tl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: "Games\nplayed"), + StatCellNum(playerStat: tl.gamesWon, isScreenBig: bigScreen, playerStatLabel: "Games\nwon"), + StatCellNum(playerStat: tl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Winrate\nprecentage"), + ], + ), + ), + if (tl.nerdStats != null) + Column( + children: [ + Text("Nerd Stats", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + StatCellNum(playerStat: tl.nerdStats!.app, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Attack\nPer Piece"), + StatCellNum(playerStat: tl.nerdStats!.vsapm, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "VS/APM"), + StatCellNum( + playerStat: tl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Downstack\nPer Second"), + StatCellNum( + playerStat: tl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Downstack\nPer Piece"), + StatCellNum(playerStat: tl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "APP + DS/P"), + StatCellNum(playerStat: tl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Cheese\nIndex"), + StatCellNum(playerStat: tl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Garbage\nEfficiency"), + StatCellNum(playerStat: tl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: "Weighted\nAPP"), + StatCellNum(playerStat: tl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: "Area") + ]), + ) + ], + ), + if (tl.estTr != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 48), + child: SizedBox( + width: bigScreen ? MediaQuery.of(context).size.width * 0.4 : MediaQuery.of(context).size.width * 0.85, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Est. of TR:", + style: TextStyle(fontSize: 24), + ), + Text( + tl.estTr!.esttr.toStringAsFixed(2), + style: const TextStyle(fontSize: 24), + ), + ], + ), + if (tl.rating >= 0) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Accuracy:", + style: TextStyle(fontSize: 24), + ), + Text( + tl.esttracc!.toStringAsFixed(2), + style: const TextStyle(fontSize: 24), + ), + ], + ), + ], + ), + ), + ), + if (tl.nerdStats != null) + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceAround, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 48), + child: SizedBox( + height: 300, + width: 300, + child: RadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), + 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: + return RadarChartTitle( + text: 'APM', + angle: angle, + ); + case 1: + return RadarChartTitle( + text: 'PPS', + angle: angle, + ); + case 2: + return RadarChartTitle(text: 'VS', angle: angle); + case 3: + return RadarChartTitle(text: 'APP', angle: angle + 180); + case 4: + return RadarChartTitle(text: 'DS/S', angle: angle + 180); + case 5: + return RadarChartTitle(text: 'DS/P', angle: angle + 180); + case 6: + return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180); + case 7: + return RadarChartTitle(text: 'VS/APM', angle: angle + 180); + case 8: + return RadarChartTitle(text: 'Cheese', angle: angle); + case 9: + return RadarChartTitle(text: 'Gb Eff.', angle: angle); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + dataEntries: [ + RadarEntry(value: tl.apm! * 1), + RadarEntry(value: tl.pps! * 45), + RadarEntry(value: tl.vs! * 0.444), + RadarEntry(value: tl.nerdStats!.app * 185), + RadarEntry(value: tl.nerdStats!.dss * 175), + RadarEntry(value: tl.nerdStats!.dsp * 450), + RadarEntry(value: tl.nerdStats!.appdsp * 140), + RadarEntry(value: tl.nerdStats!.vsapm * 60), + RadarEntry(value: tl.nerdStats!.cheese * 1.25), + RadarEntry(value: tl.nerdStats!.gbe * 315), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + 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), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 48), + child: SizedBox( + height: 300, + width: 300, + child: RadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), + 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: + return RadarChartTitle( + text: 'Opener', + angle: angle, + ); + case 1: + return RadarChartTitle( + text: 'Stride', + angle: angle, + ); + case 2: + return RadarChartTitle(text: 'Inf Ds', angle: angle + 180); + case 3: + return RadarChartTitle(text: 'Plonk', angle: angle); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + dataEntries: [ + RadarEntry(value: tl.playstyle!.opener), + RadarEntry(value: tl.playstyle!.stride), + RadarEntry(value: tl.playstyle!.infds), + RadarEntry(value: tl.playstyle!.plonk), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 1), + const RadarEntry(value: 1), + const RadarEntry(value: 1), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional + ), + ), + ), + ], + ) + ] + : [ + Text("That user never played Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + ], + ); + }, + ); + }); + } +} diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart new file mode 100644 index 0000000..20250d2 --- /dev/null +++ b/lib/widgets/user_thingy.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/views/tracked_players_view.dart'; +import 'dart:developer' as developer; +import 'package:tetra_stats/widgets/stat_sell_num.dart'; + +extension StringExtension on String { + String capitalize() { + return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; + } +} + +Future copyToClipboard(String text) async { + await Clipboard.setData(ClipboardData(text: text)); +} + +class UserThingy extends StatelessWidget { + final TetrioPlayer player; + final bool showStateTimestamp; + final Function setState; + const UserThingy({Key? key, required this.player, required this.showStateTimestamp, required this.setState}) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + bool bigScreen = constraints.maxWidth > 768; + double bannerHeight = bigScreen ? 240 : 120; + double pfpHeight = 128; + return Column( + children: [ + Flex( + direction: Axis.vertical, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.topCenter, + children: [ + if (player.bannerRevision != null) + Image.network( + "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), + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: player.role == "banned" + ? Image.asset( + "res/avatars/tetrio_banned.png", + fit: BoxFit.fitHeight, + height: pfpHeight, + ) + : 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: pfpHeight, + ); + }) + : Image.asset( + "res/avatars/tetrio_anon.png", + fit: BoxFit.fitHeight, + height: pfpHeight, + ), + ), + ), + if (player.verified) + Padding( + padding: EdgeInsets.fromLTRB( + pfpHeight - 22, + bigScreen // verified icon top padding: + ? (player.bannerRevision != null ? bannerHeight + pfpHeight - 96 : pfpHeight + pfpHeight - 32) // for big screen + : (player.bannerRevision != null ? bannerHeight + pfpHeight - 58 : pfpHeight + pfpHeight - 32), // for small screen + 0, + 0), + child: const Icon(Icons.verified), + ) + ], + ), + Flexible( + child: Column( + children: [ + Text(player.username, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + 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!"))); + }), + ], + )), + showStateTimestamp + ? Text("Fetched ${player.state}") + : Wrap(direction: Axis.horizontal, alignment: WrapAlignment.center, spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, children: [ + FutureBuilder( + future: teto.isPlayerTracking(player.userId), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.data != null && snapshot.data!) { + return Column( + children: [ + IconButton( + icon: Icon(Icons.person_remove), + onPressed: () { + teto.deletePlayerToTrack(player.userId).then((value) => setState()); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Removed from tracking list!"))); + }, + ), + Text("Stop tracking") + ], + ); + } else { + return Column( + children: [ + IconButton( + icon: Icon(Icons.person_add), + onPressed: () { + teto.addPlayerToTrack(player).then((value) => setState()); + teto.storeState(player); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Added to tracking list!"))); + }, + ), + Text("Track") + ], + ); + } + } + }), + Column( + children: [ + IconButton( + icon: Icon(Icons.balance), + onPressed: () {}, + ), + Text("Compare") + ], + ) + ]), + ], + ), + (player.role != "banned") + ? Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, // hard WHAT??? + children: [ + StatCellNum( + playerStat: player.level, + playerStatLabel: "XP Level", + isScreenBig: bigScreen, + snackBar: "${player.xp.floor().toString()} XP, ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} % until next level", + ), + if (player.gameTime >= Duration.zero) + StatCellNum( + playerStat: player.gameTime.inHours, playerStatLabel: "Hours\nPlayed", isScreenBig: bigScreen, snackBar: player.gameTime.toString()), + if (player.gamesPlayed >= 0) StatCellNum(playerStat: player.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: "Online\nGames"), + if (player.gamesWon >= 0) StatCellNum(playerStat: player.gamesWon, isScreenBig: bigScreen, playerStatLabel: "Games\nWon"), + if (player.friendCount > 0) StatCellNum(playerStat: player.friendCount, isScreenBig: bigScreen, playerStatLabel: "Friends"), + ], + ) + : Text( + "BANNED", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontWeight: FontWeight.w900, + color: Colors.red, + fontSize: bigScreen ? 60 : 45, + ), + ), + if (player.badstanding != null && player.badstanding!) + Text( + "BAD STANDING", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: "Eurostile Round Extended", + fontWeight: FontWeight.w900, + color: Colors.red, + fontSize: bigScreen ? 60 : 45, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Text( + "${player.country != null ? "${player.country?.toUpperCase()} • " : ""}${player.role.capitalize()} account ${player.registrationTime == null ? "that was from very beginning" : 'created ${player.registrationTime}'}${player.botmaster != null ? " by ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? "Not a supporter" : "Supporter tier ${player.supporterTier}"}", + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: "Eurostile Round", + fontSize: 16, + )), + ) + ], + ), + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + for (var badge in player.badges) + IconButton( + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + badge.label, + style: const TextStyle(fontFamily: "Eurostile Round Extended"), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 25, + children: [ + Image.asset("res/tetrio_badges/${badge.badgeId}.png"), + Text(badge.ts != null ? "Obtained ${badge.ts}" : "That badge was assigned manualy by TETR.IO admins"), + ], + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ), + tooltip: badge.label, + icon: Image.asset( + "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/pubspec.yaml b/pubspec.yaml index 11beba8..1fb3b73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ flutter: - 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/ift_1.png - res/tetrio_badges/ift_2.png - res/tetrio_badges/ift_3.png @@ -100,20 +101,28 @@ flutter: - res/tetrio_badges/mmc_tabi_superlobby.png - res/tetrio_badges/mmc_tabi_superlobby2.png - res/tetrio_badges/mmc_tabi_superlobby3.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/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 @@ -121,8 +130,16 @@ flutter: - res/tetrio_badges/ttsdtc_1.png - res/tetrio_badges/ttsdtc_2.png - res/tetrio_badges/ttsdtc_3.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 diff --git a/res/tetrio_badges/hnprism_3.png b/res/tetrio_badges/hnprism_3.png new file mode 100644 index 0000000..5caa0e2 Binary files /dev/null and b/res/tetrio_badges/hnprism_3.png differ diff --git a/res/tetrio_badges/redgevo_1.png b/res/tetrio_badges/redgevo_1.png new file mode 100644 index 0000000..fd03b88 Binary files /dev/null and b/res/tetrio_badges/redgevo_1.png differ diff --git a/res/tetrio_badges/sakurablend_2.png b/res/tetrio_badges/sakurablend_2.png new file mode 100644 index 0000000..b5ce4ca Binary files /dev/null and b/res/tetrio_badges/sakurablend_2.png differ diff --git a/res/tetrio_badges/sakurablend_3.png b/res/tetrio_badges/sakurablend_3.png new file mode 100644 index 0000000..b15fcc0 Binary files /dev/null and b/res/tetrio_badges/sakurablend_3.png differ diff --git a/res/tetrio_badges/scuncapped_3.png b/res/tetrio_badges/scuncapped_3.png new file mode 100644 index 0000000..4925bbd Binary files /dev/null and b/res/tetrio_badges/scuncapped_3.png differ diff --git a/res/tetrio_badges/sfu_raccoon_2.png b/res/tetrio_badges/sfu_raccoon_2.png new file mode 100644 index 0000000..9f98af0 Binary files /dev/null and b/res/tetrio_badges/sfu_raccoon_2.png differ diff --git a/res/tetrio_badges/sfu_raccoon_3.png b/res/tetrio_badges/sfu_raccoon_3.png new file mode 100644 index 0000000..e4ebc90 Binary files /dev/null and b/res/tetrio_badges/sfu_raccoon_3.png differ diff --git a/res/tetrio_badges/taws_u50_2.png b/res/tetrio_badges/taws_u50_2.png new file mode 100644 index 0000000..0731799 Binary files /dev/null and b/res/tetrio_badges/taws_u50_2.png differ diff --git a/res/tetrio_badges/tawshdsl_capped.png b/res/tetrio_badges/tawshdsl_capped.png new file mode 100644 index 0000000..3a252dc Binary files /dev/null and b/res/tetrio_badges/tawshdsl_capped.png differ diff --git a/res/tetrio_badges/ubcea_1.png b/res/tetrio_badges/ubcea_1.png new file mode 100644 index 0000000..8076904 Binary files /dev/null and b/res/tetrio_badges/ubcea_1.png differ diff --git a/res/tetrio_badges/ubcea_2.png b/res/tetrio_badges/ubcea_2.png new file mode 100644 index 0000000..3f4d46e Binary files /dev/null and b/res/tetrio_badges/ubcea_2.png differ diff --git a/res/tetrio_badges/ubcea_3.png b/res/tetrio_badges/ubcea_3.png new file mode 100644 index 0000000..f9ff961 Binary files /dev/null and b/res/tetrio_badges/ubcea_3.png differ diff --git a/res/tetrio_badges/underdog_1.png b/res/tetrio_badges/underdog_1.png new file mode 100644 index 0000000..9307ecc Binary files /dev/null and b/res/tetrio_badges/underdog_1.png differ diff --git a/res/tetrio_badges/underdog_2.png b/res/tetrio_badges/underdog_2.png new file mode 100644 index 0000000..a2b0a74 Binary files /dev/null and b/res/tetrio_badges/underdog_2.png differ diff --git a/res/tetrio_badges/underdog_3.png b/res/tetrio_badges/underdog_3.png new file mode 100644 index 0000000..272fcc2 Binary files /dev/null and b/res/tetrio_badges/underdog_3.png differ diff --git a/res/tetrio_badges/wpl_2.png b/res/tetrio_badges/wpl_2.png new file mode 100644 index 0000000..6dc8536 Binary files /dev/null and b/res/tetrio_badges/wpl_2.png differ diff --git a/res/tetrio_badges/wpl_3.png b/res/tetrio_badges/wpl_3.png new file mode 100644 index 0000000..1922237 Binary files /dev/null and b/res/tetrio_badges/wpl_3.png differ