From 6c8e7b9147a6cf61bde7238a0394a0cf7044e97a Mon Sep 17 00:00:00 2001 From: dan63047 Date: Wed, 6 Nov 2024 02:07:43 +0300 Subject: [PATCH] Big refactoring, upated design of nerd stats, info center ideas --- lib/data_objects/tetrio_constants.dart | 13 + lib/main.dart | 47 +- lib/utils/copy_to_clipboard.dart | 5 + lib/utils/numers_formats.dart | 1 + lib/views/calc_view.dart | 146 -- lib/views/compare_view.dart | 1871 ---------------- lib/views/customization_view.dart | 178 -- lib/views/destination_calculator.dart | 6 +- lib/views/destination_cutoffs.dart | 3 +- lib/views/destination_graphs.dart | 5 +- lib/views/destination_home.dart | 24 +- lib/views/destination_info.dart | 95 + lib/views/destination_leaderboards.dart | 2 +- lib/views/destination_saved_data.dart | 4 +- lib/views/destination_settings.dart | 3 +- lib/views/main_view.dart | 1943 +++------------- lib/views/main_view_tiles.dart | 2106 ------------------ lib/views/mathes_view.dart | 84 - lib/views/rank_averages_view.dart | 554 ----- lib/views/rank_view.dart | 2 +- lib/views/ranks_averages_view.dart | 176 -- lib/views/settings_view.dart | 302 --- lib/views/sprint_and_blitz_averages.dart | 10 +- lib/views/state_view.dart | 2 +- lib/views/states_view.dart | 119 - lib/views/tl_leaderboard_view.dart | 218 -- lib/views/tl_match_view.dart | 2 +- lib/views/tracked_players_view.dart | 133 -- lib/views/user_view.dart | 2 +- lib/views/zenith_record_view.dart | 38 - lib/widgets/alpha_league_entry_thingy.dart | 40 + lib/widgets/badges_thingy.dart | 90 + lib/widgets/beta_league_entry_thingy.dart | 131 ++ lib/widgets/compare_thingy.dart | 147 ++ lib/widgets/distinguishment_thingy.dart | 107 + lib/widgets/error_thingy.dart | 84 + lib/widgets/fake_distinguishment_thingy.dart | 68 + lib/widgets/future_error.dart | 38 + lib/widgets/gauget_num.dart | 111 - lib/widgets/gauget_thingy.dart | 77 + lib/widgets/graphs.dart | 393 ++-- lib/widgets/info_thingy.dart | 20 + lib/widgets/nerd_stats_thingy.dart | 152 ++ lib/widgets/news_thingy.dart | 188 ++ lib/widgets/singleplayer_record.dart | 2 +- lib/widgets/tl_rating_thingy.dart | 70 +- lib/widgets/tl_records_thingy.dart | 38 + lib/widgets/tl_thingy.dart | 431 +--- lib/widgets/user_thingy.dart | 783 +++---- lib/widgets/zenith_thingy.dart | 387 ++-- res/images/info card 1 focus.png | Bin 0 -> 82812 bytes res/images/info card 1.png | Bin 0 -> 70091 bytes 52 files changed, 2502 insertions(+), 8949 deletions(-) create mode 100644 lib/utils/copy_to_clipboard.dart delete mode 100644 lib/views/calc_view.dart delete mode 100644 lib/views/compare_view.dart delete mode 100644 lib/views/customization_view.dart create mode 100644 lib/views/destination_info.dart delete mode 100644 lib/views/main_view_tiles.dart delete mode 100644 lib/views/mathes_view.dart delete mode 100644 lib/views/rank_averages_view.dart delete mode 100644 lib/views/ranks_averages_view.dart delete mode 100644 lib/views/settings_view.dart delete mode 100644 lib/views/states_view.dart delete mode 100644 lib/views/tl_leaderboard_view.dart delete mode 100644 lib/views/tracked_players_view.dart delete mode 100644 lib/views/zenith_record_view.dart create mode 100644 lib/widgets/alpha_league_entry_thingy.dart create mode 100644 lib/widgets/badges_thingy.dart create mode 100644 lib/widgets/beta_league_entry_thingy.dart create mode 100644 lib/widgets/compare_thingy.dart create mode 100644 lib/widgets/distinguishment_thingy.dart create mode 100644 lib/widgets/error_thingy.dart create mode 100644 lib/widgets/fake_distinguishment_thingy.dart create mode 100644 lib/widgets/future_error.dart delete mode 100644 lib/widgets/gauget_num.dart create mode 100644 lib/widgets/gauget_thingy.dart create mode 100644 lib/widgets/info_thingy.dart create mode 100644 lib/widgets/nerd_stats_thingy.dart create mode 100644 lib/widgets/news_thingy.dart create mode 100644 lib/widgets/tl_records_thingy.dart create mode 100644 res/images/info card 1 focus.png create mode 100644 res/images/info card 1.png diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index 3d9d127..0588125 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; const int currentSeason = 2; +final DateTime sprintAndBlitzRelevance = DateTime(2024, 8, 25); const double noTrRd = 60.9; const double apmWeight = 1; const double ppsWeight = 45; @@ -12,6 +13,18 @@ const double appdspWeight = 140; const double vsapmWeight = 60; const double cheeseWeight = 1.25; const double gbeWeight = 315; + +const Map xpTableScuffed = { // level: xp required + 05000: 67009018.4885772, + 10000: 763653437.386, + 15000: 2337651144.54149, + 20000: 4572735210.50902, + 25000: 7376166347.04745, + 30000: 10693620096.2168, + 40000: 18728882739.482, + 50000: 28468683855.2853 +}; + const List ranks = [ "d", "d+", diff --git a/lib/main.dart b/lib/main.dart index 9ff7a57..d05dfac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,19 +7,12 @@ import 'dart:developer' as developer; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; -import 'package:tetra_stats/views/customization_view.dart'; -import 'package:tetra_stats/views/ranks_averages_view.dart'; -import 'package:tetra_stats/views/sprint_and_blitz_averages.dart'; -import 'package:tetra_stats/views/tl_leaderboard_view.dart'; import 'package:window_manager/window_manager.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; -import 'package:tetra_stats/views/settings_view.dart'; -import 'package:tetra_stats/views/tracked_players_view.dart'; -import 'package:tetra_stats/views/calc_view.dart'; +import 'package:tetra_stats/views/main_view.dart'; import 'package:go_router/go_router.dart'; late final PackageInfo packageInfo; @@ -72,44 +65,6 @@ final router = GoRouter( GoRoute( path: "/", builder: (_, __) => const MainView(), - routes: [ - GoRoute( - path: 'settings', - builder: (_, __) => const SettingsView(), - routes: [ - GoRoute( - path: 'customization', - builder: (_, __) => const CustomizationView(), - ), - ] - ), - GoRoute( - path: "leaderboard", - builder: (_, __) => const TLLeaderboardView(), - routes: [ - GoRoute( - path: "LBvalues", - builder: (_, __) => const RankAveragesView(), - ), - ] - ), - GoRoute( - path: "LBvalues", - builder: (_, __) => const RankAveragesView(), - ), - GoRoute( - path: 'states', - builder: (_, __) => const TrackedPlayersView(), - ), - GoRoute( - path: 'calc', - builder: (_, __) => const CalcView(), - ), - GoRoute( - path: 'sprintAndBlitzAverages', - builder: (_, __) => const SprintAndBlitzView(), - ) - ] ), GoRoute( // that one intended for Android users, that can open https://ch.tetr.io/u/ links path: "/u/:userId", diff --git a/lib/utils/copy_to_clipboard.dart b/lib/utils/copy_to_clipboard.dart new file mode 100644 index 0000000..727a6c8 --- /dev/null +++ b/lib/utils/copy_to_clipboard.dart @@ -0,0 +1,5 @@ +import 'package:flutter/services.dart'; + +Future copyToClipboard(String text) async { + await Clipboard.setData(ClipboardData(text: text)); +} \ No newline at end of file diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index f359edb..3e72b3d 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -2,6 +2,7 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; final NumberFormat compareIntf = NumberFormat("+#,###;-#,###")..maximumFractionDigits = 0; +final NumberFormat fDiff = NumberFormat("+#,###.####;-#,###.####"); final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2; final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart deleted file mode 100644 index 603e4d6..0000000 --- a/lib/views/calc_view.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/est_tr.dart'; -import 'package:tetra_stats/data_objects/nerd_stats.dart'; -import 'package:tetra_stats/data_objects/playstyle.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:window_manager/window_manager.dart'; - -double? apm; -double? pps; -double? vs; -NerdStats? nerdStats; -EstTr? estTr; -Playstyle? playstyle; -late String oldWindowTitle; - -class CalcView extends StatefulWidget { - const CalcView({super.key}); - - @override - State createState() => CalcState(); -} - -class CalcState extends State { - TextEditingController ppsController = TextEditingController(); - TextEditingController apmController = TextEditingController(); - TextEditingController vsController = TextEditingController(); - - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.statsCalc}"); - } - super.initState(); - } - - @override - void dispose() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - void calc() { - apm = double.tryParse(apmController.text); - pps = double.tryParse(ppsController.text); - vs = double.tryParse(vsController.text); - if (apm != null && pps != null && vs != null) { - nerdStats = NerdStats(apm!, pps!, vs!); - estTr = EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp, nerdStats!.gbe); - playstyle = Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm, nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank); - setState(() {}); - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Please, enter valid values"))); - } - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return Scaffold( - appBar: AppBar( - title: Text(t.statsCalc), - ), - backgroundColor: Colors.black, - body: SingleChildScrollView( - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 768), - child: Column(children: [ - Padding( - padding: const EdgeInsets.fromLTRB(14, 16, 16, 32), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: TextField( - onSubmitted: (value) => calc(), - controller: apmController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("APM"), alignLabelWithHint: true), - ), - )), - Expanded( - child: TextField( - onSubmitted: (value) => calc(), - controller: ppsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("PPS"), alignLabelWithHint: true), - )), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 12), - child: TextField( - onSubmitted: (value) => calc(), - controller: vsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(label: Text("VS"), alignLabelWithHint: true), - ), - )), - TextButton( - onPressed: () => calc(), - child: Text(t.calc), - ), - ], - ), - ), - const Divider(), - if (nerdStats == null) Text(t.calcViewNoValues) - else Column(children: [ - _ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.vsapm, label: "VS/APM", fractionDigits: 3), - _ListEntry(value: nerdStats!.dss, label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.dsp, label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.appdsp, label: "APP + DS/P", fractionDigits: 3), - _ListEntry(value: nerdStats!.cheese, label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.gbe, label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.nyaapp, label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: nerdStats!.area, label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3), - _ListEntry(value: estTr!.esttr, label: t.statCellNum.estOfTR, fractionDigits: 3), - Graphs(apm!, pps!, vs!, nerdStats!, playstyle!) - ],) - ],), - ), - ), - ), - ); - } -} - -class _ListEntry extends StatelessWidget { - final double value; - final String label; - final int? fractionDigits; - const _ListEntry({required this.value, required this.label, this.fractionDigits}); - - @override - Widget build(BuildContext context) { - NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits ?? 0); - return ListTile(title: Text(label), trailing: Text(f.format(value), style: const TextStyle(fontSize: 22))); - } -} diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart deleted file mode 100644 index 8e78493..0000000 --- a/lib/views/compare_view.dart +++ /dev/null @@ -1,1871 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:io'; -import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/summaries.dart'; -import 'package:tetra_stats/data_objects/tetra_league.dart'; -import 'package:tetra_stats/data_objects/tetrio_constants.dart'; -import 'package:tetra_stats/data_objects/tetrio_zen.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart' show teto; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/widgets/vs_graphs.dart'; -import 'package:window_manager/window_manager.dart'; - -enum Mode{ - player, - stats, - averages -} -Mode greenSideMode = Mode.player; -List theGreenSide = [null, null, null]; // TetrioPlayer?, List>?, Summary -Mode redSideMode = Mode.player; -List theRedSide = [null, null, null]; -final DateFormat dateFormat = DateFormat.yMd(LocaleSettings.currentLocale.languageCode).add_Hm(); -var numbersReg = RegExp(r'\d+(\.\d*)*'); -late String oldWindowTitle; - -class CompareView extends StatefulWidget { - final List greenSide; - final List redSide; - final Mode greenMode; - final Mode redMode; - const CompareView({super.key, required this.greenSide, required this.redSide, required this.greenMode, required this.redMode}); - - @override - State createState() => CompareState(); -} - -class CompareState extends State { - late ScrollController _scrollController; - - @override - void initState() { - theGreenSide = widget.greenSide; - fetchGreenSide(widget.greenSide[0].userId); - if (widget.redSide[0] != null) fetchRedSide(widget.redSide[0].userId); - _scrollController = ScrollController(); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - } - super.initState(); - } - - @override - void dispose(){ - theGreenSide = [null, null, null]; - greenSideMode = Mode.player; - theRedSide = [null, null, null]; - redSideMode = Mode.player; - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - void fetchRedSide(String user) async { - try { - if (user.startsWith("\$avg")){ - try{ - //var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; - //Summaries summary = Summaries("avg${user.substring(4).toLowerCase()}", average, TetrioZen(level: 0, score: 0)); - redSideMode = Mode.averages; - //theRedSide = [null, null, summary]; - return setState(() {}); - }on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); - return; - } - } - var tearDownToNumbers = numbersReg.allMatches(user); - if (tearDownToNumbers.length == 3) { - redSideMode = Mode.stats; - var threeNumbers = tearDownToNumbers.toList(); - double apm = double.parse(threeNumbers[0][0]!); - double pps = double.parse(threeNumbers[1][0]!); - double vs = double.parse(threeNumbers[2][0]!); - theRedSide = [null, - null, - Summaries(user, TetraLeague( - id: "", - timestamp: DateTime.now(), - apm: apm, - pps: pps, - vs: vs, - rd: noTrRd, - gamesPlayed: -1, - gamesWon: -1, - bestRank: "z", - decaying: true, - tr: -1, - gxe: -1, - rank: "z", - percentileRank: "z", - percentile: 1, - standing: -1, - standingLocal: -1, - nextAt: -1, - prevAt: -1, season: currentSeason), TetrioZen(level: 0, score: 0))]; - return setState(() {}); - } - var player = await teto.fetchPlayer(user); - Summaries summary = await teto.fetchSummaries(player.userId); - redSideMode = Mode.player; - //late List states; - // List>? dStates = >[]; - // try{ - // states = await teto.getPlayer(player.userId); - // for (final TetrioPlayer state in states) { - // dStates.add(DropdownMenuItem( - // value: state, child: Text(dateFormat.format(state.state)))); - // } - // dStates.firstWhere((element) => element.value == player, orElse: () { - // dStates?.add(DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne))); - // return DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne)); - // },); - // }on Exception { - // dStates = null; - // } - theRedSide = [player, null, summary]; - } on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); - } - _justUpdate(); - } - - void changeRedSide(TetraLeague user) { - setState(() { - //theRedSide[0] = user; - theRedSide[2].league = user; - }); - } - - void fetchGreenSide(String user) async { - try { - if (user.startsWith("\$avg")){ - try{ - //var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; - //Summaries summary = Summaries("avg${user.substring(4).toLowerCase()}", average, TetrioZen(level: 0, score: 0)); - greenSideMode = Mode.averages; - //theGreenSide = [null, null, summary]; - return setState(() {}); - }on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); - return; - } - } - var tearDownToNumbers = numbersReg.allMatches(user); - if (tearDownToNumbers.length == 3) { - greenSideMode = Mode.stats; - var threeNumbers = tearDownToNumbers.toList(); - double apm = double.parse(threeNumbers[0][0]!); - double pps = double.parse(threeNumbers[1][0]!); - double vs = double.parse(threeNumbers[2][0]!); - theGreenSide = [null, - null, - Summaries(user, TetraLeague( - id: "", - timestamp: DateTime.now(), - apm: apm, - pps: pps, - vs: vs, - rd: noTrRd, - gamesPlayed: -1, - gamesWon: -1, - bestRank: "z", - decaying: true, - tr: -1, - gxe: -1, - rank: "z", - percentileRank: "z", - percentile: 1, - standing: -1, - standingLocal: -1, - nextAt: -1, - prevAt: -1, season: currentSeason), TetrioZen(level: 0, score: 0))]; - return setState(() {}); - } - var player = await teto.fetchPlayer(user); - Summaries summary = await teto.fetchSummaries(player.userId); - greenSideMode = Mode.player; - // late List states; - // List>? dStates = >[]; - // try{ - // states = await teto.getPlayer(player.userId); - // for (final TetrioPlayer state in states) { - // dStates.add(DropdownMenuItem( - // value: state, child: Text(dateFormat.format(state.state)))); - // } - // dStates.firstWhere((element) => element.value == player, orElse: () { - // dStates?.add(DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne))); - // return DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne)); - // },); - // }on Exception { - // dStates = null; - // } - theGreenSide = [player, null, summary]; - } on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); - } - _justUpdate(); - } - - void changeGreenSide(TetraLeague user) { - setState(() { - //theGreenSide[0] = user; - theGreenSide[2].league = user; - }); - } - - double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { - return ((1 / - (1 + pow(10, - (notyourGlicko - yourGlicko) / - (400 * sqrt(1 + (3 * pow(0.0057564273, 2) * - (pow(yourRD, 2) + pow(notyourRD, 2)) / pow(pi, 2) - ))) - ) - ) - )); - } - - void _justUpdate() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - bool bigScreen = MediaQuery.of(context).size.width > 768; - String titleGreenSide; - String titleRedSide; - switch (greenSideMode){ - case Mode.player: - titleGreenSide = theGreenSide[0] != null ? theGreenSide[0].username.toUpperCase() : "???"; - break; - case Mode.stats: - titleGreenSide = "${theGreenSide[2].league.apm} APM, ${theGreenSide[2].league.pps} PPS, ${theGreenSide[2].league.vs} VS"; - break; - case Mode.averages: - titleGreenSide = t.averageXrank(rankLetter: theGreenSide[2].league.rank.toUpperCase()); - break; - } - switch (redSideMode){ - case Mode.player: - titleRedSide = theRedSide[0] != null ? theRedSide[0].username.toUpperCase() : "???"; - break; - case Mode.stats: - titleRedSide = "${theRedSide[2].league.apm} APM, ${theRedSide[2].league.pps} PPS, ${theRedSide[2].league.vs} VS"; - break; - case Mode.averages: - titleRedSide = t.averageXrank(rankLetter: theRedSide[2].league.rank.toUpperCase()); - break; - } - windowManager.setTitle("Tetra Stats: $titleGreenSide ${t.vs} $titleRedSide"); - return Scaffold( - appBar: AppBar(title: Text("$titleGreenSide ${t.vs} $titleRedSide")), - backgroundColor: Colors.black, - body: SingleChildScrollView( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 768), - child: Column(children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.green, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, 0.4], - )), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: PlayerSelector( - data: theGreenSide, - mode: greenSideMode, - fetch: fetchGreenSide, - change: changeGreenSide, - updateState: _justUpdate, - ), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(top: 16), - child: Text("VS"), - ), - Expanded( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.red, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, 0.4], - )), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: PlayerSelector( - data: theRedSide, - mode: redSideMode, - fetch: fetchRedSide, - change: changeRedSide, - updateState: _justUpdate, - ), - ), - ), - ), - ], - ), - ), - const Divider(), - if (!listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])) Column( - children: [ - if (theGreenSide[0] != null && theRedSide[0] != null && theGreenSide[0]!.role != "banned" && theRedSide[0]!.role != "banned") - Column( - children: [ - CompareRegTimeThingy( - greenSide: theGreenSide[0].registrationTime, - redSide: theRedSide[0].registrationTime, - label: t.registred), - CompareThingy( - label: t.statCellNum.level, - greenSide: theGreenSide[0].level, - redSide: theRedSide[0].level, - higherIsBetter: true, - fractionDigits: 2, - ), - if (!theGreenSide[0].gameTime.isNegative && !theRedSide[0].gameTime.isNegative) - CompareThingy( - greenSide: theGreenSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - redSide: theRedSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - label: t.statCellNum.hoursPlayed.replaceAll(RegExp(r'\n'), " "), - higherIsBetter: true, - fractionDigits: 2, - ), - if (theGreenSide[0].gamesPlayed >= 0 && theRedSide[0].gamesPlayed >= 0) - CompareThingy( - label: t.statCellNum.onlineGames.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesPlayed, - redSide: theRedSide[0].gamesPlayed, - higherIsBetter: true, - ), - if (theGreenSide[0].gamesWon >= 0 && theRedSide[0].gamesWon >= 0) - CompareThingy( - label: t.statCellNum.gamesWon.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesWon, - redSide: theRedSide[0].gamesWon, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.friends, - greenSide: theGreenSide[0].friendCount, - redSide: theRedSide[0].friendCount, - higherIsBetter: true, - ), - const Divider(), - ], - ), - if (theGreenSide[0] != null && theRedSide[0] != null && (theGreenSide[0]!.role == "banned" || theRedSide[0]!.role == "banned")) - CompareBoolThingy( - greenSide: theGreenSide[0].role == "banned", - redSide: theRedSide[0].role == "banned", - label: t.normalBanned, - trueIsBetter: false - ), - (theGreenSide[2].league.gamesPlayed > 0 || greenSideMode == Mode.stats) && (theRedSide[2].league.gamesPlayed > 0 || redSideMode == Mode.stats) - ? Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.tetraLeague, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theRedSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "TR", - greenSide: theGreenSide[2].league.tr, - redSide: theRedSide[2].league.tr, - fractionDigits: 2, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].league.gamesPlayed, - redSide: theRedSide[2].league.gamesPlayed, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].league.gamesWon, - redSide: theRedSide[2].league.gamesWon, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "WR %", - greenSide: - theGreenSide[2].league.winrate * 100, - redSide: theRedSide[2].league.winrate * 100, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theRedSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "Glicko", - greenSide: theGreenSide[2].league.glicko!, - redSide: theRedSide[2].league.glicko!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theRedSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "RD", - greenSide: theGreenSide[2].league.rd!, - redSide: theRedSide[2].league.rd!, - fractionDigits: 3, - higherIsBetter: false, - ), - if (theGreenSide[2].league.standing > 0 && - theRedSide[2].league.standing > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpShort, - greenSide: theGreenSide[2].league.standing, - redSide: theRedSide[2].league.standing, - higherIsBetter: false, - ), - if (theGreenSide[2].league.standingLocal > 0 && - theRedSide[2].league.standingLocal > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpcShort, - greenSide: - theGreenSide[2].league.standingLocal, - redSide: theRedSide[2].league.standingLocal, - higherIsBetter: false, - ), - if (theGreenSide[2].league.apm != null && - theRedSide[2].league.apm != null) - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].league.apm!, - redSide: theRedSide[2].league.apm!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.pps != null && - theRedSide[2].league.pps != null) - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].league.pps!, - redSide: theRedSide[2].league.pps!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.vs != null && - theRedSide[2].league.vs != null) - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].league.vs!, - redSide: theRedSide[2].league.vs!, - fractionDigits: 2, - higherIsBetter: true, - ), - ], - ) - : CompareBoolThingy( - greenSide: theGreenSide[2].league.gamesPlayed > 0, - redSide: theRedSide[2].league.gamesPlayed > 0, - label: t.playedTL, - trueIsBetter: false), - if (theGreenSide[2].league.nerdStats != null && - theRedSide[2].league.nerdStats != null) - Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.nerdStats, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].league.nerdStats!.app, - redSide: theRedSide[2].league.nerdStats!.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].league.nerdStats!.vsapm, - redSide: theRedSide[2].league.nerdStats!.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].league.nerdStats!.dss, - redSide: theRedSide[2].league.nerdStats!.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].league.nerdStats!.dsp, - redSide: theRedSide[2].league.nerdStats!.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].league.nerdStats!.appdsp, - redSide: theRedSide[2].league.nerdStats!.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].league.nerdStats!.cheese, - redSide: theRedSide[2].league.nerdStats!.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].league.nerdStats!.gbe, - redSide: theRedSide[2].league.nerdStats!.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].league.nerdStats!.nyaapp, - redSide: theRedSide[2].league.nerdStats!.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].league.nerdStats!.area, - redSide: theRedSide[2].league.nerdStats!.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: theGreenSide[2].league.estTr!.esttr, - redSide: theRedSide[2].league.estTr!.esttr, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theGreenSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.accOfEstShort, - greenSide: theGreenSide[2].league.esttracc!, - redSide: theRedSide[2].league.esttracc!, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].league.playstyle!.opener, - redSide: theRedSide[2].league.playstyle!.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].league.playstyle!.plonk, - redSide: theRedSide[2].league.playstyle!.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].league.playstyle!.stride, - redSide: theRedSide[2].league.playstyle!.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].league.playstyle!.infds, - redSide: theRedSide[2].league.playstyle!.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].league.apm!, theGreenSide[2].league.pps!, theGreenSide[2].league.vs!, theGreenSide[2].league.nerdStats!, theGreenSide[2].league.playstyle!, theRedSide[2].league.apm!, theRedSide[2].league.pps!, theRedSide[2].league.vs!, theRedSide[2].league.nerdStats!, theRedSide[2].league.playstyle!), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.winChance, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (greenSideMode != Mode.stats && redSideMode != Mode.stats && - theGreenSide[2].league.gamesPlayed > 9 && theRedSide[2].league.gamesPlayed > 9) - CompareThingy( - label: t.byGlicko, - greenSide: getWinrateByTR( - theGreenSide[2].league.glicko!, - theGreenSide[2].league.rd!, - theRedSide[2].league.glicko!, - theRedSide[2].league.rd!) * - 100, - redSide: getWinrateByTR( - theRedSide[2].league.glicko!, - theRedSide[2].league.rd!, - theGreenSide[2].league.glicko!, - theGreenSide[2].league.rd!) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - CompareThingy( - label: t.byEstTR, - greenSide: getWinrateByTR( - theGreenSide[2].league.estTr!.estglicko, - theGreenSide[2].league.rd ?? noTrRd, - theRedSide[2].league.estTr!.estglicko, - theRedSide[2].league.rd ?? noTrRd) * - 100, - redSide: getWinrateByTR( - theRedSide[2].league.estTr!.estglicko, - theRedSide[2].league.rd ?? noTrRd, - theGreenSide[2].league.estTr!.estglicko, - theGreenSide[2].league.rd ?? noTrRd) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - ], - ), - if (theGreenSide[2].zenith != null && theRedSide[2].zenith != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.quickPlay, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Height", - greenSide: theGreenSide[2].zenith.stats.zenith!.altitude, - redSide: theRedSide[2].zenith.stats.zenith!.altitude, - fractionDigits: 2, - higherIsBetter: true, - postfix: "m", - ), - CompareThingy( - label: "Position", - greenSide: theGreenSide[2].zenith.rank, - redSide: theRedSide[2].zenith.rank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "Position (Country)", - greenSide: theGreenSide[2].zenith.countryRank, - redSide: theRedSide[2].zenith.countryRank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].zenith.aggregateStats.apm, - redSide: theRedSide[2].zenith.aggregateStats.apm, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].zenith.aggregateStats.pps, - redSide: theRedSide[2].zenith.aggregateStats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].zenith.aggregateStats.vs, - redSide: theRedSide[2].zenith.aggregateStats.vs, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KO's", - greenSide: theGreenSide[2].zenith.stats.kills, - redSide: theRedSide[2].zenith.stats.kills, - higherIsBetter: true, - ), - CompareThingy( - label: "CPS", - greenSide: theGreenSide[2].zenith.stats.cps, - redSide: theRedSide[2].zenith.stats.cps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Peak CPS", - greenSide: theGreenSide[2].zenith.stats.zenith!.peakrank, - redSide: theRedSide[2].zenith.stats.zenith!.peakrank, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareDurationThingy( - label: "Time", - greenSide: theGreenSide[2].zenith.stats.finalTime, - redSide: theRedSide[2].zenith.stats.finalTime, - higherIsBetter: false, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].zenith.stats.finessePercentage * 100, - redSide: theRedSide[2].zenith.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("${t.quickPlay} ${t.nerdStats}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.app, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.vsapm, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.dss, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.dsp, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].zenith.aggregateStats.nerdStats.appdsp, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].zenith.aggregateStats.nerdStats.cheese, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.gbe, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].zenith.aggregateStats.nerdStats.nyaapp, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.area, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.opener, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.plonk, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.stride, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.infds, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].zenith.aggregateStats.apm, theGreenSide[2].zenith.aggregateStats.pps, theGreenSide[2].zenith.aggregateStats.vs, theGreenSide[2].zenith.aggregateStats.nerdStats, theGreenSide[2].zenith.aggregateStats.playstyle, theRedSide[2].zenith.aggregateStats.apm, theRedSide[2].zenith.aggregateStats.pps, theRedSide[2].zenith.aggregateStats.vs, theRedSide[2].zenith.aggregateStats.nerdStats, theRedSide[2].zenith.aggregateStats.playstyle), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].zenith != null, redSide: theRedSide[2].zenith != null, label: "Played QP", trueIsBetter: true), - if (theGreenSide[2].zenithEx != null && theRedSide[2].zenithEx != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("${t.quickPlay} ${t.expert}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Height", - greenSide: theGreenSide[2].zenithEx.stats.zenith!.altitude, - redSide: theRedSide[2].zenithEx.stats.zenith!.altitude, - fractionDigits: 2, - higherIsBetter: true, - postfix: "m", - ), - CompareThingy( - label: "Position", - greenSide: theGreenSide[2].zenithEx.rank, - redSide: theRedSide[2].zenithEx.rank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "Position (Country)", - greenSide: theGreenSide[2].zenithEx.countryRank, - redSide: theRedSide[2].zenithEx.countryRank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].zenithEx.aggregateStats.apm, - redSide: theRedSide[2].zenithEx.aggregateStats.apm, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].zenithEx.aggregateStats.pps, - redSide: theRedSide[2].zenithEx.aggregateStats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].zenithEx.aggregateStats.vs, - redSide: theRedSide[2].zenithEx.aggregateStats.vs, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KO's", - greenSide: theGreenSide[2].zenithEx.stats.kills, - redSide: theRedSide[2].zenithEx.stats.kills, - higherIsBetter: true, - ), - CompareThingy( - label: "CPS", - greenSide: theGreenSide[2].zenithEx.stats.cps, - redSide: theRedSide[2].zenithEx.stats.cps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Peak CPS", - greenSide: theGreenSide[2].zenithEx.stats.zenith!.peakrank, - redSide: theRedSide[2].zenithEx.stats.zenith!.peakrank, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareDurationThingy( - label: "Time", - greenSide: theGreenSide[2].zenithEx.stats.finalTime, - redSide: theRedSide[2].zenithEx.stats.finalTime, - higherIsBetter: false, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].zenithEx.stats.finessePercentage * 100, - redSide: theRedSide[2].zenithEx.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("${t.quickPlay} ${t.expert} ${t.nerdStats}", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.app, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.vsapm, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.dss, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.dsp, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].zenithEx.aggregateStats.nerdStats.appdsp, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].zenithEx.aggregateStats.nerdStats.cheese, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.gbe, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].zenithEx.aggregateStats.nerdStats.nyaapp, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.area, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.opener, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.plonk, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.stride, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.infds, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].zenithEx.aggregateStats.apm, theGreenSide[2].zenithEx.aggregateStats.pps, theGreenSide[2].zenithEx.aggregateStats.vs, theGreenSide[2].zenithEx.aggregateStats.nerdStats, theGreenSide[2].zenithEx.aggregateStats.playstyle, theRedSide[2].zenithEx.aggregateStats.apm, theRedSide[2].zenithEx.aggregateStats.pps, theRedSide[2].zenithEx.aggregateStats.vs, theRedSide[2].zenithEx.aggregateStats.nerdStats, theRedSide[2].zenithEx.aggregateStats.playstyle), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].zenithEx != null, redSide: theRedSide[2].zenithEx != null, label: "Played QP Expert", trueIsBetter: true), - if (theGreenSide[2].sprint != null && theRedSide[2].sprint != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.sprint, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareDurationThingy( - label: "Time", - greenSide: theGreenSide[2].sprint.stats.finalTime, - redSide: theRedSide[2].sprint.stats.finalTime, - higherIsBetter: false, - ), - CompareThingy( - label: "Lines", - greenSide: theGreenSide[2].sprint.stats.lines, - redSide: theRedSide[2].sprint.stats.lines, - higherIsBetter: false, - ), - CompareThingy( - label: t.statCellNum.pieces.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].sprint.stats.piecesPlaced, - redSide: theRedSide[2].sprint.stats.piecesPlaced, - higherIsBetter: false, - ), - CompareThingy( - label: t.statCellNum.keys.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].sprint.stats.inputs, - redSide: theRedSide[2].sprint.stats.inputs, - higherIsBetter: false, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].sprint.stats.pps, - redSide: theRedSide[2].sprint.stats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KPP", - greenSide: theGreenSide[2].sprint.stats.kpp, - redSide: theRedSide[2].sprint.stats.kpp, - fractionDigits: 2, - higherIsBetter: false, - ), - CompareThingy( - label: "KPS", - greenSide: theGreenSide[2].sprint.stats.kps, - redSide: theRedSide[2].sprint.stats.kps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].sprint.stats.finessePercentage * 100, - redSide: theRedSide[2].sprint.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - CompareThingy( - label: "Holds", - greenSide: theGreenSide[2].sprint.stats.holds, - redSide: theRedSide[2].sprint.stats.holds, - higherIsBetter: false, - ), - CompareThingy( - label: "T-spins", - greenSide: theGreenSide[2].sprint.stats.tSpins, - redSide: theRedSide[2].sprint.stats.tSpins, - higherIsBetter: false, - ), - CompareThingy( - label: "Quads", - greenSide: theGreenSide[2].sprint.stats.clears.quads, - redSide: theRedSide[2].sprint.stats.clears.quads, - higherIsBetter: true, - ), - CompareThingy( - label: "PC's", - greenSide: theGreenSide[2].sprint.stats.clears.allClears, - redSide: theRedSide[2].sprint.stats.clears.allClears, - higherIsBetter: true, - ), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].sprint != null, redSide: theRedSide[2].sprint != null, label: "Played 40 Lines", trueIsBetter: true), - if (theGreenSide[2].blitz != null && theRedSide[2].blitz != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.blitz, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Score", - greenSide: theGreenSide[2].blitz.stats.score, - redSide: theRedSide[2].blitz.stats.score, - higherIsBetter: true, - ), - CompareThingy( - label: "SPP", - greenSide: theGreenSide[2].blitz.stats.spp, - redSide: theRedSide[2].blitz.stats.spp, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Level", - greenSide: theGreenSide[2].blitz.stats.level, - redSide: theRedSide[2].blitz.stats.level, - higherIsBetter: true, - ), - CompareThingy( - label: "Lines", - greenSide: theGreenSide[2].blitz.stats.lines, - redSide: theRedSide[2].blitz.stats.lines, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.pieces.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].blitz.stats.piecesPlaced, - redSide: theRedSide[2].blitz.stats.piecesPlaced, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.keys.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].blitz.stats.inputs, - redSide: theRedSide[2].blitz.stats.inputs, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].blitz.stats.pps, - redSide: theRedSide[2].blitz.stats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KPP", - greenSide: theGreenSide[2].blitz.stats.kpp, - redSide: theRedSide[2].blitz.stats.kpp, - fractionDigits: 2, - higherIsBetter: false, - ), - CompareThingy( - label: "KPS", - greenSide: theGreenSide[2].blitz.stats.kps, - redSide: theRedSide[2].blitz.stats.kps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].blitz.stats.finessePercentage * 100, - redSide: theRedSide[2].blitz.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - CompareThingy( - label: "Holds", - greenSide: theGreenSide[2].blitz.stats.holds, - redSide: theRedSide[2].blitz.stats.holds, - higherIsBetter: false, - ), - CompareThingy( - label: "T-spins", - greenSide: theGreenSide[2].blitz.stats.tSpins, - redSide: theRedSide[2].blitz.stats.tSpins, - higherIsBetter: false, - ), - CompareThingy( - label: "Quads", - greenSide: theGreenSide[2].blitz.stats.clears.quads, - redSide: theRedSide[2].blitz.stats.clears.quads, - higherIsBetter: true, - ), - CompareThingy( - label: "PC's", - greenSide: theGreenSide[2].blitz.stats.clears.allClears, - redSide: theRedSide[2].blitz.stats.clears.allClears, - higherIsBetter: true, - ), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].blitz != null, redSide: theRedSide[2].blitz != null, label: "Played Blitz", trueIsBetter: true), - if (greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Level", - greenSide: theGreenSide[2].zen.level, - redSide: theRedSide[2].zen.level, - higherIsBetter: true, - ), - CompareThingy( - label: "Score", - greenSide: theGreenSide[2].zen.score, - redSide: theRedSide[2].zen.score, - higherIsBetter: true, - ), - ], - ) - ], - ) - else Padding( - padding: const EdgeInsets.all(8.0), - child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), - ) - ], - ), - ), - ), - ), - ); - } -} - -class PlayerSelector extends StatelessWidget { - final List data; - final Mode mode; - final Function fetch; - final Function change; - final Function updateState; - const PlayerSelector( - {super.key, - required this.data, - required this.mode, - required this.updateState, - required this.fetch, - required this.change}); - - @override - Widget build(BuildContext context) { - final TextEditingController playerController = TextEditingController(); - String underFieldString = ""; - if (!listEquals(data, [null, null, null])){ - switch (mode){ - case Mode.player: - playerController.text = data[0] != null ? data[0].username : ""; - break; - case Mode.stats: - playerController.text = "${data[2].league.apm} ${data[2].league.pps} ${data[2].league.vs}"; - break; - case Mode.averages: - playerController.text = "\$avg${data[2].league.rank.toUpperCase()}"; - break; - } - } - if (!listEquals(data, [null, null, null])){ - switch (mode){ - case Mode.player: - underFieldString = data[0] != null ? data[0].toString() : "???"; - break; - case Mode.stats: - underFieldString = "${data[2].league.apm} APM, ${data[2].league.pps} PPS, ${data[2].league.vs} VS"; - break; - case Mode.averages: - underFieldString = t.averageXrank(rankLetter: data[2].league.rank.toUpperCase()); - break; - } - } - return Column( - children: [ - TextField( - autocorrect: false, - enableSuggestions: false, - maxLength: 25, - controller: playerController, - decoration: const InputDecoration(counter: Offstage()), - onSubmitted: (String value) { - underFieldString = "Fetching..."; - fetch(value); - }), - if (data[0] != null && data[1] != null) - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: DropdownButton( - items: data[1], - value: data[0], - onChanged: (value) => change(value!), - ), - ) - else Text( - underFieldString, - style: const TextStyle( - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - ), - ], - ); - } -} - -const TextStyle verdictStyle = TextStyle(fontSize: 14, fontFamily: "Eurostile Round Condensed", color: Colors.grey, height: 1.1); - -class CompareThingy extends StatelessWidget { - final num greenSide; - final num redSide; - final String label; - final bool higherIsBetter; - final int? fractionDigits; - final String? postfix; - final String? prefix; - const CompareThingy( - {super.key, - required this.greenSide, - required this.redSide, - required this.label, - required this.higherIsBetter, - this.fractionDigits, - this.prefix, - this.postfix}); - - String verdict(num greenSide, num redSide, int fraction) { - var f = NumberFormat("+#,###.##;-#,###.##"); - f.maximumFractionDigits = fraction; - return f.format((greenSide - redSide)) + (postfix ?? ""); - } - - @override - Widget build(BuildContext context) { - var f = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode); - f.maximumFractionDigits = fractionDigits ?? 0; - return Padding( - padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - transform: const GradientRotation(0.6), - stops: [ - 0.0, - higherIsBetter - ? greenSide > redSide - ? 0.6 - : 0 - : greenSide < redSide - ? 0.6 - : 0 - ], - ) - ), - child: Text( - (prefix ?? "") + f.format(greenSide) + (postfix ?? ""), - style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 1.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 2.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - textAlign: TextAlign.start, - ), - )), - Column( - children: [ - Text( - label, - style: const TextStyle(fontSize: 22), - textAlign: TextAlign.center, - ), - Text( - verdict(greenSide, redSide, - fractionDigits != null ? fractionDigits! + 2 : 0), - style: verdictStyle, - textAlign: TextAlign.center, - ) - ], - ), - Expanded( - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.red, Colors.transparent], - begin: Alignment.centerRight, - end: Alignment.centerLeft, - transform: const GradientRotation(-0.6), - stops: [ - 0.0, - higherIsBetter - ? redSide > greenSide - ? 0.6 - : 0 - : redSide < greenSide - ? 0.6 - : 0 - ], - )), - child: Text( - (prefix ?? "") + f.format(redSide) + (postfix ?? ""), - style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - textAlign: TextAlign.end, - ), - )), - ], - ), - ); - } -} - -class CompareBoolThingy extends StatelessWidget { - final bool greenSide; - final bool redSide; - final String label; - final bool trueIsBetter; - const CompareBoolThingy( - {super.key, - required this.greenSide, - required this.redSide, - required this.label, - required this.trueIsBetter}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), - child: Row(children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - stops: [ - 0.0, - trueIsBetter - ? greenSide - ? 0.6 - : 0 - : !greenSide - ? 0.6 - : 0 - ], - )), - child: Text( - greenSide ? t.yes : t.no, - style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - textAlign: TextAlign.start, - ), - )), - Column( - children: [ - Text( - label, - style: const TextStyle(fontSize: 22), - textAlign: TextAlign.center, - ), - const Text("---", style: verdictStyle, textAlign: TextAlign.center) - ], - ), - Expanded( - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.red, Colors.transparent], - begin: Alignment.centerRight, - end: Alignment.centerLeft, - stops: [ - 0.0, - trueIsBetter - ? redSide - ? 0.6 - : 0 - : !redSide - ? 0.6 - : 0 - ], - )), - child: Text( - redSide ? t.yes : t.no, - style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - textAlign: TextAlign.end, - ), - )), - ]), - ); - } -} - -class CompareDurationThingy extends StatelessWidget { - final Duration greenSide; - final Duration redSide; - final String label; - final bool higherIsBetter; - const CompareDurationThingy( - {super.key, - required this.greenSide, - required this.redSide, - required this.label, - required this.higherIsBetter}); - - Duration verdict(Duration greenSide, Duration redSide) { - return greenSide - redSide; - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded(child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - transform: const GradientRotation(0.6), - stops: [ - 0.0, - higherIsBetter - ? greenSide > redSide - ? 0.6 - : 0 - : greenSide < redSide - ? 0.6 - : 0 - ], - ) - ), - child: Text(get40lTime(greenSide.inMicroseconds), style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), textAlign: TextAlign.start) - )), - Column( - children: [ - Text( - label, - style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - textAlign: TextAlign.center, - ), - Text( - verdict(greenSide, redSide).toString(), style: verdictStyle, textAlign: TextAlign.center) - ], - ), - Expanded(child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.red, Colors.transparent], - begin: Alignment.centerRight, - end: Alignment.centerLeft, - transform: const GradientRotation(-0.6), - stops: [ - 0.0, - higherIsBetter - ? redSide > greenSide - ? 0.6 - : 0 - : redSide < greenSide - ? 0.6 - : 0 - ], - )), - child: Text(get40lTime(redSide.inMicroseconds), style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), textAlign: TextAlign.end) - )), - ], - ), - ); - } -} - -class CompareRegTimeThingy extends StatelessWidget { - final DateTime? greenSide; - final DateTime? redSide; - final String label; - final int? fractionDigits; - const CompareRegTimeThingy( - {super.key, - required this.greenSide, - required this.redSide, - required this.label, - this.fractionDigits}); - - String verdict(DateTime? greenSide, DateTime? redSide) { - var f = NumberFormat("#,### ${t.daysLater};#,### ${t.dayseBefore}"); - String result = "---"; - if (greenSide != null && redSide != null) { - result = f.format(greenSide.difference(redSide).inDays); - } - return result; - } - - @override - Widget build(BuildContext context) { - DateFormat f = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode); - return Padding( - padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.green, Colors.transparent], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - stops: [ - 0.0, - greenSide == null - ? 0.6 - : redSide != null && greenSide!.isBefore(redSide!) - ? 0.6 - : 0 - ], - )), - child: Text( - greenSide != null ? f.format(greenSide!) : t.fromBeginning, - style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - textAlign: TextAlign.start, - ), - )), - Column( - children: [ - Text( - label, - style: const TextStyle(fontSize: 22), - textAlign: TextAlign.center, - ), - Text(verdict(greenSide, redSide), style: verdictStyle, textAlign: TextAlign.center) - ], - ), - Expanded( - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [Colors.red, Colors.transparent], - begin: Alignment.centerRight, - end: Alignment.centerLeft, - stops: [ - 0.0, - redSide == null - ? 0.6 - : greenSide != null && redSide!.isBefore(greenSide!) - ? 0.6 - : 0 - ], - )), - child: Text( - redSide != null ? f.format(redSide!) : t.fromBeginning, - style: const TextStyle( - fontSize: 22, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], - ), - textAlign: TextAlign.end, - ), - )), - ], - ), - ); - } -} diff --git a/lib/views/customization_view.dart b/lib/views/customization_view.dart deleted file mode 100644 index f518c8a..0000000 --- a/lib/views/customization_view.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:tetra_stats/views/settings_view.dart' show subtitleStyle; -import 'package:tetra_stats/main.dart' show MyAppState, prefs; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:window_manager/window_manager.dart'; - -late String oldWindowTitle; -Color pickerColor = Colors.cyanAccent; -Color currentColor = Colors.cyanAccent; - -class CustomizationView extends StatefulWidget { - const CustomizationView({super.key}); - - @override - State createState() => CustomizationState(); -} - -class CustomizationState extends State { - late bool oskKagariGimmick; - late bool sheetbotRadarGraphs; - late int ratingMode; - late int timestampMode; - - void changeColor(Color color) { - setState(() => pickerColor = color); - } - - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) { - windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.settings}"); - } - _getPreferences(); - super.initState(); - } - - @override - void dispose() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - void _getPreferences() { - if (prefs.getBool("oskKagariGimmick") != null) { - oskKagariGimmick = prefs.getBool("oskKagariGimmick")!; - } else { - oskKagariGimmick = true; - } - if (prefs.getBool("sheetbotRadarGraphs") != null) { - sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")!; - } else { - sheetbotRadarGraphs = false; - } - if (prefs.getInt("ratingMode") != null) { - ratingMode = prefs.getInt("ratingMode")!; - } else { - ratingMode = 0; - } - if (prefs.getInt("timestampMode") != null) { - timestampMode = prefs.getInt("timestampMode")!; - } else { - timestampMode = 0; - } - } - - ThemeData getTheme(BuildContext context, Color color){ - return Theme.of(context).copyWith(colorScheme: ColorScheme.dark(primary: color, secondary: Colors.white)); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - List>? locales = - >[]; - for (var v in AppLocale.values) { - locales.add(DropdownMenuItem( - value: v, child: Text(t.locales[v.languageTag]!))); - } - return Scaffold( - appBar: AppBar( - title: Text(t.customization), - ), - backgroundColor: Colors.black, - body: SafeArea( - child: ListView( - children: [ - ListTile( - title: Text(t.AccentColor), - subtitle: Text(t.AccentColorDescription, style: subtitleStyle), - trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25), - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: const Text('Pick an accent color'), - content: SingleChildScrollView( - child: ColorPicker( - pickerColor: pickerColor, - onColorChanged: changeColor, - ), - ), - actions: [ - ElevatedButton( - child: const Text('Set'), - onPressed: () { - setState(() { - context.findAncestorStateOfType()?.setAccentColor(pickerColor); - prefs.setInt("accentColor", pickerColor.value); - }); - Navigator.of(context).pop(); - }, - ), - ])); - } - ), - // const ListTile( - // title: Text("Stats Table in TL mathes list"), - // subtitle: Text("Not implemented"), - // ), - ListTile(title: Text(t.timestamps), - subtitle: Text(t.timestampsDescription, style: subtitleStyle), - trailing: DropdownButton( - value: timestampMode, - items: [ - DropdownMenuItem(value: 0, child: Text(t.timestampsAbsoluteGMT)), - DropdownMenuItem(value: 1, child: Text(t.timestampsAbsoluteLocalTime)), - DropdownMenuItem(value: 2, child: Text(t.timestampsRelative)) - ], - onChanged: (dynamic value){ - prefs.setInt("timestampMode", value); - setState(() { - timestampMode = value; - }); - }, - ), - ), - ListTile(title: Text(t.rating), - subtitle: Text(t.ratingDescription, style: subtitleStyle), - trailing: DropdownButton( - value: ratingMode, - items: [ - const DropdownMenuItem(value: 0, child: Text("TR")), - const DropdownMenuItem(value: 1, child: Text("Glicko")), - DropdownMenuItem(value: 2, child: Text(t.ratingLBposition)) - ], - onChanged: (dynamic value){ - prefs.setInt("ratingMode", value); - setState(() { - ratingMode = value; - }); - }, - ), - ), - ListTile(title: Text(t.sheetbotGraphs), - subtitle: Text(t.sheetbotGraphsDescription, style: subtitleStyle), - trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){ - prefs.setBool("sheetbotRadarGraphs", value); - setState(() { - sheetbotRadarGraphs = value; - }); - }),), - ListTile(title: Text(t.oskKagari), - subtitle: Text(t.oskKagariDescription, style: subtitleStyle), - trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){ - prefs.setBool("oskKagariGimmick", value); - setState(() { - oskKagariGimmick = value; - }); - }),) - ], - )), - ); - } -} diff --git a/lib/views/destination_calculator.dart b/lib/views/destination_calculator.dart index c5c98cd..fce3747 100644 --- a/lib/views/destination_calculator.dart +++ b/lib/views/destination_calculator.dart @@ -9,7 +9,9 @@ import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; +import 'package:tetra_stats/widgets/info_thingy.dart'; +import 'package:tetra_stats/widgets/nerd_stats_thingy.dart'; class DestinationCalculator extends StatefulWidget{ final BoxConstraints constraints; @@ -243,7 +245,7 @@ class _DestinationCalculatorState extends State { child: NerdStatsThingy(nerdStats: nerdStats!) ), if (playstyle != null) Card( - child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!) + child: Graphs(apm!, pps!, vs!, nerdStats!, playstyle!) ), if (nerdStats == null) InfoThingy("Enter values and press \"Calc\" to see Nerd Stats for them") ], diff --git a/lib/views/destination_cutoffs.dart b/lib/views/destination_cutoffs.dart index 8321033..7feb500 100644 --- a/lib/views/destination_cutoffs.dart +++ b/lib/views/destination_cutoffs.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; @@ -9,8 +8,8 @@ import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/rank_view.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:vector_math/vector_math_64.dart' hide Colors; diff --git a/lib/views/destination_graphs.dart b/lib/views/destination_graphs.dart index 64221d6..7bb5ad7 100644 --- a/lib/views/destination_graphs.dart +++ b/lib/views/destination_graphs.dart @@ -10,7 +10,9 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/widgets/error_thingy.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; class DestinationGraphs extends StatefulWidget{ @@ -207,7 +209,6 @@ class _DestinationGraphsState extends State { if (snapshot.data!.isEmpty || !snapshot.data!.containsKey(_season)) return ErrorThingy(eText: "Not enough data"); List<_HistoryChartSpot> selectedGraph = snapshot.data![_season]![_Ychart]!; yAxisTitle = chartsShortTitles[_Ychart]!; - // TODO: this graph can Krash return SfCartesianChart( tooltipBehavior: _historyTooltipBehavior, zoomPanBehavior: _zoomPanBehavior, diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 47cbd7b..6339792 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -20,12 +19,25 @@ import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/singleplayer_record_view.dart'; +import 'package:tetra_stats/widgets/badges_thingy.dart'; +import 'package:tetra_stats/widgets/distinguishment_thingy.dart'; +import 'package:tetra_stats/widgets/error_thingy.dart'; +import 'package:tetra_stats/widgets/fake_distinguishment_thingy.dart'; import 'package:tetra_stats/widgets/finesse_thingy.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; import 'package:tetra_stats/widgets/lineclears_thingy.dart'; +import 'package:tetra_stats/widgets/nerd_stats_thingy.dart'; +import 'package:tetra_stats/widgets/news_thingy.dart'; import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; +import 'package:tetra_stats/widgets/tl_records_thingy.dart'; +import 'package:tetra_stats/widgets/tl_thingy.dart'; +import 'package:tetra_stats/widgets/user_thingy.dart'; +import 'package:tetra_stats/widgets/zenith_thingy.dart'; class DestinationHome extends StatefulWidget{ final String searchFor; @@ -423,8 +435,8 @@ class _DestinationHomeState extends State with SingleTickerProv ], ), ), - if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats, averages: averages), - if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) + if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats, averages: averages, lbPos: lbPos), + if (data.nerdStats != null) Graphs(data.apm!, data.pps!, data.vs!, data.nerdStats!, data.playstyle!) ], ); } @@ -713,7 +725,7 @@ class _DestinationHomeState extends State with SingleTickerProv ), ), if (record != null) NerdStatsThingy(nerdStats: record.aggregateStats.nerdStats), - if (record != null) GraphsThingy(nerdStats: record.aggregateStats.nerdStats, playstyle: record.aggregateStats.playstyle, apm: record.aggregateStats.apm, pps: record.aggregateStats.pps, vs: record.aggregateStats.vs) + if (record != null) Graphs(record.aggregateStats.apm, record.aggregateStats.pps, record.aggregateStats.vs, record.aggregateStats.nerdStats, record.aggregateStats.playstyle) ], ); } @@ -1016,7 +1028,7 @@ class _DestinationHomeState extends State with SingleTickerProv width: 450, child: Column( children: [ - NewUserThingy(player: snapshot.data!.player!, initIsTracking: snapshot.data!.isTracked, showStateTimestamp: false, setState: setState), + UserThingy(player: snapshot.data!.player!, initIsTracking: snapshot.data!.isTracked, showStateTimestamp: false, setState: setState), if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), diff --git a/lib/views/destination_info.dart b/lib/views/destination_info.dart new file mode 100644 index 0000000..75d89df --- /dev/null +++ b/lib/views/destination_info.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/views/sprint_and_blitz_averages.dart'; + +class DestinationInfo extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationInfo({super.key, required this.constraints}); + + @override + State createState() => _DestinationInfo(); +} + +class InfoCard extends StatelessWidget { + final double height; + final String assetLink; + final String? assetLinkOnFocus; + final String title; + final String description; + final void Function() onPressed; + + const InfoCard({required this.height, required this.assetLink, required this.title, required this.description, this.assetLinkOnFocus, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.hardEdge, + child: SizedBox( + width: 450, + height: height, + child: Column( + children: [ + Image.asset(assetLink, fit: BoxFit.cover, height: 300.0), + TextButton(child: Text(title, style: Theme.of(context).textTheme.titleLarge!.copyWith(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), textAlign: TextAlign.center), onPressed: onPressed), + Padding( + padding: const EdgeInsets.all(12.0), + child: Text(description), + ), + Spacer() + ], + ), + ), + ); + } + +} + +class _DestinationInfo extends State { + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Center(child: Text("Information Center", style: Theme.of(context).textTheme.titleLarge)), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + InfoCard( + height: widget.constraints.maxHeight - 77, + assetLink: "res/images/info card 1 focus.png", + title: "40 Lines & Blitz Averages", + description: "Since calculating 40 Lines & Blitz averages is tedious process, it gets updated only once in a while. Click on the title of this card to see the full 40 Lines & Blitz averages table\n\n${t.sprintAndBlitsRelevance(date: DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(sprintAndBlitzRelevance))}", + onPressed: (){ + Navigator.push(context, MaterialPageRoute( + builder: (context) => SprintAndBlitzView(), + )); + } + ), + InfoCard( + height: widget.constraints.maxHeight - 77, + assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", + title: "Shizuru!", + description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru ", + onPressed: (){} + ), + InfoCard( + height: widget.constraints.maxHeight - 77, + assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", + title: "About Tetra Stats", + description: "Developed by dan63\n", + onPressed: (){}, + ), + Card() + ], + ), + ) + ], + ); + } +} diff --git a/lib/views/destination_leaderboards.dart b/lib/views/destination_leaderboards.dart index e95b987..d354ba8 100644 --- a/lib/views/destination_leaderboards.dart +++ b/lib/views/destination_leaderboards.dart @@ -7,8 +7,8 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/user_view.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; class DestinationLeaderboards extends StatefulWidget{ final BoxConstraints constraints; diff --git a/lib/views/destination_saved_data.dart b/lib/views/destination_saved_data.dart index 6d09395..a1571ac 100644 --- a/lib/views/destination_saved_data.dart +++ b/lib/views/destination_saved_data.dart @@ -6,8 +6,10 @@ import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; import 'package:tetra_stats/views/state_view.dart'; +import 'package:tetra_stats/widgets/alpha_league_entry_thingy.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; +import 'package:tetra_stats/widgets/info_thingy.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; class DestinationSavedData extends StatefulWidget{ diff --git a/lib/views/destination_settings.dart b/lib/views/destination_settings.dart index 30dfc28..935bfac 100644 --- a/lib/views/destination_settings.dart +++ b/lib/views/destination_settings.dart @@ -13,7 +13,7 @@ import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/filesizes_converter.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; class DestinationSettings extends StatefulWidget{ final BoxConstraints constraints; @@ -50,7 +50,6 @@ class _DestinationSettings extends State with SingleTickerP late bool showPositions; late bool showAverages; late bool updateInBG; - final TextEditingController _playertext = TextEditingController(); late AnimationController _defaultNicknameAnimController; late Animation _goodDefaultNicknameAnim; late Animation _badDefaultNicknameAnim; diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 4ef9c32..06a4807 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,1629 +1,314 @@ -// ignore_for_file: type_literal_in_constant_pattern, use_build_context_synchronously - -import 'dart:async'; -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:http/http.dart'; -import 'package:intl/intl.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/services.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; -import 'package:tetra_stats/data_objects/beta_record.dart'; -import 'package:tetra_stats/data_objects/distinguishment.dart'; -import 'package:tetra_stats/data_objects/news.dart'; -import 'package:tetra_stats/data_objects/news_entry.dart'; -import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; -import 'package:tetra_stats/data_objects/record_extras.dart'; -import 'package:tetra_stats/data_objects/record_single.dart'; -import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; -import 'package:tetra_stats/data_objects/summaries.dart'; -import 'package:tetra_stats/data_objects/tetra_league.dart'; -import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; -import 'package:tetra_stats/data_objects/tetra_league_alpha_stream.dart'; -import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; -import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; -import 'package:tetra_stats/data_objects/tetrio_constants.dart'; -import 'package:tetra_stats/data_objects/tetrio_player.dart'; -import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; -import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; -import 'package:tetra_stats/data_objects/tetrio_zen.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart' show prefs, teto; -import 'package:tetra_stats/services/crud_exceptions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/utils/open_in_browser.dart'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/singleplayer_record_view.dart'; -import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; -import 'package:tetra_stats/views/zenith_record_view.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; -import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; -import 'package:tetra_stats/widgets/recent_sp_games.dart'; -import 'package:tetra_stats/widgets/search_box.dart'; -import 'package:tetra_stats/widgets/singleplayer_record.dart'; -import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/widgets/tl_thingy.dart'; -import 'package:tetra_stats/widgets/user_thingy.dart'; -import 'package:tetra_stats/widgets/zenith_thingy.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:go_router/go_router.dart'; - -int _chartsIndex = 0; -int _season = currentSeason-1; -bool _gamesPlayedInsteadOfDateAndTime = false; -late ZoomPanBehavior _zoomPanBehavior; -bool _smooth = false; -List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; -late ScrollController _scrollController; - -class MainView extends StatefulWidget { - final String? player; - /// The very first view, that user see when he launch this programm. - /// By default it loads my or defined in preferences user stats, but - /// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu. - const MainView({super.key, this.player}); - - @override - State createState() => _MainState(); -} - -Future copyToClipboard(String text) async { - await Clipboard.setData(ClipboardData(text: text)); -} - -class _MainState extends State with TickerProviderStateMixin { - Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up - TetrioPlayersLeaderboard? everyone; - PlayerLeaderboardPosition? meAmongEveryone; - TetraLeague? rankAverages; - double? thatRankCutoff; - double? nextRankCutoff; - double? thatRankGlickoCutoff; - double? nextRankGlickoCutoff; - String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for - String _titleNickname = ""; - /// Each dropdown menu item contains list of dots for the graph - /// chartsData[season-1][chart] - List>>> chartsData = []; - //var tableData = []; - final bodyGlobalKey = GlobalKey(); - bool _showSearchBar = false; - Timer backgroundUpdate = Timer(const Duration(days: 365), (){}); - bool _TLHistoryWasFetched = false; - late TabController _tabController; - late TabController _wideScreenTabController; - bool zenithEX = false; - - String get title => "Tetra Stats: $_titleNickname"; - - @override - void initState() { - initDB(); - _scrollController = ScrollController(); - _tabController = TabController(length: 9, vsync: this); - _wideScreenTabController = TabController(length: 5, vsync: this); - _zoomPanBehavior = ZoomPanBehavior( - enablePinching: true, - enableSelectionZooming: true, - enableMouseWheelZooming : true, - enablePanning: true, - ); - // We need to show something - if (widget.player != null){ // if we have user input, - changePlayer(widget.player!); // it's gonna be user input - }else{ - _getPreferences() // otherwise, checking for preferences - .then((value) => changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc")); // no preferences - loading me - } - super.initState(); - } - - @override - void dispose() { - _tabController.dispose(); - _scrollController.dispose(); - super.dispose(); - } - - Future _getPreferences() async { - prefs = await SharedPreferences.getInstance(); - } - - /// That function initiate search of data about [player]. If [fetchHistory] is true, - /// also attempting to retrieve players history. Can trow an Exception if fails - void changePlayer(String player, {bool fetchHistory = false, bool fetchTLmatches = false}) { - setState(() { - _searchFor = player; - me = fetch(_searchFor, fetchHistory: fetchHistory, fetchTLmatches: fetchTLmatches); - }); - } - - void initDB() async{ - await teto.open(); - } - - /// Retrieves data from 3 different Tetra Channel API endpoints + 1 endpoint from p1nkl0bst3r's API - /// using [nickOrID] of player. - /// - /// If [fetchHistory] is true, also retrieves players history from p1nkl0bst3r's API. If [fetchTLmatches] is true, also retrieves players old Tetra League - /// matches from p1nkl0bst3r's API. Returns list which contains [TetrioPlayer], his records, previous states, TL matches, previous TL state, - /// if player tracked (bool), news entries and topTR. - /// - /// If at least one request to Tetra Channel API fails, whole function will throw an exception. - Future fetch(String nickOrID, {bool fetchHistory = false, bool fetchTLmatches = false}) async { - TetrioPlayer me; - _TLHistoryWasFetched = false; - backgroundUpdate.cancel(); - - // If user trying to search with discord id - if (nickOrID.startsWith("ds:")){ - me = await teto.fetchPlayer(nickOrID.substring(3), isItDiscordID: true); // we trying to get him with that - }else{ - me = await teto.fetchPlayer(nickOrID); // Otherwise it's probably a user id or username - } - _searchFor = me.userId; // gonna use user id for next requests - - // Change view title and window title if avaliable - setState((){_titleNickname = me.username;}); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) await windowManager.setTitle(title); - - // Requesting Tetra League (alpha), records, news and top TR of player - List requests; - Summaries summaries = await teto.fetchSummaries(_searchFor); - late TetraLeagueBetaStream tlStream; - late News news; - // late SingleplayerStream recentSprint; - // late SingleplayerStream recentBlitz; - // late SingleplayerStream sprint; - // late SingleplayerStream blitz; - late SingleplayerStream recentZenith; - late SingleplayerStream recentZenithEX; - late TetrioPlayerFromLeaderboard? topOne; - // late TopTr? topTR; - requests = await Future.wait([ - teto.fetchSummaries(_searchFor), - teto.fetchTLStream(_searchFor), - teto.fetchNews(_searchFor), - teto.fetchStream(_searchFor, "zenith/recent"), - teto.fetchStream(_searchFor, "zenithex/recent"), - teto.fetchCutoffsBeanserver(), - (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), - ]); - //prefs.getBool("showPositions") != true ? teto.fetchCutoffsBeanserver() : Future.delayed(Duration.zero, ()=>>[]), - - //(summaries.league.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR - summaries = requests[0] as Summaries; - tlStream = requests[1] as TetraLeagueBetaStream; - // records = requests[1] as UserRecords; - news = requests[2] as News; - recentZenith = requests[3] as SingleplayerStream; - recentZenithEX = requests[4] as SingleplayerStream; - // recent = requests[3] as SingleplayerStream; - // sprint = requests[4] as SingleplayerStream; - // blitz = requests[5] as SingleplayerStream; - topOne = requests[6] as TetrioPlayerFromLeaderboard?; - // topTR = requests[8] as TopTr?; // No TR - no Top TR - - meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); - if (prefs.getBool("showPositions") == true){ - // Get tetra League leaderboard - everyone = teto.getCachedLeaderboard(); - everyone ??= await teto.fetchTLLeaderboard(); - if (meAmongEveryone == null && everyone!.leaderboard.isNotEmpty){ - meAmongEveryone = await compute(everyone!.getLeaderboardPosition, {me.userId: summaries.league}); - if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); - } - } - Map? cutoffs = (requests[5] as Cutoffs?)?.tr; - Map? cutoffsGlicko = (requests[5] as Cutoffs?)?.glicko; - - if (summaries.league.gamesPlayed > 9) { - thatRankCutoff = cutoffs?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank]; - thatRankGlickoCutoff = cutoffsGlicko?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank]; - nextRankCutoff = (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? topOne?.tr??25000 : cutoffs?[ranks.elementAtOrNull(ranks.indexOf(summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank)+1)]; - nextRankGlickoCutoff = (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank)+1)]; - } - - if (everyone != null && summaries.league.gamesPlayed > 9) rankAverages = everyone?.averages[summaries.league.percentileRank]?[0]; - - // Making list of Tetra League matches - bool isTracking = await teto.isPlayerTracking(me.userId); - List> states = await Future.wait>([ - teto.getStates(me.userId, season: 1), teto.getStates(me.userId, season: 2), - ]); - List storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches - if (isTracking){ // if tracked - save data to local DB - await teto.storeState(summaries.league); - //await teto.saveTLMatchesFromStream(tlStream); - } - TetraLeagueAlphaStream? oldMatches; - // building list of TL matches - if(fetchTLmatches) { - try{ - oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor); - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.records.length)))); - }on TetrioHistoryNotExist{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches))); - }on P1nkl0bst3rForbidden { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); - }on P1nkl0bst3rInternalProblem { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); - }on P1nkl0bst3rTooManyRequests{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); - }finally{ - _TLHistoryWasFetched = true; - } - } - if (storedRecords.isNotEmpty) { - _TLHistoryWasFetched = true; - tlStream.addFromAlphaStream(storedRecords); - } - - // tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list - // if(a.ts.isBefore(b.ts)) return 1; - // if(a.ts.isAtSameMomentAs(b.ts)) return 0; - // if(a.ts.isAfter(b.ts)) return -1; - // return 0; - // }); - - // Handling history - if(fetchHistory){ - try{ - var history = await teto.fetchAndsaveTLHistory(_searchFor); // Retrieve if needed - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); - }on TetrioHistoryNotExist{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noHistorySaved))); - }on P1nkl0bst3rForbidden { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); - }on P1nkl0bst3rInternalProblem { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); - }on P1nkl0bst3rTooManyRequests{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); - } - } - - //states.addAll(await teto.getPlayer(me.userId)); - // for (var element in states) { // For graphs I need only unique entries - // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); - // if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); - // } - if (states[1].length >= 2 || states[0].length >= 2){ - chartsData = [for (List s in states) >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in s) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), - ]]; - }else{ - chartsData = []; - } - - if (prefs.getBool("updateInBG") == true) { - backgroundUpdate = Timer(me.cachedUntil!.difference(DateTime.now()), () { - changePlayer(me.userId); - }); - } - return [me, summaries, news, tlStream, recentZenith, recentZenithEX, states[currentSeason-1]]; - //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; - } - - /// Triggers widgets rebuild - void _justUpdate() { - setState(() {}); - } - - void toggleZenith(){ - setState(() {zenithEX = !zenithEX;}); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - bool bigScreen = MediaQuery.of(context).size.width > 1024; - return Scaffold( - drawer: widget.player == null ? NavDrawer(changePlayer) : null, // Side menu hidden if player provided - drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.2, // 20% of left side of the screen used of Drawer gesture - appBar: AppBar( - title: _showSearchBar ? SearchBox(onSubmit: changePlayer, bigScreen: MediaQuery.of(context).size.width > 768) : Text(title, style: const TextStyle(shadows: textShadow)), - backgroundColor: Colors.black, - actions: widget.player == null ? [ // search bar and PopupMenuButton hidden if player provided - _showSearchBar - ? IconButton( - onPressed: () { - setState(() { - _showSearchBar = false; - }); - }, - icon: const Icon(Icons.clear), - tooltip: t.closeSearch, - ) - : IconButton( - onPressed: () { - setState(() { - _showSearchBar = true; - }); - }, - icon: const Icon(Icons.search), - tooltip: t.openSearch, - ), - PopupMenuButton( - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - value: "refresh", - child: Text(t.refresh), - ), - PopupMenuItem( - value: "history", - child: Text(t.fetchAndsaveTLHistory), - ), - PopupMenuItem( - value: "TLmatches", - child: Text(t.fetchAndSaveOldTLmatches), - ), - PopupMenuItem( - value: "/states", - child: Text(t.showStoredData), - ), - PopupMenuItem( - value: "/calc", - child: Text(t.statsCalc), - ), - PopupMenuItem( - value: "/settings", - child: Text(t.settings), - ), - ], - onSelected: (value) { - switch (value){ - case "refresh": - changePlayer(_searchFor); - break; - case "history": - changePlayer(_searchFor, fetchHistory: true); - break; - case "TLmatches": - changePlayer(_searchFor, fetchTLmatches: true); - break; - default: - context.go(value); - } - }, - ), - ] : null, - ), - body: SafeArea( - child: FutureBuilder>( - future: me, - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator(color: Colors.white)); - case ConnectionState.done: - if (snapshot.hasData) { - return RefreshIndicator( - onRefresh: () { - return Future(() => changePlayer(snapshot.data![0].userId)); - }, - notificationPredicate: (notification) { - // with NestedScrollView local(depth == 2) OverscrollNotification are not sent - if (!kIsWeb && (notification is OverscrollNotification || Platform.isIOS)) { - return notification.depth == 2; - } - return notification.depth == 0; - }, - child: NestedScrollView( - scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false, physics: const AlwaysScrollableScrollPhysics()), - controller: _scrollController, - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: UserThingy( - player: snapshot.data![0], - showStateTimestamp: false, - setState: _justUpdate, - )), - SliverToBoxAdapter( - child: TabBar( - controller: bigScreen ? _wideScreenTabController : _tabController, - padding: const EdgeInsets.all(0.0), - isScrollable: true, - tabs: bigScreen ? [ - Tab(text: t.tetraLeague,), - Tab(text: t.history), - Tab(text: t.quickPlay), - Tab(text: "${t.sprint} & ${t.blitz}"), - Tab(text: t.other), - ] : [ - Tab(text: t.tetraLeague), - Tab(text: t.tlRecords), - Tab(text: t.history), - Tab(text: t.quickPlay), - Tab(text: "${t.quickPlay} ${t.recent}"), - Tab(text: t.sprint), - Tab(text: t.blitz), - Tab(text: t.recentRuns), - Tab(text: t.other), - ], - ), - ), - ]; - }, - body: TabBarView( - controller: bigScreen ? _wideScreenTabController : _tabController, - children: bigScreen ? [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: MediaQuery.of(context).size.width-450, - constraints: const BoxConstraints(maxWidth: 1024), - child: TLThingy( - tl: snapshot.data![1].league, - userID: snapshot.data![0].userId, - states: snapshot.data![6], - //topTR: snapshot.data![7]?.tr, - //lastMatchPlayed: snapshot.data![11], - bot: snapshot.data![0].role == "bot", - guest: snapshot.data![0].role == "anon", - thatRankCutoff: thatRankCutoff, - thatRankCutoffGlicko: thatRankGlickoCutoff, - thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, - nextRankCutoff: nextRankCutoff, - nextRankCutoffGlicko: nextRankGlickoCutoff, - nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, - averages: rankAverages, - lbPositions: meAmongEveryone - ), - ), - SizedBox( - width: 450, - child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true) - ), - ],), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: MediaQuery.of(context).size.width-450, - constraints: const BoxConstraints(maxWidth: 1024), - child: SingleChildScrollView(child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)) - ), - SizedBox( - width: 450.0, - child: _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true), - ) - ], - ), - _TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, recent: SingleplayerStream(userId: "userId", records: [], type: "recent"), sprintStream: SingleplayerStream(userId: "userId", records: [], type: "40l"), blitzStream: SingleplayerStream(userId: "userId", records: [], type: "blitz")), - _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![2]) - ] : [ - TLThingy( - tl: snapshot.data![1].league, - userID: snapshot.data![0].userId, - states: const [], //snapshot.data![2], - //topTR: snapshot.data![7]?.tr, - //lastMatchPlayed: snapshot.data![11], - bot: snapshot.data![0].role == "bot", - guest: snapshot.data![0].role == "anon", - thatRankCutoff: thatRankCutoff, - thatRankCutoffGlicko: thatRankGlickoCutoff, - thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, - nextRankCutoff: nextRankCutoff, - nextRankCutoffGlicko: nextRankGlickoCutoff, - nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, - averages: rankAverages, - lbPositions: meAmongEveryone - ), - _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0), - SingleChildScrollView(child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)), - _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true), - SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")), - SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "Blitz")), - _RecentSingleplayersThingy(SingleplayerStream(userId: "userId", records: [], type: "recent")), - _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![2]) - ], - ), - ), - ); - } else if (snapshot.hasError) { - String errText = ""; - String? subText; - switch (snapshot.error.runtimeType){ - case TetrioPlayerNotExist: - errText = t.errors.noSuchUser; - subText = t.errors.noSuchUserSub; - break; - case TetrioDiscordNotExist: - errText = t.errors.discordNotAssigned; - subText = t.errors.discordNotAssignedSub; - case ConnectionIssue: - var err = snapshot.error as ConnectionIssue; - errText = t.errors.connection(code: err.code, message: err.message); - break; - case TetrioForbidden: - errText = t.errors.forbidden; - subText = t.errors.forbiddenSub(nickname: 'osk'); - break; - case TetrioTooManyRequests: - errText = t.errors.tooManyRequests; - subText = t.errors.tooManyRequestsSub; - break; - case TetrioOskwareBridgeProblem: - errText = t.errors.oskwareBridge; - subText = t.errors.oskwareBridgeSub; - break; - case TetrioInternalProblem: - errText = kIsWeb ? t.errors.internalWebVersion : t.errors.internal; - subText = kIsWeb ? t.errors.internalWebVersionSub : t.errors.internalSub; - break; - case ClientException: - errText = t.errors.clientException; - break; - default: - errText = snapshot.error.toString(); - subText = snapshot.stackTrace.toString(); - } - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - if (subText != null) Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(subText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), - ), - ], - ) - ); - } - break; - } - return const Center(child: Text('default case of FutureBuilder', style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center)); - }, - ), - ), - ); - } -} - -class NavDrawer extends StatefulWidget { - final Function changePlayer; - - /// Thing, that shows from the left side of the view. - /// Requires [changePlayer] function in order to be able to change players on main view - const NavDrawer(this.changePlayer, {super.key}); - - @override - State createState() => _NavDrawerState(); -} - -class _NavDrawerState extends State { - String homePlayerNickname = "Checking..."; - @override - void initState() { - super.initState(); - _setHomePlayerNickname(prefs.getString("player")); - } - - @override - void dispose() { - super.dispose(); - } - - /// Sets username for home button in NavDrawer. - /// Accepts [id] or username. If it's not provided, sets my nickname. - /// Otherwise, sets username or [id] if failed to find - Future _setHomePlayerNickname(String? id) async { - if (id != null) { - try { - homePlayerNickname = await teto.getNicknameByID(id); - } on TetrioPlayerNotExist { - homePlayerNickname = id; - } - } else { - homePlayerNickname = "dan63047"; - } - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Drawer( - child: StreamBuilder( - stream: teto.allPlayers, - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - final allPlayers = (snapshot.data != null) - ? snapshot.data as Map - : {}; - allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted - List keys = allPlayers.keys.toList(); - return NestedScrollView( - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: DrawerHeader( - child: Text(t.playersYouTrack, style: const TextStyle(color: Colors.white, fontSize: 25), - ))), - SliverToBoxAdapter( - child: ListTile( // Home button - leading: const Icon(Icons.home), - title: Text(homePlayerNickname), - onTap: () { - widget.changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // changes player on main view to the one from preferences - Navigator.of(context).pop(); // and then NavDrawer closes itself. - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( // Leaderboard button - leading: const Icon(Icons.leaderboard), - title: Text(t.tlLeaderboard), - onTap: () { - context.go("/leaderboard"); - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( // Rank averages button - leading: const Icon(Icons.compress), - title: Text(t.rankAveragesViewTitle), - onTap: () { - context.go("/LBvalues"); - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( // Rank averages button - leading: const Icon(Icons.bar_chart), - title: Text(t.sprintAndBlitsViewTitle), - onTap: () { - context.go("/sprintAndBlitzAverages"); - }, - ), - ), - const SliverToBoxAdapter(child: Divider()) - ]; - }, - body: ListView.builder( // Builds list of tracked players. - itemCount: allPlayers.length, - itemBuilder: (context, index) { - var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. - return ListTile( - title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states - onTap: () { - widget.changePlayer(keys[i]); // changes to chosen player - Navigator.of(context).pop(); // and closes itself. - }, - ); - })); - case ConnectionState.done: - return const Center(child: Text('done case of StreamBuilder')); // what if that thing breaks? - } - }, - ), - ); - } -} - -class _TLRecords extends StatelessWidget { - final String userID; - final Function changePlayer; - final List data; - final bool wasActiveInTL; - final bool oldMathcesHere; - final bool separateScrollController; - - /// Widget, that displays Tetra League records. - /// Accepts list of TL records ([data]) and [userID] of player from the view - const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere, this.separateScrollController = false}); - - @override - Widget build(BuildContext context) { - if (data.isEmpty) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) - ], - )); - } - bool bigScreen = MediaQuery.of(context).size.width >= 768; - int length = data.length; - return ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: separateScrollController ? ScrollController() : null, - itemCount: oldMathcesHere ? length : length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == length) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) - ], - )); - } - - var accentColor = data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins > data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins ? Colors.green : Colors.red; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - stops: const [0, 0.05], - colors: [accentColor, Colors.transparent] - ) - ), - child: ListTile( - leading: Text("${data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins} : ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins}", - style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), - title: Text("vs. ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).username}"), - subtitle: Text(timestamp(data[index].ts), style: const TextStyle(color: Colors.grey)), - trailing: TrailingStats( - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.apm, - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.pps, - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.vs, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.apm, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.pps, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.vs, - ), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), - ), - ); - }); - } -} - -class _ZenithRecords extends StatelessWidget { - final String userID; - final SingleplayerStream data; - final bool separateScrollController; - - /// Widget, that displays Quick Play records. - /// Accepts list of TL records ([data]) and [userID] of player from the view - const _ZenithRecords({required this.userID, required this.data, this.separateScrollController = false}); - - @override - Widget build(BuildContext context) { - if (data.records.isEmpty) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - ], - )); - } - bool bigScreen = MediaQuery.of(context).size.width >= 768; - int length = data.records.length; - return ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: separateScrollController ? ScrollController() : null, - itemCount: length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == length) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - ], - )); - } - const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100, fontSize: 13); - return Container( - child: ListTile( - leading: Text("QP", - style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), - title: Text("${f2.format(data.records[index].stats.zenith!.altitude)} m${(data.records[index].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (data.records[index].extras as ZenithExtras).mods.length)})" : ""}"), - subtitle: Text(timestamp(data.records[index].timestamp), style: const TextStyle(color: Colors.grey)), - trailing: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text("${f2.format(data.records[index].aggregateStats.apm)} APM, ${f2.format(data.records[index].aggregateStats.pps)} PPS", style: style, textAlign: TextAlign.right), - Text("${f2.format(data.records[index].stats.cps)} CSP (${f2.format(data.records[index].stats.zenith!.peakrank)} peak)", style: style, textAlign: TextAlign.right), - Text("${data.records[index].stats.kills} KO's, ${getMoreNormalTime(data.records[index].stats.finalTime)}", style: style, textAlign: TextAlign.right) - ], - ), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => ZenithRecordView(record: data.records[index]))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), - ), - ); - }); - } -} - -class _History extends StatelessWidget{ - final List>>> chartsData; - final String userID; - final Function update; - final Function changePlayer; - final bool wasActiveInTL; - - /// Widget, that can show history of some stat of the player on the graph. - /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); - - @override - Widget build(BuildContext context) { - if (chartsData.isEmpty) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) - ], - )); - } - bool bigScreen = MediaQuery.of(context).size.width > 768; - List<_HistoryChartSpot> selectedGraph = chartsData[_season][_chartsIndex].value!; - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], - value: _season, - onChanged: (value) { - _season = value!; - update(); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: const [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], - value: _gamesPlayedInsteadOfDateAndTime, - onChanged: (value) { - _gamesPlayedInsteadOfDateAndTime = value!; - update(); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: chartsData[_season], - value: chartsData[_season][_chartsIndex].value, - onChanged: (value) { - _chartsIndex = chartsData[_season].indexWhere((element) => element.value == value); - update(); - } - ), - ], - ), - if (selectedGraph.length > 300) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox(value: _smooth, - checkColor: Colors.black, - onChanged: ((value) { - _smooth = value!; - update(); - })), - Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - if(chartsData[_season][_chartsIndex].value!.length > 1) _HistoryChartThigy(data: selectedGraph, smooth: _smooth, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact()) - else if (chartsData[_season][_chartsIndex].value!.length <= 1) Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL && _season == 0) Text(t.errors.actionSuggestion), - if (wasActiveInTL && _season == 0) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) - ], - )) - ], - ), - ); - } -} - -class _HistoryChartSpot{ - final DateTime timestamp; - final int gamesPlayed; - final String rank; - final double stat; - const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat); -} - -class _HistoryChartThigy extends StatefulWidget{ - final List<_HistoryChartSpot> data; - final bool smooth; - final String yAxisTitle; - final bool bigScreen; - final double leftSpace; - final NumberFormat yFormat; - final NumberFormat? xFormat; - - /// Implements graph for the _History widget. Requires [data] which is a list of dots for the graph. [yAxisTitle] used to keep track of changes. - /// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets number format - /// for left titles - const _HistoryChartThigy({required this.data, required this.smooth, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat}); - - @override - State<_HistoryChartThigy> createState() => _HistoryChartThigyState(); -} - -class _HistoryChartThigyState extends State<_HistoryChartThigy> { - late String previousAxisTitle; - late bool previousGamesPlayedInsteadOfDateAndTime; - late TooltipBehavior _tooltipBehavior; - - - @override - void initState(){ - super.initState(); - _tooltipBehavior = TooltipBehavior( - color: Colors.black, - borderColor: Colors.white, - enable: true, - animationDuration: 0, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "${f4.format(data.stat)} ${widget.yAxisTitle}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), - ), - ), - Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) - ], - ), - ); - } - ); - previousAxisTitle = widget.yAxisTitle; - previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; - } - - @override - void dispose(){ - super.dispose(); -} - - @override - Widget build(BuildContext context) { - if ((previousAxisTitle != widget.yAxisTitle) || (previousGamesPlayedInsteadOfDateAndTime != _gamesPlayedInsteadOfDateAndTime)) { - previousAxisTitle = widget.yAxisTitle; - previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; - setState((){}); - } - EdgeInsets padding = widget.bigScreen ? const EdgeInsets.fromLTRB(40, 30, 40, 30) : const EdgeInsets.fromLTRB(0, 40, 16, 48); - return SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height - 104, - child: Padding( padding: padding, - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerSignal: (signal) { - if (signal is PointerScrollEvent) { - setState(() { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView - }); - } - }, - child: SfCartesianChart( - tooltipBehavior: _tooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, - ), - margin: const EdgeInsets.all(0), - series: [ - if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( - enableTooltip: true, - dataSource: widget.data, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (widget.data.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ) - else StepLineSeries<_HistoryChartSpot, DateTime>( - enableTooltip: true, - dataSource: widget.data, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (widget.data.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ), - ], - ), - ), - ) - ); - } -} - -class _TwoRecordsThingy extends StatelessWidget { - final RecordSingle? sprint; - final RecordSingle? blitz; - final SingleplayerStream recent; - final SingleplayerStream sprintStream; - final SingleplayerStream blitzStream; - final String? rank; - - const _TwoRecordsThingy({required this.sprint, required this.blitz, this.rank, required this.recent, required this.sprintStream, required this.blitzStream}); - - Color getColorOfRank(int rank){ - if (rank == 1) return Colors.yellowAccent; - if (rank == 2) return Colors.blueGrey; - if (rank == 3) return Colors.brown[400]!; - if (rank <= 9) return Colors.blueAccent; - if (rank <= 99) return Colors.greenAccent; - return Colors.grey; - } - - @override - Widget build(BuildContext context) { - late MapEntry closestAverageBlitz; - late bool blitzBetterThanClosestAverage; - bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null; - late MapEntry closestAverageSprint; - late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && sprint != null) ? sprint!.stats.finalTime < sprintAverages[rank]! : null; - if (sprint != null) { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.stats.finalTime).abs() < (b -sprint!.stats.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = sprint!.stats.finalTime < closestAverageSprint.value; - } - if (blitz != null){ - closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.stats.score).abs() < (b -blitz!.stats.score).abs() ? a : b)); - blitzBetterThanClosestAverage = blitz!.stats.score > closestAverageBlitz.value; - } - return SingleChildScrollView(child: Padding( - padding: const EdgeInsets.only(top: 20.0), - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - crossAxisAlignment: WrapCrossAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding(padding: const EdgeInsets.only(right: 8.0), - child: sprint != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: sprint != null ? get40lTime(sprint!.stats.finalTime.inMicroseconds) : "---", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey), - //children: [TextSpan(text: get40lTime(record!.stats.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ), - if (sprint != null) RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )), - TextSpan(text: "№${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank))), - const TextSpan(text: " • "), - TextSpan(text: timestamp(sprint!.timestamp)), - ] - ), - ), - ],), - ], - ), - if (sprint != null) Wrap( - //mainAxisSize: MainAxisSize.max, - alignment: WrapAlignment.spaceBetween, - spacing: 20, - children: [ - StatCellNum(playerStat: sprint!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: sprint!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: sprint!.stats.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - ], - ), - if (sprint != null) FinesseThingy(sprint?.stats.finesse, sprint?.stats.finessePercentage), - if (sprint != null) LineclearsThingy(sprint!.stats.clears, sprint!.stats.lines, sprint!.stats.holds, sprint!.stats.tSpins), - if (sprint != null) Text("${sprint!.stats.inputs} KP • ${f2.format(sprint!.stats.kps)} KPS"), - if (sprint != null) Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${sprint!.replayId}"));}, child: Text(t.openSPreplay)), - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${sprint!.replayId}"));}, child: Text(t.downloadSPreplay)), - ], - ), - if (sprintStream.records.length > 1) SizedBox( - width: 400, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (int i = 1; i < sprintStream.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))), - leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), - title: Text(get40lTime(sprintStream.records[i].stats.finalTime.inMicroseconds), - style: Theme.of(context).textTheme.displayLarge), - subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(sprintStream.records[i], sprintStream.records[i].gamemode) - ) - ], - ), - ) - ] - ), - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), - children: [ - TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.stats.score) : "---"), - //WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48)) - ] - ), - ), - if (blitz != null) RichText( - textAlign: TextAlign.end, - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )), - TextSpan(text: timestamp(blitz!.timestamp)), - const TextSpan(text: " • "), - TextSpan(text: "№${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank))), - ] - ), - ), - ],), - Padding(padding: const EdgeInsets.only(left: 8.0), - child: blitz != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96)), - ], - ), - if (blitz != null) Wrap( - //mainAxisSize: MainAxisSize.max, - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - StatCellNum(playerStat: blitz!.stats.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: blitz!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: blitz!.stats.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true) - ], - ), - if (blitz != null) FinesseThingy(blitz?.stats.finesse, blitz?.stats.finessePercentage), - if (blitz != null) LineclearsThingy(blitz!.stats.clears, blitz!.stats.lines, blitz!.stats.holds, blitz!.stats.tSpins), - if (blitz != null) Text("${blitz!.stats.piecesPlaced} P • ${blitz!.stats.inputs} KP • ${f2.format(blitz!.stats.kpp)} KPP • ${f2.format(blitz!.stats.kps)} KPS"), - if (blitz != null) Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${blitz!.replayId}"));}, child: Text(t.openSPreplay)), - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${blitz!.replayId}"));}, child: Text(t.downloadSPreplay)), - ], - ), - if (blitzStream.records.length > 1) SizedBox( - width: 400, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (int i = 1; i < blitzStream.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))), - leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), - title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].stats.score)} points", - style: Theme.of(context).textTheme.displayLarge), - subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(blitzStream.records[i], blitzStream.records[i].gamemode) - ) - ], - ), - ) - ], - ), - SizedBox( - width: 400, - child: RecentSingleplayerGames(recent: recent), - ) - ]), - )); - } -} - -class _RecentSingleplayersThingy extends StatelessWidget { - final SingleplayerStream recent; - - const _RecentSingleplayersThingy(this.recent); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: RecentSingleplayerGames(recent: recent, hideTitle: true) - ); - } -} - -class _OtherThingy extends StatelessWidget { - final TetrioZen? zen; - final String? bio; - final Distinguishment? distinguishment; - final News? newsletter; - - /// Widget, that shows players [distinguishment], [bio], [zen] and [newsletter] - const _OtherThingy({required this.zen, required this.bio, required this.distinguishment, this.newsletter}); - - /// Distinguishment title is not very predictable thing. - /// Receives [text], which is header and returns sets of widgets for RichText widget - List getDistinguishmentTitle(String? text) { - // TWC champions don't have header in their distinguishments - if (distinguishment?.type == "twc") return [const TextSpan(text: "TETR.IO World Champion", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.yellowAccent))]; - // In case if it missing for some other reason, return this - if (text == null) return [const TextSpan(text: "Header is missing", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.redAccent))]; - - // Handling placeholders for logos - var exploded = text.split(" "); // wtf PHP reference? - List result = []; - for (String shit in exploded){ - switch (shit) { // if %% thingy was found, insert svg of icon - case "%osk%": - result.add(WidgetSpan(child: Padding( - padding: const EdgeInsets.only(left: 8), - child: SvgPicture.asset("res/icons/osk.svg", height: 28), - ))); - break; - case "%tetrio%": - result.add(WidgetSpan(child: Padding( - padding: const EdgeInsets.only(left: 8), - child: SvgPicture.asset("res/icons/tetrio-logo.svg", height: 28), - ))); - break; - default: // if not, insert text span - result.add(TextSpan(text: " $shit", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white))); - } - } - return result; - } - - /// Distinguishment title is barely predictable thing. - /// Receives [text], which is footer and returns sets of widgets for RichText widget - String getDistinguishmentSubtitle(String? text){ - // TWC champions don't have footer in their distinguishments - if (distinguishment?.type == "twc") return "${distinguishment?.detail} TETR.IO World Championship"; - // In case if it missing for some other reason, return this - if (text == null) return "Footer is missing"; - // If everything ok, return as it is - return text; - } - - /// Handles [news] entry and returns widget that contains this entry - ListTile getNewsTile(NewsEntry news){ - Map gametypes = { - "40l": t.sprint, - "blitz": t.blitz, - "5mblast": "5,000,000 Blast", - "zenith": "Quick Play", - "zenithex": "Quick Play Expert", - }; - - // Individuly handle each entry type - switch (news.type) { - case "leaderboard": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.leaderboardStart, - children: [ - TextSpan(text: "№${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.leaderboardMiddle), - TextSpan(text: "№${gametypes[news.data["gametype"]]}", style: const TextStyle(fontWeight: FontWeight.bold)), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - ); - case "personalbest": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.personalbest, - children: [ - TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.personalbestMiddle), - TextSpan(text: switch (news.data["gametype"]){ - "blitz" => NumberFormat.decimalPattern().format(news.data["result"]), - "40l" => get40lTime((news.data["result"]*1000).floor()), - "5mblast" => get40lTime((news.data["result"]*1000).floor()), - "zenith" => "${f2.format(news.data["result"])} m.", - "zenithex" => "${f2.format(news.data["result"])} m.", - _ => "unknown" - }, - style: const TextStyle(fontWeight: FontWeight.bold) - ), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/improvement-local.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "badge": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.badgeStart, - children: [ - TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.badgeEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_badges/${news.data["type"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "rankup": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.rankupStart, - children: [ - TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.rankupEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter_gift": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterGiftStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - default: // if type is unknown - return ListTile( - title: Text(t.newsParts.unknownNews(type: news.type)), - subtitle: Text(timestamp(news.timestamp)), - ); - } - } - - Widget getShit(BuildContext context, bool bigScreen, bool showNewsTitle){ - return Column( - children: [ - if (distinguishment != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), - child: Column( - children: [ - Text(t.distinguishment, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28), textAlign: TextAlign.center), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: getDistinguishmentTitle(distinguishment?.header), - ), - ), - Text(getDistinguishmentSubtitle(distinguishment?.footer), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), - ], - ), - ), - if (bio != null) - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 48), - child: Column( - children: [ - Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended",fontSize: bigScreen ? 42 : 28)), - MarkdownBody(data: bio!, styleSheet: MarkdownStyleSheet(textScaler: TextScaler.linear(1.5), textAlign: WrapAlignment.center)) // Text(bio!, style: const Theme.of(context).textTheme.displayLarge), - ], - ), - ), - if (zen != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), - child: Column( - children: [ - Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Text("${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), - Text("${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: Theme.of(context).textTheme.displayLarge), - Container( - constraints: const BoxConstraints(maxWidth: 300.0), - child: Row(children: [ - const Text("Score requirement to level up:"), - const Spacer(), - Text(intf.format(zen!.scoreRequirement)) - ],), - ) - ], - ), - ), - if (newsletter != null && newsletter!.news.isNotEmpty && showNewsTitle) - Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ], - ); - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - if (constraints.maxWidth >= 1024){ - return Row( - children: [ - SizedBox(width: 450, child: getShit(context, true, false)), - SizedBox(width: constraints.maxWidth - 450, child: ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: newsletter!.news.length+1, - itemBuilder: (BuildContext context, int index) { - return index == 0 ? Center(child: Text(t.news, style: Theme.of(context).textTheme.titleLarge)) : getNewsTile(newsletter!.news[index-1]); - } - )) - ] - ); - } - else { - return ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: newsletter!.news.length+1, - itemBuilder: (BuildContext context, int index) { - return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter!.news[index-1]); - }, - ); - } - }); - } -} +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/views/destination_calculator.dart'; +import 'package:tetra_stats/views/destination_cutoffs.dart'; +import 'package:tetra_stats/views/destination_graphs.dart'; +import 'package:tetra_stats/views/destination_home.dart'; +import 'package:tetra_stats/views/destination_info.dart'; +import 'package:tetra_stats/views/destination_leaderboards.dart'; +import 'package:tetra_stats/views/destination_saved_data.dart'; +import 'package:tetra_stats/views/destination_settings.dart'; +import 'package:tetra_stats/main.dart'; + +late Future _data; +late Future _newsData; +TetrioPlayersLeaderboard? _everyone; + +Future getData(String searchFor) async { + TetrioPlayer player; + try{ + if (searchFor.startsWith("ds:")){ + player = await teto.fetchPlayer(searchFor.substring(3), isItDiscordID: true); // we trying to get him with that + }else{ + player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username + } + + }on TetrioPlayerNotExist{ + return FetchResults(false, null, [], null, null, null, null, false, TetrioPlayerNotExist()); + } + late Summaries summaries; + late Cutoffs? cutoffs; + late CutoffsTetrio? averages; + try { + List requests = await Future.wait([ + teto.fetchSummaries(player.userId), + teto.fetchCutoffsBeanserver(), + if (prefs.getBool("showAverages") == true) teto.fetchCutoffsTetrio() + ]); + + summaries = requests[0]; + cutoffs = requests.elementAtOrNull(1); + averages = requests.elementAtOrNull(2); + } on Exception catch (e) { + return FetchResults(false, null, [], null, null, null, null, false, e); + } + PlayerLeaderboardPosition? _meAmongEveryone; + if (prefs.getBool("showPositions") == true){ + // Get tetra League leaderboard + _everyone = teto.getCachedLeaderboard(); + _everyone ??= await teto.fetchTLLeaderboard(); + if (_everyone!.leaderboard.isNotEmpty){ + _meAmongEveryone = await compute(_everyone!.getLeaderboardPosition, {player.userId: summaries.league}); + if (_meAmongEveryone != null) teto.cacheLeaderboardPositions(player.userId, _meAmongEveryone); + } + } + List states = await teto.getStates(player.userId, season: currentSeason); + + bool isTracking = await teto.isPlayerTracking(player.userId); + if (isTracking){ // if tracked - save data to local DB + await teto.storeState(summaries.league); + } + + return FetchResults(true, player, states, summaries, cutoffs, averages, _meAmongEveryone, isTracking, null); + } + +class MainView extends StatefulWidget { + final String? player; + /// The very first view, that user see when he launch this programm. + /// By default it loads my or defined in preferences user stats, but + /// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu. + const MainView({super.key, this.player}); + + @override + State createState() => _MainState(); +} + +enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} +enum CardMod {info, records, ex, exRecords} +Map cardsTitles = { + Cards.overview: "Overview", + Cards.tetraLeague: t.tetraLeague, + Cards.quickPlay: t.quickPlay, + //Cards.quickPlayExpert: "${t.quickPlay} ${t.expert}", + Cards.sprint: t.sprint, + Cards.blitz: t.blitz, + //Cards.other: t.other +}; + +late ScrollController controller; + +class _MainState extends State with TickerProviderStateMixin { + int destination = 0; + String _searchFor = "6098518e3d5155e6ec429cdc"; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + teto.open(); + controller = ScrollController(); + changePlayer(_searchFor); + super.initState(); + } + + void changePlayer(String player) { + setState(() { + _searchFor = player; + _data = getData(_searchFor); + _newsData = teto.fetchNews(_searchFor); + }); + } + + @override + void dispose() { + controller.dispose(); + _searchController.dispose(); + super.dispose(); + } + + NavigationRailDestination getDestinationButton(IconData icon, String title){ + return NavigationRailDestination( + icon: Tooltip( + message: title, + child: Icon(icon) + ), + selectedIcon: Icon(icon), + label: Text(title), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + drawer: SearchDrawer(changePlayer: changePlayer, controller: _searchController), + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TweenAnimationBuilder( + child: NavigationRail( + leading: FloatingActionButton( + elevation: 0, + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + child: const Icon(Icons.search), + ), + trailing: IconButton( + onPressed: () { + // Add your onPressed code here! + }, + icon: const Icon(Icons.more_horiz_rounded), + ), + destinations: [ + getDestinationButton(Icons.home, "Home"), + getDestinationButton(Icons.data_thresholding_outlined, "Graphs"), + getDestinationButton(Icons.leaderboard, "Leaderboards"), + getDestinationButton(Icons.compress, "Cutoffs"), + getDestinationButton(Icons.calculate, "Calc"), + getDestinationButton(Icons.info_outline, "Information"), + getDestinationButton(Icons.storage, "Saved Data"), + getDestinationButton(Icons.settings, "Settings"), + ], + selectedIndex: destination, + onDestinationSelected: (value) { + setState(() { + destination = value; + }); + }, + ), + duration: Durations.long4, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(-80+value*80, 0, 0), + child: Opacity(opacity: value, child: child), + ); + }, + ), + Expanded( + child: switch (destination){ + 0 => DestinationHome(searchFor: _searchFor, constraints: constraints, dataFuture: _data, newsFuture: _newsData), + 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), + 2 => DestinationLeaderboards(constraints: constraints), + 3 => DestinationCutoffs(constraints: constraints), + 4 => DestinationCalculator(constraints: constraints), + 5 => DestinationInfo(constraints: constraints), + 6 => DestinationSavedData(constraints: constraints), + 7 => DestinationSettings(constraints: constraints), + _ => Text("Unknown destination $destination") + }, + ) + ]); + }, + )); + } +} + +class SearchDrawer extends StatefulWidget{ + final Function changePlayer; + final TextEditingController controller; + const SearchDrawer({super.key, required this.changePlayer, required this.controller}); + + @override + State createState() => _SearchDrawerState(); +} + +class _SearchDrawerState extends State { + @override + Widget build(BuildContext context) { + return Drawer( + child: StreamBuilder( + stream: teto.allPlayers, + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.done: + case ConnectionState.active: + final allPlayers = (snapshot.data != null) + ? snapshot.data as Map + : {}; + allPlayers.remove(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted + List keys = allPlayers.keys.toList(); + return NestedScrollView( + headerSliverBuilder: (BuildContext context, bool value){ + return [ + SliverToBoxAdapter( + child: SearchBar( + controller: widget.controller, + hintText: "Enter the username", + hintStyle: const WidgetStatePropertyAll(TextStyle(color: Colors.grey)), + trailing: [ + IconButton(onPressed: (){setState(() { + widget.changePlayer(widget.controller.value.text); + Navigator.of(context).pop(); + });}, icon: const Icon(Icons.search)) + ], + onSubmitted: (value) { + setState(() { + widget.changePlayer(value); + Navigator.of(context).pop(); + }); + }, + ), + ), + SliverToBoxAdapter( + child: ListTile( + leading: Icon(Icons.home), + title: Text(prefs.getString("player") ?? "dan63"), + onTap: () { + widget.changePlayer(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); + Navigator.of(context).pop(); + }, + ), + ), + SliverToBoxAdapter( + child: Divider(), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text("Tracked Players", style: Theme.of(context).textTheme.headlineLarge), + ), + ) + ]; + }, + body: ListView.builder( // Builds list of tracked players. + itemCount: allPlayers.length, + itemBuilder: (context, index) { + var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. + return ListTile( + title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states + trailing: IconButton(onPressed: (){ + teto.deletePlayerToTrack(keys[i]); + }, icon: Icon(Icons.delete, color: Colors.grey)), + onTap: () { + widget.changePlayer(keys[i]); // changes to chosen player + Navigator.of(context).pop(); // and closes itself. + }, + ); + }) + ); + } + } + ) + ); + } +} + +// class EstTrThingy extends StatelessWidget{ +// final EstTr estTr; + +// const EstTrThingy({super.key, required this.estTr}); + +// @override +// Widget build(BuildContext context) { +// return const Card( +// //child: , +// ); +// } +// } \ No newline at end of file diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart deleted file mode 100644 index 4367135..0000000 --- a/lib/views/main_view_tiles.dart +++ /dev/null @@ -1,2106 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide Badge; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:http/http.dart'; -import 'package:intl/intl.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/badge.dart'; -import 'package:tetra_stats/data_objects/beta_record.dart'; -import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; -import 'package:tetra_stats/data_objects/distinguishment.dart'; -import 'package:tetra_stats/data_objects/est_tr.dart'; -import 'package:tetra_stats/data_objects/nerd_stats.dart'; -import 'package:tetra_stats/data_objects/news.dart'; -import 'package:tetra_stats/data_objects/news_entry.dart'; -import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; -import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; -import 'package:tetra_stats/data_objects/playstyle.dart'; -import 'package:tetra_stats/data_objects/record_extras.dart'; -import 'package:tetra_stats/data_objects/record_single.dart'; -import 'package:tetra_stats/data_objects/summaries.dart'; -import 'package:tetra_stats/data_objects/tetra_league.dart'; -import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; -import 'package:tetra_stats/data_objects/tetrio_constants.dart'; -import 'package:tetra_stats/data_objects/tetrio_player.dart'; -import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/services/crud_exceptions.dart'; -import 'package:tetra_stats/utils/colors_functions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/destination_calculator.dart'; -import 'package:tetra_stats/views/destination_cutoffs.dart'; -import 'package:tetra_stats/views/destination_graphs.dart'; -import 'package:tetra_stats/views/destination_home.dart'; -import 'package:tetra_stats/views/destination_leaderboards.dart'; -import 'package:tetra_stats/views/destination_saved_data.dart'; -import 'package:tetra_stats/views/destination_settings.dart'; -import 'package:tetra_stats/views/tl_match_view.dart'; -import 'package:tetra_stats/views/compare_view_tiles.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/main.dart'; -import 'package:tetra_stats/widgets/tl_progress_bar.dart'; -import 'package:tetra_stats/widgets/user_thingy.dart'; -import 'package:transparent_image/transparent_image.dart'; -import 'package:vector_math/vector_math_64.dart' hide Colors; - -// TODO: Refactor it - -var fDiff = NumberFormat("+#,###.####;-#,###.####"); -late Future _data; -late Future _newsData; -TetrioPlayersLeaderboard? _everyone; - -Future getData(String searchFor) async { - TetrioPlayer player; - try{ - if (searchFor.startsWith("ds:")){ - player = await teto.fetchPlayer(searchFor.substring(3), isItDiscordID: true); // we trying to get him with that - }else{ - player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username - } - - }on TetrioPlayerNotExist{ - return FetchResults(false, null, [], null, null, null, null, false, TetrioPlayerNotExist()); - } - late Summaries summaries; - late Cutoffs? cutoffs; - late CutoffsTetrio? averages; - try { - List requests = await Future.wait([ - teto.fetchSummaries(player.userId), - teto.fetchCutoffsBeanserver(), - if (prefs.getBool("showAverages") == true) teto.fetchCutoffsTetrio() - ]); - - summaries = requests[0]; - cutoffs = requests.elementAtOrNull(1); - averages = requests.elementAtOrNull(2); - } on Exception catch (e) { - return FetchResults(false, null, [], null, null, null, null, false, e); - } - PlayerLeaderboardPosition? _meAmongEveryone; - if (prefs.getBool("showPositions") == true){ - // Get tetra League leaderboard - _everyone = teto.getCachedLeaderboard(); - _everyone ??= await teto.fetchTLLeaderboard(); - if (_everyone!.leaderboard.isNotEmpty){ - _meAmongEveryone = await compute(_everyone!.getLeaderboardPosition, {player.userId: summaries.league}); - if (_meAmongEveryone != null) teto.cacheLeaderboardPositions(player.userId, _meAmongEveryone); - } - } - List states = await teto.getStates(player.userId, season: currentSeason); - - bool isTracking = await teto.isPlayerTracking(player.userId); - if (isTracking){ // if tracked - save data to local DB - await teto.storeState(summaries.league); - } - - return FetchResults(true, player, states, summaries, cutoffs, averages, _meAmongEveryone, isTracking, null); - } - -class MainView extends StatefulWidget { - final String? player; - /// The very first view, that user see when he launch this programm. - /// By default it loads my or defined in preferences user stats, but - /// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu. - const MainView({super.key, this.player}); - - @override - State createState() => _MainState(); -} - -enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} -enum CardMod {info, records, ex, exRecords} -Map cardsTitles = { - Cards.overview: "Overview", - Cards.tetraLeague: t.tetraLeague, - Cards.quickPlay: t.quickPlay, - //Cards.quickPlayExpert: "${t.quickPlay} ${t.expert}", - Cards.sprint: t.sprint, - Cards.blitz: t.blitz, - //Cards.other: t.other -}; - -late ScrollController controller; - -class _MainState extends State with TickerProviderStateMixin { - int destination = 0; - String _searchFor = "6098518e3d5155e6ec429cdc"; - final TextEditingController _searchController = TextEditingController(); - - @override - void initState() { - teto.open(); - controller = ScrollController(); - changePlayer(_searchFor); - super.initState(); - } - - void changePlayer(String player) { - setState(() { - _searchFor = player; - _data = getData(_searchFor); - _newsData = teto.fetchNews(_searchFor); - }); - } - - @override - void dispose() { - controller.dispose(); - _searchController.dispose(); - super.dispose(); - } - - NavigationRailDestination getDestinationButton(IconData icon, String title){ - return NavigationRailDestination( - icon: Tooltip( - message: title, - child: Icon(icon) - ), - selectedIcon: Icon(icon), - label: Text(title), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - drawer: SearchDrawer(changePlayer: changePlayer, controller: _searchController), - body: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TweenAnimationBuilder( - child: NavigationRail( - leading: FloatingActionButton( - elevation: 0, - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - child: const Icon(Icons.search), - ), - trailing: IconButton( - onPressed: () { - // Add your onPressed code here! - }, - icon: const Icon(Icons.more_horiz_rounded), - ), - destinations: [ - getDestinationButton(Icons.home, "Home"), - getDestinationButton(Icons.data_thresholding_outlined, "Graphs"), - getDestinationButton(Icons.leaderboard, "Leaderboards"), - getDestinationButton(Icons.compress, "Cutoffs"), - getDestinationButton(Icons.calculate, "Calc"), - getDestinationButton(Icons.info_outline, "Information"), - getDestinationButton(Icons.storage, "Saved Data"), - getDestinationButton(Icons.settings, "Settings"), - ], - selectedIndex: destination, - onDestinationSelected: (value) { - setState(() { - destination = value; - }); - }, - ), - duration: Durations.long4, - tween: Tween(begin: 0, end: 1), - curve: Easing.standard, - builder: (context, value, child) { - return Container( - transform: Matrix4.translationValues(-80+value*80, 0, 0), - child: Opacity(opacity: value, child: child), - ); - }, - ), - Expanded( - child: switch (destination){ - 0 => DestinationHome(searchFor: _searchFor, constraints: constraints, dataFuture: _data, newsFuture: _newsData), - 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), - 2 => DestinationLeaderboards(constraints: constraints), - 3 => DestinationCutoffs(constraints: constraints), - 4 => DestinationCalculator(constraints: constraints), - 5 => DestinationInfo(constraints: constraints), - 6 => DestinationSavedData(constraints: constraints), - 7 => DestinationSettings(constraints: constraints), - _ => Text("Unknown destination $destination") - }, - ) - ]); - }, - )); - } -} - -class DestinationInfo extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationInfo({super.key, required this.constraints}); - - @override - State createState() => _DestinationInfo(); -} - -class InfoCard extends StatelessWidget { - final double height; - final String assetLink; - final String title; - final String description; - - const InfoCard({required this.height, required this.assetLink, required this.title, required this.description}); - - @override - Widget build(BuildContext context) { - return Card( - clipBehavior: Clip.hardEdge, - child: SizedBox( - width: 450, - height: height, - child: Column( - children: [ - Image.asset("res/images/Снимок экрана_2023-11-06_01-00-50.png", fit: BoxFit.cover, height: 300.0), - Text(title, style: Theme.of(context).textTheme.titleLarge), - Padding( - padding: const EdgeInsets.all(12.0), - child: Text(description), - ), - Spacer() - ], - ), - ), - ); - } - -} - -class _DestinationInfo extends State { - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: Center(child: Text("Information Center", style: Theme.of(context).textTheme.titleLarge)), - ), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - InfoCard( - height: widget.constraints.maxHeight - 77, - assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", - title: "Shizuru!", - description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru " - ), - InfoCard( - height: widget.constraints.maxHeight - 77, - assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", - title: "Shizuru!", - description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru " - ), - InfoCard( - height: widget.constraints.maxHeight - 77, - assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", - title: "Shizuru!", - description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru " - ), - Card() - ], - ), - ) - ], - ); - } -} - -class NewsThingy extends StatelessWidget{ - final News news; - - const NewsThingy(this.news, {super.key}); - - ListTile getNewsTile(NewsEntry news){ - Map gametypes = { - "40l": t.sprint, - "blitz": t.blitz, - "5mblast": "5,000,000 Blast", - "zenith": "Quick Play", - "zenithex": "Quick Play Expert", - }; - - // Individuly handle each entry type - switch (news.type) { - case "leaderboard": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.leaderboardStart, - children: [ - TextSpan(text: "№${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.leaderboardMiddle), - TextSpan(text: "№${gametypes[news.data["gametype"]]}", style: const TextStyle(fontWeight: FontWeight.bold)), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - ); - case "personalbest": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.personalbest, - children: [ - TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.personalbestMiddle), - TextSpan(text: switch (news.data["gametype"]){ - "blitz" => NumberFormat.decimalPattern().format(news.data["result"]), - "40l" => get40lTime((news.data["result"]*1000).floor()), - "5mblast" => get40lTime((news.data["result"]*1000).floor()), - "zenith" => "${f2.format(news.data["result"])} m.", - "zenithex" => "${f2.format(news.data["result"])} m.", - _ => "unknown" - }, - style: const TextStyle(fontWeight: FontWeight.bold) - ), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/improvement-local.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "badge": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.badgeStart, - children: [ - TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.badgeEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_badges/${news.data["type"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "rankup": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.rankupStart, - children: [ - TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.rankupEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter_gift": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterGiftStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - default: // if type is unknown - return ListTile( - title: Text(t.newsParts.unknownNews(type: news.type)), - subtitle: Text(timestamp(news.timestamp)), - ); - } - } - - @override - Widget build(BuildContext context) { - return Card( - child: SingleChildScrollView( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ] - ), - if (news.news.isEmpty) const Center(child: Text("Empty list")) - else for (NewsEntry entry in news.news) getNewsTile(entry) - ], - ), - ), - ); - } - -} - -class DistinguishmentThingy extends StatelessWidget{ - final Distinguishment distinguishment; - - const DistinguishmentThingy(this.distinguishment, {super.key}); - - List getDistinguishmentTitle(String? text) { - // TWC champions don't have header in their distinguishments - if (distinguishment.type == "twc") return [const TextSpan(text: "TETR.IO World Champion", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.yellowAccent))]; - // In case if it missing for some other reason, return this - if (text == null) return [const TextSpan(text: "Header is missing", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.redAccent))]; - - // Handling placeholders for logos - var exploded = text.split(" "); // wtf PHP reference? - List result = []; - for (String shit in exploded){ - switch (shit) { // if %% thingy was found, insert svg of icon - case "%osk%": - result.add(WidgetSpan(child: Padding( - padding: const EdgeInsets.only(left: 8), - child: SvgPicture.asset("res/icons/osk.svg", height: 28), - ))); - break; - case "%tetrio%": - result.add(WidgetSpan(child: Padding( - padding: const EdgeInsets.only(left: 8), - child: SvgPicture.asset("res/icons/tetrio-logo.svg", height: 28), - ))); - break; - default: // if not, insert text span - result.add(TextSpan(text: " $shit", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white))); - } - } - return result; - } - - /// Distinguishment title is barely predictable thing. - /// Receives [text], which is footer and returns sets of widgets for RichText widget - String getDistinguishmentSubtitle(String? text){ - // TWC champions don't have footer in their distinguishments - if (distinguishment.type == "twc") return "${distinguishment.detail} TETR.IO World Championship"; - // In case if it missing for some other reason, return this - if (text == null) return "Footer is missing"; - // If everything ok, return as it is - return text; - } - - Color getCardTint(String type, String detail){ - switch(type){ - case "staff": - switch(detail){ - case "founder": return const Color(0xAAFD82D4); - case "kagarin": return const Color(0xAAFF0060); - case "team": return const Color(0xAAFACC2E); - case "team-minor": return const Color(0xAAF5BD45); - case "administrator": return const Color(0xAAFF4E8A); - case "globalmod": return const Color(0xAAE878FF); - case "communitymod": return const Color(0xAA4E68FB); - case "alumni": return const Color(0xAA6057DB); - default: return theme.colorScheme.surface; - } - case "champion": - switch (detail){ - case "blitz": - case "40l": return const Color(0xAACCF5F6); - case "league": return const Color(0xAAFFDB31); - } - case "twc": return const Color(0xAAFFDB31); - default: return theme.colorScheme.surface; - } - return theme.colorScheme.surface; - } - - @override - Widget build(BuildContext context) { - return Card( - surfaceTintColor: getCardTint(distinguishment.type, distinguishment.detail??"null"), - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.distinguishment, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ], - ), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: getDistinguishmentTitle(distinguishment.header), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0), - child: Text(getDistinguishmentSubtitle(distinguishment.footer), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), - ), - ], - ), - ); - } -} - -class FakeDistinguishmentThingy extends StatelessWidget{ - final bool banned; - final bool badStanding; - final bool bot; - final String? botMaintainers; - - FakeDistinguishmentThingy({super.key, this.banned = false, this.badStanding = false, this.bot = false, this.botMaintainers}); - - Color getCardTint(){ - if (banned) return Colors.red; - if (badStanding) return Colors.redAccent; - if (bot) return const Color.fromARGB(255, 60, 93, 55); - return theme.colorScheme.surface; - } - - InlineSpan getDistinguishmentTitle() { - String text = ""; - if (banned) text = "banned"; - if (badStanding) text = "bad standing"; - if (bot) text = "bot account"; - return TextSpan(text: text.toUpperCase(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white)); - } - - String getDistinguishmentSubtitle(){ - if (banned) return "Bans are placed when TETR.IO rules or terms of service are broken"; - if (badStanding) return "One or more recent bans on record"; - if (bot) return "Operated by $botMaintainers"; - return ""; - } - - @override - Widget build(BuildContext context) { - return Card( - surfaceTintColor: getCardTint(), - child: Container( - decoration: banned ? const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.transparent, Color.fromARGB(171, 244, 67, 54), Color.fromARGB(171, 244, 67, 54)], - stops: [0.1, 0.9, 0.01], - tileMode: TileMode.mirror, - begin: Alignment.topLeft, - end: AlignmentDirectional(-0.95, -0.95) - ) - ) : null, - child: Column( - children: [ - Center( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [getDistinguishmentTitle()], - ), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0), - child: Text(getDistinguishmentSubtitle(), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), - ), - ], - ), - ), - ); - } - -} - -class BadgesThingy extends StatelessWidget{ - final List badges; - - const BadgesThingy({super.key, required this.badges}); - - @override - Widget build(BuildContext context) { - return Card( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), - child: Row( - children: [ - const Text("Badges", style: TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer(), - Text(intf.format(badges.length)) - ], - ), - ), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (var badge in 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 - ? t.obtainDate(date: timestamp(badge.ts!)) - : t.assignedManualy), - ], - ) - ], - ), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ), - tooltip: badge.label, - icon: Image.asset( - "res/tetrio_badges/${badge.badgeId}.png", - height: 32, - errorBuilder: (context, error, stackTrace) { - return Image.network( - kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png", - height: 32, - errorBuilder:(context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 32, width: 32); - } - ); - }, - ) - ) - ], - ), - ) - ], - ), - ); - } -} - -class NewUserThingy extends StatefulWidget { - final TetrioPlayer player; - final bool showStateTimestamp; - final bool initIsTracking; - final Function setState; - - const NewUserThingy({super.key, required this.player, required this.initIsTracking, required this.showStateTimestamp, required this.setState}); - - @override - State createState() => _NewUserThingyState(); -} - -class _NewUserThingyState extends State with SingleTickerProviderStateMixin { - late AnimationController _addToTrackAnimController; - late Animation _addToTrackAnim; - - @override - void initState(){ - _addToTrackAnimController = AnimationController( - value: widget.initIsTracking ? 1.0 : 0.0, - duration: Durations.extralong4, - vsync: this, - ); - _addToTrackAnim = new Tween( - begin: 0.0, - end: 1.0, - ).animate(new CurvedAnimation( - parent: _addToTrackAnimController, - curve: Cubic(.15,-0.40,.86,-0.39), - reverseCurve: Cubic(0,.99,.99,1.01) - )); - - super.initState(); - } - - @override - void dispose() { - _addToTrackAnimController.dispose(); - super.dispose(); - } - - Color roleColor(String role){ - switch (role){ - case "sysop": - return const Color.fromARGB(255, 23, 165, 133); - case "admin": - return const Color.fromARGB(255, 255, 78, 138); - case "mod": - return const Color.fromARGB(255, 204, 128, 242); - case "halfmod": - return const Color.fromARGB(255, 95, 118, 254); - case "bot": - return const Color.fromARGB(255, 60, 93, 55); - case "banned": - return const Color.fromARGB(255, 248, 28, 28); - default: - return Colors.white10; - } - } - - String fontStyle(int length){ - if (length < 10) return "Eurostile Round Extended"; - else if (length < 13) return "Eurostile Round"; - else return "Eurostile Round Condensed"; - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return LayoutBuilder(builder: (context, constraints) { - double pfpHeight = 128; - int xpTableID = 0; - - while (widget.player.xp > xpTableScuffed.values.toList()[xpTableID]) { - xpTableID++; - } - - return Card( - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Container( - constraints: const BoxConstraints(maxWidth: 960), - height: widget.player.bannerRevision != null ? 218.0 : 138.0, - child: Stack( - //clipBehavior: Clip.none, - children: [ - // TODO: osk banner can cause memory leak - if (widget.player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${widget.player.userId}&rv=${widget.player.bannerRevision}" : "https://tetr.io/user-content/banners/${widget.player.userId}.jpg?rv=${widget.player.bannerRevision}", - placeholder: kTransparentImage, - fit: BoxFit.cover, - height: 120, - fadeInCurve: Easing.standard, fadeInDuration: Durations.long4 - ), - Positioned( - top: widget.player.bannerRevision != null ? 90.0 : 10.0, - left: 16.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: widget.player.role == "banned" - ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) - : widget.player.avatarRevision != null - ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${widget.player.userId}&rv=${widget.player.avatarRevision}" : "https://tetr.io/user-content/avatars/${widget.player.userId}.jpg?rv=${widget.player.avatarRevision}", - fit: BoxFit.fitHeight, height: 128, placeholder: kTransparentImage, fadeInCurve: Easing.emphasizedDecelerate, fadeInDuration: Durations.long4) - : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight), - ) - ), - Positioned( - top: widget.player.bannerRevision != null ? 120.0 : 40.0, - left: 160.0, - child: Tooltip( - message: "${widget.player.userId}\n(Click to copy user ID)", - child: RichText(text: TextSpan(text: widget.player.username, style: TextStyle( - fontFamily: fontStyle(widget.player.username.length), - fontSize: 28, - ), - recognizer: TapGestureRecognizer()..onTap = (){ - copyToClipboard(widget.player.userId); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); - } - ) - ) - ), - ), - Positioned( - top: widget.player.bannerRevision != null ? 160.0 : 80.0, - left: 160.0, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Chip(label: Text(widget.player.role.toUpperCase(), style: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(widget.player.role))), - ), - RichText( - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: - [ - if (widget.player.friendCount > 0) const WidgetSpan(child: Icon(Icons.person), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - if (widget.player.friendCount > 0) TextSpan(text: "${intf.format(widget.player.friendCount)} "), - if (widget.player.supporterTier > 0) WidgetSpan(child: Icon(widget.player.supporterTier > 1 ? Icons.star : Icons.star_border, color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - if (widget.player.supporterTier > 0) TextSpan(text: widget.player.supporterTier.toString(), style: TextStyle(color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)), - ] - ) - ) - ], - ), - ), - Positioned( - top: widget.player.bannerRevision != null ? 193.0 : 113.0, - left: 160.0, - child: SizedBox( - width: 270, - child: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - if (widget.player.country != null) TextSpan(text: "${t.countries[widget.player.country]} • "), - TextSpan(text: timestamp(widget.player.registrationTime), style: const TextStyle(color: Colors.grey)) - ] - ) - ), - ) - ), - Positioned( - top: widget.player.bannerRevision != null ? 126.0 : 46.0, - right: 16.0, - child: RichText( - textAlign: TextAlign.end, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - TextSpan(text: "Level ${(widget.player.level.isNegative || widget.player.level.isNaN) ? "---" : intf.format(widget.player.level.floor())}", style: TextStyle(decoration: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: (widget.player.level.isNegative || widget.player.level.isNaN) ? Colors.grey : Colors.white), recognizer: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TapGestureRecognizer()?..onTap = (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text("Level ${intf.format(widget.player.level.floor())}", textAlign: TextAlign.center), - content: SingleChildScrollView( - child: ListBody(children: [ - Text( - "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(widget.player.xp)} XP", - style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold) - ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: SfLinearGauge( - minimum: 0, - maximum: 1, - interval: 1, - ranges: [ - LinearGaugeRange(startValue: 0, endValue: widget.player.level - widget.player.level.floor(), color: Colors.cyanAccent), - LinearGaugeRange(startValue: 0, endValue: (widget.player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) - ], - showTicks: true, - showLabels: false - ), - ), - Text("${t.statCellNum.xpProgress}: ${((widget.player.level - widget.player.level.floor()) * 100).toStringAsFixed(2)} %"), - Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((widget.player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - widget.player.xp)} ${t.statCellNum.xpLeft})") - ] - ), - ), - actions: [ - TextButton( - child: const Text("OK"), - onPressed: () {Navigator.of(context).pop();} - ) - ] - ) - ); - }), - const TextSpan(text:"\n"), - TextSpan(text: widget.player.gameTime.isNegative ? "-h --m" : playtime(widget.player.gameTime), style: TextStyle(color: widget.player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: widget.player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !widget.player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ - Duration accountAge = DateTime.timestamp().difference(widget.player.registrationTime); - Duration avgGametimeADay = Duration(microseconds: (widget.player.gameTime.inMicroseconds / accountAge.inDays).floor()); - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.exactGametime, textAlign: TextAlign.center), - content: SingleChildScrollView( - child: Column( - children: [ - RichText(text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white, fontSize: 28), - children: [ - TextSpan(text: "${intf.format(widget.player.gameTime.inHours)}"), - TextSpan(text: ":${nonsecs.format(widget.player.gameTime.inMinutes%60)}:${nonsecs.format(widget.player.gameTime.inSeconds%60)}"), - TextSpan(text: ".${nonsecs3.format(widget.player.gameTime.inMicroseconds%1000000)}", style: TextStyle(fontSize: 14)) - ] - )), - Text("${playtime(avgGametimeADay)} a day in average"), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text("It's ${f4.format(widget.player.gameTime.inSeconds/31536000)} years,"), - ), - Text("or ${f4.format(widget.player.gameTime.inSeconds/2628000)} months,"), - Text("or ${f4.format(widget.player.gameTime.inSeconds/86400)} days,"), - Text("or ${f2.format(widget.player.gameTime.inMilliseconds/60000)} minutes,"), - Text("or ${intf.format(widget.player.gameTime.inSeconds)} seconds"), - ] - ), - ), - actions: [ - TextButton( - child: const Text("OK"), - onPressed: () {Navigator.of(context).pop();} - ) - ] - ) - ); - }) : null), - const TextSpan(text:"\n"), - TextSpan(text: widget.player.gamesWon > -1 ? intf.format(widget.player.gamesWon) : "---", style: TextStyle(color: widget.player.gamesWon > -1 ? Colors.white : Colors.grey)), - TextSpan(text: "/${widget.player.gamesPlayed > -1 ? intf.format(widget.player.gamesPlayed) : "---"}", style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), - ] - ) - ) - ) - ], - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: AnimatedBuilder( - animation: _addToTrackAnim, - builder: (context, child) { - double firstButtonPosition = 0+(_addToTrackAnim.value as double)*25; - double secondButtonPosition = -25+(_addToTrackAnim.value as double)*25; - double firstButtonOpacity = 1-(_addToTrackAnim.value as double)*2; - double secondButtonOpacity = _addToTrackAnim.value*2-1; - return ElevatedButton.icon( - onPressed: (){ - _addToTrackAnimController.value == 1 ? teto.deletePlayerToTrack(widget.player.userId) : teto.addPlayerToTrack(widget.player); - _addToTrackAnim.isCompleted ? _addToTrackAnimController.reverse() : _addToTrackAnimController.forward(); - }, - icon: _addToTrackAnim.value < 0.5 ? Opacity( - opacity: min(1, firstButtonOpacity), - child: Transform.translate( - offset: Offset(0, _addToTrackAnim.status == AnimationStatus.forward ? firstButtonPosition*4 : firstButtonPosition), - child: Transform.rotate( - angle:_addToTrackAnim.status == AnimationStatus.forward ? (_addToTrackAnim.value as double)*2 : 0, - child: const Icon(Icons.person_add), - ), - ), - ) : Container( - transform: Matrix4.translationValues(secondButtonPosition*5, -secondButtonPosition*25, 0), - child: Opacity( - opacity: max(0, min(1, secondButtonOpacity)), - child: Transform.rotate( - angle:_addToTrackAnim.status == AnimationStatus.reverse ? (1-_addToTrackAnim.value as double)*-20 : 0, - child: const Icon(Icons.person_remove) - ) - ) - ), - label: _addToTrackAnim.value < 0.5 ? Container( - transform: Matrix4.translationValues(0, firstButtonPosition, 0), - child: Opacity( - opacity: max(min(1, firstButtonOpacity), 0), - child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.forward ? "Done!" : "Track") - ) - ) : Container( - transform: Matrix4.translationValues(0, secondButtonPosition, 0), - child: Opacity( - opacity: max(0, min(1, secondButtonOpacity)), - child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.reverse ? "Done! " : "Stop tracking") - ) - ), - style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0)))))); - }, - )), - Expanded( - child: ElevatedButton.icon( - onPressed: (){ - Navigator.push(context, MaterialPageRoute( - builder: (context) => CompareView(widget.player), - ), - ); - }, - icon: const Icon(Icons.balance), - label: Text(t.compare), - style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))) - ) - ) - ], - ) - ], - ), - ); - }); - } -} - -class SearchDrawer extends StatefulWidget{ - final Function changePlayer; - final TextEditingController controller; - const SearchDrawer({super.key, required this.changePlayer, required this.controller}); - - @override - State createState() => _SearchDrawerState(); -} - -class _SearchDrawerState extends State { - @override - Widget build(BuildContext context) { - return Drawer( - child: StreamBuilder( - stream: teto.allPlayers, - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.done: - case ConnectionState.active: - final allPlayers = (snapshot.data != null) - ? snapshot.data as Map - : {}; - allPlayers.remove(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted - List keys = allPlayers.keys.toList(); - return NestedScrollView( - headerSliverBuilder: (BuildContext context, bool value){ - return [ - SliverToBoxAdapter( - child: SearchBar( - controller: widget.controller, - hintText: "Enter the username", - hintStyle: const WidgetStatePropertyAll(TextStyle(color: Colors.grey)), - trailing: [ - IconButton(onPressed: (){setState(() { - widget.changePlayer(widget.controller.value.text); - Navigator.of(context).pop(); - });}, icon: const Icon(Icons.search)) - ], - onSubmitted: (value) { - setState(() { - widget.changePlayer(value); - Navigator.of(context).pop(); - }); - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( - leading: Icon(Icons.home), - title: Text(prefs.getString("player") ?? "dan63"), - onTap: () { - widget.changePlayer(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); - Navigator.of(context).pop(); - }, - ), - ), - SliverToBoxAdapter( - child: Divider(), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text("Tracked Players", style: Theme.of(context).textTheme.headlineLarge), - ), - ) - ]; - }, - body: ListView.builder( // Builds list of tracked players. - itemCount: allPlayers.length, - itemBuilder: (context, index) { - var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. - return ListTile( - title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states - trailing: IconButton(onPressed: (){ - teto.deletePlayerToTrack(keys[i]); - }, icon: Icon(Icons.delete, color: Colors.grey)), - onTap: () { - widget.changePlayer(keys[i]); // changes to chosen player - Navigator.of(context).pop(); // and closes itself. - }, - ); - }) - ); - } - } - ) - ); - } -} - -class TetraLeagueThingy extends StatelessWidget{ - final TetraLeague league; - final TetraLeague? toCompare; - final Cutoffs? cutoffs; - final CutoffTetrio? averages; - final PlayerLeaderboardPosition? lbPos; - - const TetraLeagueThingy({super.key, required this.league, this.toCompare, this.cutoffs, this.averages, this.lbPos}); - - @override - Widget build(BuildContext context) { - return Card( - //surfaceTintColor: rankColors[league.rank], - child: Column( - children: [ - TLRatingThingy(userID: league.id, tlData: league, oldTl: toCompare, showPositions: true), - if (league.gamesPlayed > 9) TLProgress( - tlData: league, - previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, - nextRankTRcutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.tr[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, - previousRankTRcutoffTarget: league.rank != "z" ? rankTargets[league.rank] : null, - nextRankTRcutoffTarget: (league.rank != "z" && league.rank != "x+") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(league.rank)+1)] : null, - previousGlickoCutoff: cutoffs != null ? cutoffs!.glicko[league.rank != "z" ? league.rank : league.percentileRank] : null, - nextRankGlickoCutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.glicko[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, - ), - Row( - // spacing: 25.0, - // alignment: WrapAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: Table( - defaultVerticalAlignment: TableCellVerticalAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(league.apm != null ? f2.format(league.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)), - Text(" APM", style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)), - if (toCompare != null) Text(" (${comparef2.format(league.apm!-toCompare!.apm!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.apm!-toCompare!.apm!))), - if (lbPos != null) Text(lbPos?.apm != null ? (lbPos!.apm!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.apm!.percentage*100)}%)" : " (№ ${lbPos!.apm!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.apm != null ? getColorOfRank(lbPos!.apm!.position) : null)) - ]), - TableRow(children: [ - Text(league.pps != null ? f2.format(league.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)), - Text(" PPS", style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)), - if (toCompare != null) Text(" (${comparef2.format(league.pps!-toCompare!.pps!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.pps!-toCompare!.pps!))), - if (lbPos != null) Text(lbPos?.pps != null ? (lbPos!.pps!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.pps!.percentage*100)}%)" : " (№ ${lbPos!.pps!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.pps != null ? getColorOfRank(lbPos!.pps!.position) : null)) - ]), - TableRow(children: [ - Text(league.vs != null ? f2.format(league.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)), - Text(" VS", style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)), - if (toCompare != null) Text(" (${comparef2.format(league.vs!-toCompare!.vs!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.vs!-toCompare!.vs!))), - if (lbPos != null) Text(lbPos?.vs != null ? (lbPos!.vs!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.vs!.percentage*100)}%)" : " (№ ${lbPos!.vs!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.vs != null ? getColorOfRank(lbPos!.vs!.position) : null)) - ]) - ], - ), - ), - ), - GaugetThingy(value: league.winrate, min: 0, max: 1, tickInterval: 0.25, label: "Winrate", sideSize: 128, fractionDigits: 2, moreIsBetter: true, oldValue: toCompare?.winrate, percentileFormat: true), - Expanded( - child: Center( - child: Table( - defaultVerticalAlignment: TableCellVerticalAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - //Text("APM: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Games", style: TextStyle(fontSize: 21)), - if (toCompare != null) Text(" (${comparef2.format(league.gamesPlayed-toCompare!.gamesPlayed)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - if (lbPos != null) Text(lbPos?.gamesPlayed != null ? (lbPos!.gamesPlayed!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.gamesPlayed!.percentage*100)}%)" : " (№ ${lbPos!.gamesPlayed!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.gamesPlayed != null ? getColorOfRank(lbPos!.gamesPlayed!.position) : null)) - ]), - TableRow(children: [ - //Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Won", style: TextStyle(fontSize: 21)), - if (toCompare != null) Text(" (${comparef2.format(league.gamesWon-toCompare!.gamesWon)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - if (lbPos != null) Text(lbPos?.gamesWon != null ? (lbPos!.gamesWon!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.gamesWon!.percentage*100)}%)" : " (№ ${lbPos!.gamesWon!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.gamesWon != null ? getColorOfRank(lbPos!.gamesWon!.position) : null)) - ]), - TableRow(children: [ - //Text("VS: ", style: TextStyle(fontSize: 21)), - Tooltip(child: Text("${league.gxe.isNegative ? "---" : f3.format(league.gxe)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.gxe.isNegative ? Colors.grey : Colors.white)), message: "${f2.format(league.s1tr)}",), - Text(" GLIXARE", style: TextStyle(fontSize: 21, color: league.gxe.isNegative ? Colors.grey : Colors.white)), - if (toCompare != null) Text(" (${comparef.format(league.gxe-toCompare!.gxe)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.gxe-toCompare!.gxe))), - if (lbPos != null) Text(lbPos?.glixare != null ? (lbPos!.glixare!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.glixare!.percentage*100)}%)" : " (№ ${lbPos!.glixare!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.glixare != null ? getColorOfRank(lbPos!.glixare!.position) : null)) - ]), - ], - ), - ), - ), - ], - ), - ], - ), - ); - } -} - -class NerdStatsThingy extends StatelessWidget{ - final NerdStats nerdStats; - final NerdStats? oldNerdStats; - final CutoffTetrio? averages; - - const NerdStatsThingy({super.key, required this.nerdStats, this.oldNerdStats, this.averages}); - - @override - Widget build(BuildContext context) { - return Card( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 256.0, - width: 256.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: SfRadialGauge( - backgroundColor: Colors.black, - axes: [ - RadialAxis( - startAngle: 200, - endAngle: 340, - minimum: 0.0, - maximum: 1.0, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.app, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "APP\n"), - TextSpan(text: f3.format(nerdStats.app), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.app, averages?.nerdStats?.app, true))), - if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.app - oldNerdStats!.app)}", style: TextStyle(color: getDifferenceColor(nerdStats.app - oldNerdStats!.app))), - ] - ))), - angle: 270,positionFactor: 0.5 - ), - ], - ), - RadialAxis( - startAngle: 20, - endAngle: 160, - isInversed: true, - minimum: 1.8, - maximum: 2.4, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.vsapm, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "VS/APM\n"), - TextSpan(text: f3.format(nerdStats.vsapm), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.vsapm, averages?.nerdStats?.vsapm, true))), - if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.vsapm - oldNerdStats!.vsapm)}", style: TextStyle(color: getDifferenceColor(nerdStats.vsapm - oldNerdStats!.vsapm))), - ] - ))), - angle: 90,positionFactor: 0.5 - ) - ], - ) - ] - ), - ), - ), - Expanded( - child: Wrap( - alignment: WrapAlignment.center, - spacing: 10.0, - runSpacing: 10.0, - runAlignment: WrapAlignment.start, - children: [ - GaugetThingy(value: nerdStats.dss, oldValue: oldNerdStats?.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dss), - GaugetThingy(value: nerdStats.dsp, oldValue: oldNerdStats?.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dsp), - GaugetThingy(value: nerdStats.appdsp, oldValue: oldNerdStats?.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.appdsp), - GaugetThingy(value: nerdStats.cheese, oldValue: oldNerdStats?.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2, moreIsBetter: false), - GaugetThingy(value: nerdStats.gbe, oldValue: oldNerdStats?.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.gbe), - GaugetThingy(value: nerdStats.nyaapp, oldValue: oldNerdStats?.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.nyaapp), - GaugetThingy(value: nerdStats.area, oldValue: oldNerdStats?.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1, moreIsBetter: true, avgValue: averages?.nerdStats?.area), - ], - ), - ) - ] - ), - ), - ], - ) - ); - } -} - -class EstTrThingy extends StatelessWidget{ - final EstTr estTr; - - const EstTrThingy({super.key, required this.estTr}); - - @override - Widget build(BuildContext context) { - return const Card( - //child: , - ); - } -} - -class GraphsThingy extends StatelessWidget{ - final double apm; - final double pps; - final double vs; - final NerdStats nerdStats; - final Playstyle playstyle; - - const GraphsThingy({super.key, required this.nerdStats, required this.playstyle, required this.apm, required this.pps, required this.vs}); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Center(child: Graphs(apm, pps, vs, nerdStats, playstyle)), - ), - ); - } - -} - -class GaugetThingy extends StatelessWidget{ - final double? value; - final String? subString; - final double min; - final double max; - final double? oldValue; - final double? avgValue; - final bool moreIsBetter; - final double tickInterval; - final String label; - final double sideSize; - final bool percentileFormat; - final int fractionDigits; - - const GaugetThingy({super.key, required this.value, this.subString, required this.min, required this.max, this.oldValue, this.avgValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter, this.percentileFormat = false}); - - @override - Widget build(BuildContext context) { - NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits); - return ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: SizedBox( - height: sideSize, - width: sideSize, - child: SfRadialGauge( - backgroundColor: Colors.black, - axes: [ - RadialAxis( - radiusFactor: 1.01, - minimum: min, - maximum: max, - showTicks: true, - showLabels: false, - interval: tickInterval, - minorTicksPerInterval: 0, - ranges:[ - GaugeRange(startValue: 0, endValue: (value != null && !value!.isNaN) ? value! : 0, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - Text((value != null && !value!.isNaN) ? percentileFormat ? percentage.format(value) : f.format(value) : "---", textAlign: TextAlign.center, style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: (value != null && !value!.isNaN) ? getStatColor(value!, avgValue, moreIsBetter) : Colors.grey))), - angle: 90,positionFactor: 0.10 - ), - GaugeAnnotation(widget: Container(child: - Text(label, textAlign: TextAlign.center, style: TextStyle(height: .9, color: (value != null && !value!.isNaN) ? null : Colors.grey))), - angle: 270,positionFactor: 0.3, verticalAlignment: GaugeAlignment.far, - ), - if (oldValue != null && (value != null && !value!.isNaN)) GaugeAnnotation(widget: Container(child: - Text(comparef2.format(percentileFormat ? (value!-oldValue!) * 100 : value!-oldValue!), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(moreIsBetter ? value!-oldValue! : oldValue!-value!)))), - angle: 90,positionFactor: 0.45 - ), - if (subString != null) GaugeAnnotation(widget: Container(child: - Text(subString!, textAlign: TextAlign.center, style: TextStyle(color: (value != null && !value!.isNaN) ? null : Colors.grey))), - angle: 90,positionFactor: 0.45 - ) - ], - ) - ] - ), - ), - ); - } -} - -class ZenithThingy extends StatelessWidget{ - final RecordSingle? zenith; - final bool old; - - const ZenithThingy({super.key, required this.zenith, this.old = false}); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - RichText( - text: TextSpan( - text: zenith != null ? "${f2.format(zenith!.stats.zenith!.altitude)} m" : "--- m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: (zenith != null && !old) ? Colors.white : Colors.grey), - ), - ), - if (zenith != null) RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (zenith!.rank != -1) TextSpan(text: "№ ${intf.format(zenith!.rank)}", style: TextStyle(color: getColorOfRank(zenith!.rank))), - if (zenith!.rank != -1) const TextSpan(text: " • "), - if (zenith!.countryRank != -1) TextSpan(text: "№ ${intf.format(zenith!.countryRank)} local", style: TextStyle(color: getColorOfRank(zenith!.countryRank))), - if (zenith!.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(zenith!.timestamp)), - ] - ), - ), - ], - ), - if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) Container(width: 16.0), - if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) for (String mod in (zenith!.extras as ZenithExtras).mods) Image.asset("res/icons/${mod}.png", height: 64.0) - ], - ), - if (zenith != null) Row( - children: [ - Expanded( - child: Center( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(f2.format(zenith!.aggregateStats.apm), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" APM", style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(zenith!.aggregateStats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" PPS", style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(zenith!.aggregateStats.vs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" VS", style: TextStyle(fontSize: 21)), - ]) - ], - ), - ), - ), - GaugetThingy(value: zenith!.stats.cps, min: 0, max: 12, tickInterval: 3, label: "Climb\nSpeed", subString: "Peak: ${f2.format(zenith!.stats.zenith!.peakrank)}", sideSize: 128, fractionDigits: 2, moreIsBetter: true), - Expanded( - child: Center( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(intf.format(zenith!.stats.kills), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" KO's", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - Text(zenith!.stats.topBtB.toString(), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" B2B", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - Text(zenith!.stats.garbage.maxspike_nomult.toString(), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Top spike", style: TextStyle(fontSize: 21)) - ]) - ], - ), - ), - ) - ], - ) else Row( - children: [ - Expanded( - child: Center( - child: Table( - defaultColumnWidth: IntrinsicColumnWidth(), - children: [ - const TableRow(children: [ - Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - Text(" APM", style: TextStyle(fontSize: 21, color: Colors.grey)), - ]), - const TableRow(children: [ - Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - Text(" PPS", style: TextStyle(fontSize: 21, color: Colors.grey)), - ]), - const TableRow(children: [ - Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - Text(" VS", style: TextStyle(fontSize: 21, color: Colors.grey)), - ]) - ], - ), - ), - ), - GaugetThingy(value: null, min: 0, max: 12, tickInterval: 3, label: "Climb\nSpeed", subString: "Peak: ---", sideSize: 128, fractionDigits: 0, moreIsBetter: true), - Expanded( - child: Center( - child: Table( - defaultColumnWidth: IntrinsicColumnWidth(), - children: [ - const TableRow(children: [ - Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - Text(" KO's", style: TextStyle(fontSize: 21, color: Colors.grey)) - ]), - const TableRow(children: [ - Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - Text(" B2B", style: TextStyle(fontSize: 21, color: Colors.grey)) - ]), - const TableRow(children: [ - Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), - Text(" Top spike", style: TextStyle(fontSize: 21, color: Colors.grey)) - ]) - ], - ), - ), - ) - ], - ) - ] - ), - ) - ); - } - -} - -class AlphaLeagueEntryThingy extends StatelessWidget{ - final TetraLeagueAlphaRecord record; - final String userID; - - const AlphaLeagueEntryThingy(this.record, this.userID); - - @override - Widget build(BuildContext context) { - var accentColor = record.endContext.firstWhere((element) => element.userId == userID).success ? Colors.green : Colors.red; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - stops: const [0, 0.05], - colors: [accentColor, Colors.transparent] - ) - ), - child: ListTile( - leading: Text("${record.endContext.firstWhere((element) => element.userId == userID).points} : ${record.endContext.firstWhere((element) => element.userId != userID).points}", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow)), - title: Text("vs. ${record.endContext.firstWhere((element) => element.userId != userID).username}"), - subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey)), - trailing: TrailingStats( - record.endContext.firstWhere((element) => element.userId == userID).secondary, - record.endContext.firstWhere((element) => element.userId == userID).tertiary, - record.endContext.firstWhere((element) => element.userId == userID).extra, - record.endContext.firstWhere((element) => element.userId != userID).secondary, - record.endContext.firstWhere((element) => element.userId != userID).tertiary, - record.endContext.firstWhere((element) => element.userId != userID).extra - ), - //onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: record, initPlayerId: userID))), - ), - ); - } -} - -class BetaLeagueEntryThingy extends StatelessWidget{ - final BetaRecord record; - final String userID; - - const BetaLeagueEntryThingy(this.record, this.userID); - - TextSpan matchResult(String result){ - return switch(result){ - "victory" => TextSpan( - text: "Victory", - style: TextStyle(color: Colors.greenAccent) - ), - "defeat" => TextSpan( - text: "Defeat", - style: TextStyle(color: Colors.redAccent) - ), - "tie" => TextSpan( - text: "Tie", - style: TextStyle(color: Colors.white) - ), - "dqvictory" => TextSpan( - text: "Opponent was DQ'ed", - style: TextStyle(color: Colors.lightGreenAccent) - ), - "dqdefeat" => TextSpan( - text: "Player was DQ'ed", - style: TextStyle(color: Colors.red) - ), - "nocontest" => TextSpan( - text: "No Contest", - style: TextStyle(color: Colors.blueAccent) - ), - "nullified" => TextSpan( - text: "Nullified", - style: TextStyle(color: Colors.purpleAccent) - ), - _ => TextSpan( - text: "${result.toUpperCase()}", - style: TextStyle(color: Colors.orangeAccent) - ) - }; - } - - Color deltaColor(double? delta){ - if (delta == null || delta.isNaN) return Colors.grey; - if (delta.isNegative) return Colors.redAccent; - else return Colors.greenAccent; - } - - @override - Widget build(BuildContext context) { - double? deltaTR = (record.extras.league[userID]?[1]?.tr != null && record.extras.league[userID]?[0]?.tr != null) ? record.extras.league[userID]![1]!.tr - record.extras.league[userID]![0]!.tr : null; - double? deltaGlicko = (record.extras.league[userID]?[1]?.glicko != null && record.extras.league[userID]?[0]?.glicko != null) ? record.extras.league[userID]![1]!.glicko - record.extras.league[userID]![0]!.glicko : null; - double? deltaRD = (record.extras.league[userID]?[1]?.rd != null && record.extras.league[userID]?[0]?.rd != null) ? record.extras.league[userID]![1]!.rd - record.extras.league[userID]![0]!.rd : null; - return Card( - child: ListTile( - title: Row( - children: [ - Text( - "${record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).wins} - ${record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).wins} ", - style: TextStyle(fontSize: 26, height: 0.75, fontWeight: FontWeight.bold), - ), - Text( - "vs.\n${record.enemyUsername}", - style: TextStyle(fontSize: 14, height: 0.8, fontWeight: FontWeight.w100), - ), - ], - ), - subtitle: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - RichText( - text: TextSpan( - style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - matchResult(record.extras.result), - TextSpan( - text: ", ${timestamp(record.ts)}\n" - ), - TextSpan( - text: deltaTR != null ? "${fDiff.format(deltaTR)} TR" : "??? TR", - style: TextStyle( - color: deltaColor(deltaTR) - ) - ), - TextSpan( - text: ", " - ), - TextSpan( - text: deltaGlicko != null ? "${fDiff.format(deltaGlicko)} Glicko" : "??? Glicko", - style: TextStyle( - color: deltaColor(deltaGlicko) - ) - ), - TextSpan( - text: ", " - ), - TextSpan( - text: deltaRD != null ? "${fDiff.format(deltaRD)} RD" : "??? RD", - style: TextStyle( - color: Colors.grey - ) - ), - ] - ) - ), - ], - ), - ), - trailing: TrailingStats( - record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.apm, - record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.pps, - record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.vs, - record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.apm, - record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.pps, - record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.vs, - ), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: record, initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), - ), - ); - } - -} - -class TLRecords extends StatelessWidget { - final String userID; - - /// Widget, that displays Tetra League records. - /// Accepts list of TL records ([data]) and [userID] of player from the view - const TLRecords(this.userID); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: teto.fetchTLStream(userID), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - return Column( - children: [ - for (BetaRecord record in snapshot.data!.records) BetaLeagueEntryThingy(record, userID) - ], - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - }, - ); - } -} - -class TLRatingThingy extends StatelessWidget{ - final String userID; - final TetraLeague tlData; - final TetraLeague? oldTl; - final double? topTR; - final bool? showPositions; - final DateTime? lastMatchPlayed; - - const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed, this.showPositions}); - - @override - Widget build(BuildContext context) { - bool oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; - bool bigScreen = MediaQuery.of(context).size.width >= 768; - String decimalSeparator = f4.symbols.DECIMAL_SEP; - List formatedTR = f4.format(tlData.tr).split(decimalSeparator); - List formatedGlicko = tlData.glicko != null ? f4.format(tlData.glicko).split(decimalSeparator) : ["---","--"]; - List formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); - //DateTime now = DateTime.now(); - //bool beforeS1end = now.isBefore(seasonEnd); - //int daysLeft = seasonEnd.difference(now).inDays; - //int safeRD = min(100, (100 + ((tlData.rd! >= 100 && tlData.decaying) ? 7 : max(0, 7 - (lastMatchPlayed != null ? now.difference(lastMatchPlayed!).inDays : 7))) - daysLeft).toInt()); - return Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceAround, - crossAxisAlignment: WrapCrossAlignment.center, - clipBehavior: Clip.hardEdge, - children: [ - (userID == "5e32fc85ab319c2ab1beb07c" && oskKagariGimmick) // 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/${tlData.rank}.png", height: 128), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white, height: 0.9), - children: (tlData.gamesPlayed > 9) ? switch(prefs.getInt("ratingMode")){ - 1 => [ - TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (formatedGlicko.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedGlicko[1]), - TextSpan(text: " Glicko", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - ], - 2 => [ - TextSpan(text: "${t.top} ${formatedPercentile[0]}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (formatedPercentile.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedPercentile[1]), - TextSpan(text: " %", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - ], - _ => [ - TextSpan(text: formatedTR[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (formatedTR.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedTR[1]), - TextSpan(text: " TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - ], - } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),] - ) - ), - if (oldTl != null) RichText( - textAlign: TextAlign.center, - softWrap: true, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan(text: switch(prefs.getInt("ratingMode")){ - 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", - 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", - _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" - }, - style: TextStyle( - color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ - 1 => tlData.glicko! - oldTl!.glicko!, - 2 => tlData.percentile - oldTl!.percentile, - _ => tlData.tr - oldTl!.tr - }) - ), - ), - const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), - TextSpan(text: switch(prefs.getInt("ratingMode")){ - 1 => "${fDiff.format(tlData.tr - oldTl!.tr)} TR", - _ => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko" - }, - style: TextStyle( - color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ - 1 => tlData.tr - oldTl!.tr, - _ => tlData.glicko! - oldTl!.glicko! - }) - ), - ), - const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), - TextSpan( - text: "${fDiff.format(tlData.rd! - oldTl!.rd!)} RD", - style: TextStyle(color: getDifferenceColor(oldTl!.rd! - tlData.rd!)) - ) - ], - ), - ), - if (tlData.gamesPlayed > 9) Column( - children: [ - RichText( - textAlign: TextAlign.center, - softWrap: true, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan(text: prefs.getInt("ratingMode") == 2 ? "${f2.format(tlData.tr)} TR • % ${t.rank}: ${tlData.percentileRank.toUpperCase()}" : "${t.top} ${f2.format(tlData.percentile * 100)}% (${tlData.percentileRank.toUpperCase()})"), - if (tlData.bestRank != "z") const TextSpan(text: " • "), - if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"), - if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"), - TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.tr)} TR • RD: " : "Glicko: ${tlData.glicko != null ? f2.format(tlData.glicko) : "---"}±"}"), - TextSpan(text: f2.format(tlData.rd!), style: tlData.decaying ? TextStyle(color: tlData.rd! > 98 ? Colors.red : Colors.yellow) : null), - if (tlData.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: tlData.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - //if (beforeS1end) tlData.rd! <= safeRD ? TextSpan(text: " (Safe)", style: TextStyle(color: Colors.greenAccent)) : TextSpan(text: " (> ${safeRD} RD !!!)", style: TextStyle(color: Colors.redAccent)) - ], - ), - ), - ], - ), - if (showPositions == true) RichText( - textAlign: TextAlign.start, - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (tlData.standing != -1) TextSpan(text: "№ ${intf.format(tlData.standing)}", style: TextStyle(color: getColorOfRank(tlData.standing))), - if (tlData.standing != -1 || tlData.standingLocal != -1) const TextSpan(text: " • "), - if (tlData.standingLocal != -1) TextSpan(text: "№ ${intf.format(tlData.standingLocal)} local", style: TextStyle(color: getColorOfRank(tlData.standingLocal))), - if (tlData.standing != -1 && tlData.standingLocal != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(tlData.timestamp)), - ] - ), - ), - ], - ), - ], - ); - } -} - -class FutureError extends StatelessWidget{ - final AsyncSnapshot snapshot; - - FutureError(this.snapshot); - - @override - Widget build(BuildContext context) { - return TweenAnimationBuilder( - duration: Durations.medium3, - tween: Tween(begin: 0, end: 1), - curve: Easing.standard, - builder: (context, value, child) { - return Container( - transform: Matrix4.translationValues(0, 50-value*50, 0), - child: Opacity(opacity: value, child: child), - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Icon(Icons.error_outline, size: 128.0, color: Colors.red, shadows: [ - Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red), - Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red), - ]), - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.left, style: TextStyle(fontFamily: "Monospace")), - ), - Spacer() - ], - ), - ); - } -} - -class ErrorThingy extends StatelessWidget{ - final FetchResults? data; - final String? eText; - - const ErrorThingy({this.data, this.eText}); - - @override - Widget build(BuildContext context) { - IconData icon = Icons.error_outline; - String errText = eText??""; - String? subText; - if (data?.exception != null) switch (data!.exception!.runtimeType){ - case TetrioPlayerNotExist: - icon = Icons.search_off; - errText = t.errors.noSuchUser; - subText = t.errors.noSuchUserSub; - break; - case TetrioDiscordNotExist: - icon = Icons.search_off; - errText = t.errors.discordNotAssigned; - subText = t.errors.discordNotAssignedSub; - case ConnectionIssue: - var err = data!.exception as ConnectionIssue; - errText = t.errors.connection(code: err.code, message: err.message); - break; - case TetrioForbidden: - icon = Icons.remove_circle; - errText = t.errors.forbidden; - subText = t.errors.forbiddenSub(nickname: 'osk'); - break; - case TetrioTooManyRequests: - errText = t.errors.tooManyRequests; - subText = t.errors.tooManyRequestsSub; - break; - case TetrioOskwareBridgeProblem: - errText = t.errors.oskwareBridge; - subText = t.errors.oskwareBridgeSub; - break; - case TetrioInternalProblem: - errText = kIsWeb ? t.errors.internalWebVersion : t.errors.internal; - subText = kIsWeb ? t.errors.internalWebVersionSub : t.errors.internalSub; - break; - case ClientException: - errText = t.errors.clientException; - break; - default: - errText = data!.exception.toString(); - } - return TweenAnimationBuilder( - duration: Durations.medium3, - tween: Tween(begin: 0, end: 1), - curve: Easing.standard, - builder: (context, value, child) { - return Container( - transform: Matrix4.translationValues(0, 50-value*50, 0), - child: Opacity(opacity: value, child: child), - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Icon(icon, size: 128.0, color: Colors.red, shadows: [ - Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red), - Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red), - ]), - Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - if (subText != null) Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(subText, textAlign: TextAlign.center), - ), - Spacer() - ], - ), - ); - } -} - -class InfoThingy extends StatelessWidget{ - final String info; - - const InfoThingy(this.info); - - @override - Widget build(BuildContext context) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), - SizedBox(height: 5.0), - Text(info, textAlign: TextAlign.center), - ], - )); - } - -} \ No newline at end of file diff --git a/lib/views/mathes_view.dart b/lib/views/mathes_view.dart deleted file mode 100644 index fa709c8..0000000 --- a/lib/views/mathes_view.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/main.dart' show teto; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:window_manager/window_manager.dart'; - -late String oldWindowTitle; - -class MatchesView extends StatefulWidget { - final String userID; - final String username; - const MatchesView({super.key, required this.userID, required this.username}); - - @override - State createState() => MatchesState(); -} - -class MatchesState extends State { - - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.matchesViewTitle(nickname: widget.username)}"); - } - super.initState(); - } - - @override - void dispose(){ - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - bool bigScreen = MediaQuery.of(context).size.width > 768; - final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); - return Scaffold( - appBar: AppBar( - title: Text(t.matchesViewTitle(nickname: widget.username)), - ), - backgroundColor: Colors.black, - body: SafeArea( - child: FutureBuilder( - future: teto.getTLMatchesbyPlayerID(widget.userID), - builder: (context, snapshot){ - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator(color: Colors.white)); - case ConnectionState.done: - return ListView( - physics: const AlwaysScrollableScrollPhysics(), - children: (snapshot.data!.isNotEmpty) - ? [for (var value in snapshot.data!) ListTile( - leading: Text("${value.endContext.firstWhere((element) => element.userId == widget.userID).points} : ${value.endContext.firstWhere((element) => element.userId != widget.userID).points}", - style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : - const TextStyle(fontSize: 28)), - title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != widget.userID).username}"), - subtitle: Text(dateFormat.format(value.timestamp)), - trailing: IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: () { - DateTime nn = value.timestamp; - teto.deleteTLMatch(value.ownId).then((value) => setState(() { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.matchRemoved(date: dateFormat.format(nn))))); - })); - }, - ), - )] - : [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))], - ); - } - } - ) - ) - ); - } -} diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart deleted file mode 100644 index a0422f4..0000000 --- a/lib/views/rank_averages_view.dart +++ /dev/null @@ -1,554 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio_constants.dart'; -import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; -import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/views/main_view.dart' show MainView; -import 'package:window_manager/window_manager.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; - -var _chartsShortTitlesDropdowns = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; -Stats _chartsX = Stats.tr; -Stats _chartsY = Stats.apm; -late TooltipBehavior _tooltipBehavior; -late ZoomPanBehavior _zoomPanBehavior; -List _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; -List<_MyScatterSpot> _spots = []; -Stats _sortBy = Stats.tr; -late List they; -bool _reversed = false; -List _itemCountries = [for (MapEntry e in t.countries.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; -String _country = ""; -late String _oldWindowTitle; -final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); -final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); - -class RankView extends StatefulWidget { - final List rank; - const RankView({super.key, required this.rank}); - - @override - State createState() => RankState(); -} - -class RankState extends State with SingleTickerProviderStateMixin { - late ScrollController _scrollController; - late TabController _tabController; - late String previousAxisTitles; - late double minX; - late double actualMinX; - late double maxX; - late double actualMaxX; - late double minY; - late double actualMinY; - late double maxY; - late double actualMaxY; - late double xScale; - late double yScale; - String headerTooltip = t.pseudoTooltipHeaderInit; - String footerTooltip = t.pseudoTooltipFooterInit; - ValueNotifier hoveredPointId = ValueNotifier(-1); - double scaleFactor = 5e2; - double dragFactor = 7e2; - - @override - void initState() { - _scrollController = ScrollController(); - _tabController = TabController(length: 6, vsync: this); - _zoomPanBehavior = ZoomPanBehavior( - enablePinching: true, - enableSelectionZooming: true, - enableMouseWheelZooming : true, - enablePanning: true, - ); - _tooltipBehavior = TooltipBehavior( - color: Colors.black, - borderColor: Colors.white, - enable: true, - animationDuration: 0, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "${data.nickname} (${data.rank.toUpperCase()})", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20), - ), - ), - Text('${_f4.format(data.x)} ${chartsShortTitles[_chartsX]}\n${_f4.format(data.y)} ${chartsShortTitles[_chartsY]}') - ], - ), - ); - } - ); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => _oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}"); - } - super.initState(); - previousAxisTitles = _chartsX.toString()+_chartsY.toString(); - they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); - createSpots(); - } - - void createSpots(){ - _spots = [ - for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"]) - if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception - _MyScatterSpot( - entry.getStatByEnum(_chartsX).toDouble(), - entry.getStatByEnum(_chartsY).toDouble(), - entry.userId, - entry.username, - entry.rank, - rankColors[entry.rank]??Colors.white - ) - ]; - } - - @override - void dispose() { - _tabController.dispose(); - _scrollController.dispose(); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(_oldWindowTitle); - super.dispose(); - } - - void _justUpdate() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - bool bigScreen = MediaQuery.of(context).size.width > 768; - if (previousAxisTitles != _chartsX.toString()+_chartsY.toString()){ - createSpots(); - previousAxisTitles = _chartsX.toString()+_chartsY.toString(); - } - final t = Translations.of(context); - //they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); - return Scaffold( - appBar: AppBar( - title: Text(widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())), - ), - backgroundColor: Colors.black, - body: SafeArea( - child: NestedScrollView( - controller: _scrollController, - headerSliverBuilder: (context, value) { - return [ SliverToBoxAdapter( - child: Column( - children: [ - Flex( - direction: Axis.vertical, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - alignment: Alignment.topCenter, - children: [Image.asset("res/tetrio_tl_alpha_ranks/${widget.rank[0].rank}.png",fit: BoxFit.fitHeight,height: 128), ], - ), - Flexible( - child: Column( - children: [ - Text( - widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase()), - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - Text( - t.players(n: widget.rank[1]["entries"].length), - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ], - )), - ], - ), - ], - )), - SliverToBoxAdapter( - child: TabBar( - controller: _tabController, - isScrollable: true, - tabs: [ - Tab(text: t.chart), - Tab(text: t.entries), - Tab(text: t.minimums), - Tab(text: t.averages), - Tab(text: t.maximums), - Tab(text: t.other), - ], - )), - ]; - }, - body: TabBarView( - controller: _tabController, - children: [ - Column( - children: [ - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.end, - spacing: 20, - children: [ - Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Text("X:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: _chartsShortTitlesDropdowns, - value: _chartsX, - onChanged: (value) { - _chartsX = value; - _justUpdate(); - }), - ], - ), - ], - ), - Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Text("Y:", style: TextStyle(fontSize: 22)), - ), - DropdownButton( - items: _chartsShortTitlesDropdowns, - value: _chartsY, - onChanged: (value) { - _chartsY = value; - _justUpdate(); - }), - ], - ), - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - if (widget.rank[1]["entries"].length > 1) - SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height - 104, - child: Padding( - padding: bigScreen ? const EdgeInsets.fromLTRB(40, 10, 40, 20) : const EdgeInsets.fromLTRB(0, 10, 16, 20), - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerSignal: (signal) { - if (signal is PointerScrollEvent) { - setState(() { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView - }); - } - }, - child: SfCartesianChart( - tooltipBehavior: _tooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - //primaryXAxis: CategoryAxis(), - series: [ - ScatterSeries( - enableTooltip: true, - dataSource: _spots, - animationDuration: 0, - pointColorMapper: (data, _) => data.color, - xValueMapper: (data, _) => data.x, - yValueMapper: (data, _) => data.y, - onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: _spots[point.pointIndex!].nickname), maintainState: false)), - ) - ], - ), - ), - )) - else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) - ], - ), - Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 16, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.sortBy}: ", style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton( - items: _itemStats, - value: _sortBy, - onChanged: ((value) { - _sortBy = value; - setState(() { - they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); - }); - }), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.reversed}: ", style: const TextStyle(color: Colors.white, fontSize: 25)), - Padding(padding: const EdgeInsets.fromLTRB(0, 5.5, 0, 7.5), - child: Checkbox( - value: _reversed, - checkColor: Colors.black, - onChanged: ((value) { - _reversed = value!; - setState(() { - they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); - }); - }), - ), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.country}: ", style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton( - items: _itemCountries, - value: _country, - onChanged: ((value) { - _country = value; - setState(() { - they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); - }); - }), - ), - ], - ), - ], - ), - ), - Expanded( - child: ListView.builder( - itemCount: they.length, - itemBuilder: (context, index) { - bool bigScreen = MediaQuery.of(context).size.width > 768; - return ListTile( - title: Text(they[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - subtitle: Text( - _sortBy == Stats.tr ? "${_f2.format(they[index].apm)} APM, ${_f2.format(they[index].pps)} PPS, ${_f2.format(they[index].vs)} VS, ${_f2.format(they[index].nerdStats.app)} APP, ${_f2.format(they[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(they[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", - style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${_f2.format(they[index].tr)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null), - Image.asset("res/tetrio_tl_alpha_ranks/${they[index].rank}.png", height: bigScreen ? 48 : 16), - ], - ), - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: they[index].username), maintainState: false)); - }, - ); - }), - ) - ], - ), - Column( - children: [ - Text(t.lowestValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Expanded( - child: ListView( - children: [ - _ListEntry(value: widget.rank[1]["lowestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestTRid"], username: widget.rank[1]["lowestTRnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestGlixare"], label: "Glixare", id: widget.rank[1]["lowestGlixareID"], username: widget.rank[1]["lowestGlixareNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["lowestS1trID"], username: widget.rank[1]["lowestS1trNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestGlicko"], label: "Glicko", id: widget.rank[1]["lowestGlickoID"], username: widget.rank[1]["lowestGlickoNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestRdID"], username: widget.rank[1]["lowestRdNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGamesPlayedID"], username: widget.rank[1]["lowestGamesPlayedNick"], approximate: false), - _ListEntry(value: widget.rank[1]["lowestGamesWon"], label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGamesWonID"], username: widget.rank[1]["lowestGamesWonNick"], approximate: false), - _ListEntry(value: widget.rank[1]["lowestWinrate"] * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestWinrateID"], username: widget.rank[1]["lowestWinrateNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestAPM"], label: t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPMid"], username: widget.rank[1]["lowestAPMnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestPPS"], label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestPPSid"], username: widget.rank[1]["lowestPPSnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestVS"], label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestVSid"], username: widget.rank[1]["lowestVSnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPPid"], username: widget.rank[1]["lowestAPPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestVSAPM"], label: "VS / APM", id: widget.rank[1]["lowestVSAPMid"], username: widget.rank[1]["lowestVSAPMnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestDSSid"], username: widget.rank[1]["lowestDSSnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestDSPid"], username: widget.rank[1]["lowestDSPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPPDSPid"], username: widget.rank[1]["lowestAPPDSPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestCheeseID"], username: widget.rank[1]["lowestCheeseNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGBEid"], username: widget.rank[1]["lowestGBEnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestNyaAPPid"], username: widget.rank[1]["lowestNyaAPPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestArea"], label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAreaID"], username: widget.rank[1]["lowestAreaNick"], approximate: false, fractionDigits: 1), - _ListEntry(value: widget.rank[1]["lowestEstTR"], label: t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestEstTRid"], username: widget.rank[1]["lowestEstTRnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["lowestEstAcc"], label: t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestEstAccID"], username: widget.rank[1]["lowestEstAccNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestOpener"], label: "Opener", id: widget.rank[1]["lowestOpenerID"], username: widget.rank[1]["lowestOpenerNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestPlonk"], label: "Plonk", id: widget.rank[1]["lowestPlonkID"], username: widget.rank[1]["lowestPlonkNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestStride"], label: "Stride", id: widget.rank[1]["lowestStrideID"], username: widget.rank[1]["lowestStrideNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["lowestInfDS"], label: "Inf. DS", id: widget.rank[1]["lowestInfDSid"], username: widget.rank[1]["lowestInfDSnick"], approximate: false, fractionDigits: 3) - ], - ), - ), - ], - ), - Column( - children: [ - Text(t.averageValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Expanded( - child: ListView(children: [ - _ListEntry(value: widget.rank[0].tr, label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[0].gxe, label: "Glixare", id: "", username: "", approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[0].s1tr, label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: "", username: "", approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[0].glicko, label: "Glicko", id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[0].rd, label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[0].gamesPlayed, label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 0), - _ListEntry(value: widget.rank[0].gamesWon, label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 0), - _ListEntry(value: widget.rank[0].winrate * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[0].apm, label: t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[0].pps, label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[0].vs, label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["avgAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgVSAPM"], label: "VS / APM", id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["avgGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgArea"], label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 1), - _ListEntry(value: widget.rank[1]["avgEstTR"], label: t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["avgEstAcc"], label: t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgOpener"], label: "Opener", id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgPlonk"], label: "Plonk", id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgStride"], label: "Stride", id: "", username: "", approximate: true, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["avgInfDS"], label: "Inf. DS", id: "", username: "", approximate: true, fractionDigits: 3), - ])) - ], - ), - Column( - children: [ - Text(t.highestValues, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Expanded( - child: ListView( - children: [ - _ListEntry(value: widget.rank[1]["highestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestTRid"], username: widget.rank[1]["highestTRnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestGlixare"], label: "Glixare", id: widget.rank[1]["highestGlixareID"], username: widget.rank[1]["highestGlixareNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["highestS1trID"], username: widget.rank[1]["highestS1trNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestGlicko"], label: "Glicko", id: widget.rank[1]["highestGlickoID"], username: widget.rank[1]["highestGlickoNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestRdID"], username: widget.rank[1]["highestRdNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGamesPlayedID"], username: widget.rank[1]["highestGamesPlayedNick"], approximate: false), - _ListEntry(value: widget.rank[1]["highestGamesWon"], label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGamesWonID"], username: widget.rank[1]["highestGamesWonNick"], approximate: false), - _ListEntry(value: widget.rank[1]["highestWinrate"] * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestWinrateID"], username: widget.rank[1]["highestWinrateNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestAPM"], label: t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPMid"], username: widget.rank[1]["highestAPMnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestPPS"], label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestPPSid"], username: widget.rank[1]["highestPPSnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestVS"], label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestVSid"], username: widget.rank[1]["highestVSnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPPid"], username: widget.rank[1]["highestAPPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestVSAPM"], label: "VS / APM", id: widget.rank[1]["highestVSAPMid"], username: widget.rank[1]["highestVSAPMnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestDSSid"], username: widget.rank[1]["highestDSSnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestDSPid"], username: widget.rank[1]["highestDSPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPPDSPid"], username: widget.rank[1]["highestAPPDSPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestCheeseID"], username: widget.rank[1]["highestCheeseNick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGBEid"], username: widget.rank[1]["highestGBEnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestNyaAPPid"], username: widget.rank[1]["highestNyaAPPnick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestArea"], label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAreaID"], username: widget.rank[1]["highestAreaNick"], approximate: false, fractionDigits: 1), - _ListEntry(value: widget.rank[1]["highestEstTR"], label: t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestEstTRid"], username: widget.rank[1]["highestEstTRnick"], approximate: false, fractionDigits: 2), - _ListEntry(value: widget.rank[1]["highestEstAcc"], label: t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestEstAccID"], username: widget.rank[1]["highestEstAccNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestOpener"], label: "Opener", id: widget.rank[1]["highestOpenerID"], username: widget.rank[1]["highestOpenerNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestPlonk"], label: "Plonk", id: widget.rank[1]["highestPlonkID"], username: widget.rank[1]["highestPlonkNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestStride"], label: "Stride", id: widget.rank[1]["highestStrideID"], username: widget.rank[1]["highestStrideNick"], approximate: false, fractionDigits: 3), - _ListEntry(value: widget.rank[1]["highestInfDS"], label: "Inf. DS", id: widget.rank[1]["highestInfDSid"], username: widget.rank[1]["highestInfDSnick"], approximate: false, fractionDigits: 3), - ], - ), - ) - ], - ), - Column( - children: [ - Expanded( - child: ListView(children: [ - _ListEntry(value: widget.rank[1]["totalGamesPlayed"], label: t.statCellNum.totalGames, id: "", username: "", approximate: true, fractionDigits: 0), - _ListEntry(value: widget.rank[1]["totalGamesWon"], label: t.statCellNum.totalWon, id: "", username: "", approximate: true, fractionDigits: 0), - _ListEntry(value: (widget.rank[1]["totalGamesWon"] / widget.rank[1]["totalGamesPlayed"]) * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3), - ])) - ], - ), - ], - )))); - } -} - -class _ListEntry extends StatelessWidget { - final num value; - final String label; - final String id; - final String username; - final bool approximate; - final int? fractionDigits; - const _ListEntry( - {required this.value, - required this.label, - this.fractionDigits, - required this.id, - required this.username, - required this.approximate}); - - @override - Widget build(BuildContext context) { - NumberFormat f = NumberFormat.decimalPatternDigits( - locale: LocaleSettings.currentLocale.languageCode, - decimalDigits: fractionDigits ?? 0); - return ListTile( - title: Text(label), - trailing: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(f.format(value), - style: const TextStyle(fontSize: 22, height: 0.9)), - if (id.isNotEmpty) Text(t.forPlayer(username: username), style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.w100),) - ], - ), - onTap: id.isNotEmpty - ? () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MainView(player: id), - maintainState: false, - ), - ); - } - : null, - ); - } -} - -class _MyScatterSpot{ - num x; - num y; - String id; - String nickname; - String rank; - Color color; - _MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); -} diff --git a/lib/views/rank_view.dart b/lib/views/rank_view.dart index 1249376..826d0fd 100644 --- a/lib/views/rank_view.dart +++ b/lib/views/rank_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; class RankView extends StatefulWidget { final String rank; diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart deleted file mode 100644 index a368d35..0000000 --- a/lib/views/ranks_averages_view.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; -import 'package:tetra_stats/data_objects/tetrio_constants.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:tetra_stats/main.dart' show teto; - -class RankAveragesView extends StatefulWidget { - const RankAveragesView({super.key}); - - @override - State createState() => RanksAverages(); -} - -late String oldWindowTitle; - -class RanksAverages extends State { - - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.rankAveragesViewTitle}"); - } - super.initState(); - } - - @override - void dispose() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(t.rankAveragesViewTitle), - ), - backgroundColor: Colors.black, - body: SafeArea( - child: FutureBuilder(future: teto.fetchCutoffsTetrio(), builder: (context, snapshot){ - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator(color: Colors.white)); - case ConnectionState.done: - if (snapshot.hasData){ - return Container( - alignment: Alignment.center, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Container( - alignment: Alignment.center, - width: 900, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - border: TableBorder.all(color: Colors.grey.shade900), - columnWidths: const { - 0: FixedColumnWidth(48), - 1: FixedColumnWidth(155), - 2: FixedColumnWidth(150), - 3: FixedColumnWidth(90), - 4: FixedColumnWidth(130), - }, - children: [ - TableRow( - children: [ - Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), - ), - ] - ), - for (String rank in snapshot.data!.data.keys) TableRow( - decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])), - children: [ - Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} APP\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} VS/APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: RichText( - textAlign: TextAlign.right, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow), - children: [ - TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)), - TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)), - TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null)) - ] - )) - ), - ] - ) - ], - ), - Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))) - ], - ), - ), - ), - ), - ); - } - if (snapshot.hasError){ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - if (snapshot.stackTrace != null) Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), - ), - ], - ) - ); - } - return const Text("end of FutureBuilder"); - } - }) - ), - ); - } -} diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart deleted file mode 100644 index 6734b14..0000000 --- a/lib/views/settings_view.dart +++ /dev/null @@ -1,302 +0,0 @@ -import 'dart:io'; -import 'package:go_router/go_router.dart'; -import 'package:tetra_stats/data_objects/tetrio_player.dart'; -import 'package:tetra_stats/main.dart' show packageInfo, teto, prefs; -import 'package:file_selector/file_selector.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/services/crud_exceptions.dart'; -import 'package:tetra_stats/utils/open_in_browser.dart'; -import 'package:window_manager/window_manager.dart'; - -late String oldWindowTitle; -TextStyle subtitleStyle = const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey); - -class SettingsView extends StatefulWidget { - const SettingsView({super.key}); - - @override - State createState() => SettingsState(); -} - -class SettingsState extends State { - String defaultNickname = "Checking..."; - late bool showPositions; - late bool updateInBG; - final TextEditingController _playertext = TextEditingController(); - - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.settings}"); - } - _getPreferences(); - super.initState(); - } - - @override - void dispose(){ - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - void _getPreferences() { - showPositions = prefs.getBool("showPositions") ?? false; - updateInBG = prefs.getBool("updateInBG") ?? false; - _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); - } - - Future _removePlayer() async { - await prefs.remove('player'); - await _setDefaultNickname("6098518e3d5155e6ec429cdc"); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - List>? locales = >[]; - for (var v in AppLocale.values){ - locales.add(DropdownMenuItem( - value: v, child: Text(t.locales[v.languageTag]!))); - } - return Scaffold( - appBar: AppBar( - title: Text(t.settings), - ), - backgroundColor: Colors.black, - body: SafeArea( - child: ListView( - children: [ - ListTile( - title: Text(t.exportDB), - subtitle: Text(t.exportDBDescription, style: subtitleStyle), - onTap: () { - if (kIsWeb){ - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); - } else if (Platform.isAndroid){ - var downloadFolder = Directory("/storage/emulated/0/Download"); - File exportedDB = File("${downloadFolder.path}/TetraStats.db"); - getApplicationDocumentsDirectory().then((value) { - exportedDB.writeAsBytes(File("${value.path}/TetraStats.db").readAsBytesSync()); - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.androidExportAlertTitle, - style: const TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: [Text(t.androidExportText(exportedDB: exportedDB))]), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - )); - }); - } else if (Platform.isLinux || Platform.isWindows) { - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.desktopExportAlertTitle, - style: const TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: [ - Text(t.desktopExportText) - ]), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - )); - } - }, - ), - ListTile( - title: Text(t.importDB), - subtitle: Text(t.importDBDescription, style: subtitleStyle), - onTap: () { - if (kIsWeb){ - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb))); - }else if(Platform.isAndroid){ - FilePicker.platform.pickFiles( - type: FileType.any, - ).then((value){ - if (value != null){ - var newDB = value.paths[0]!; - teto.close().then((value){ - if(!newDB.endsWith("db")){ - return ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importWrongFileType))); - } - getApplicationDocumentsDirectory().then((value){ - var oldDB = File("${value.path}/TetraStats.db"); - oldDB.writeAsBytes(File(newDB).readAsBytesSync(), flush: true).then((value){ - teto.open(); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importSuccess))); - }); - }); - }); - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importCancelled))); - } - }); - }else{ - const XTypeGroup typeGroup = XTypeGroup( - label: 'Tetra Stats Database', - extensions: ['db'], - ); - openFile(acceptedTypeGroups: [typeGroup]).then((value){ - if (value != null){ - var newDB = value.path; - teto.close().then((value){ - getApplicationDocumentsDirectory().then((value){ - var oldDB = File("${value.path}/TetraStats.db"); - oldDB.writeAsBytes(File(newDB).readAsBytesSync()).then((value){ - teto.open(); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importSuccess))); - }); - }); - }); - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importCancelled))); - } - }); - } - }, - ), - ListTile( - title: Text(t.yourID), - subtitle: Text(t.yourIDText, style: subtitleStyle), - trailing: Text(defaultNickname), - onTap: () => showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.yourIDAlertTitle, - style: const TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: [ - Text(t.yourIDText), - TextField(controller: _playertext, maxLength: 25) - ]), - ), - actions: [ - TextButton( - child: Text(t.popupActions.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(t.popupActions.submit), - onPressed: () async { - if (_playertext.text.isEmpty) { - _removePlayer(); - Navigator.of(context).pop(); - return; - } - late TetrioPlayer user; - try{ - user = await teto.fetchPlayer(_playertext.text.toLowerCase().trim()); - }on Exception{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.noSuchUser))); - return; - } - _setPlayer(user.userId); - if (context.mounted) Navigator.of(context).pop(); - setState(() {}); - }, - ) - ], - )), - ), - ListTile( - title: Text(t.language), - subtitle: Text("By default, the system language will be selected (if available among Tetra Stats locales, otherwise English)", style: subtitleStyle), - trailing: DropdownButton( - items: locales, - value: LocaleSettings.currentLocale, - onChanged: (value){ - LocaleSettings.setLocale(value!); - if(value.languageCode == Platform.localeName.substring(0, 2)){ - prefs.remove('locale'); - }else{ - prefs.setString('locale', value.languageCode); - } - }, - ), - ), - ListTile(title: Text(t.customization), - subtitle: Text(t.customizationDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), - trailing: const Icon(Icons.arrow_right), - onTap: () { - context.go("/settings/customization"); - },), - ListTile(title: Text(t.updateInBackground), - subtitle: Text(t.updateInBackgroundDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), - trailing: Switch(value: updateInBG, onChanged: (bool value){ - prefs.setBool("updateInBG", value); - setState(() { - updateInBG = value; - }); - }),), - ListTile(title: Text(t.lbStats), - subtitle: Text(t.lbStatsDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), - trailing: Switch(value: showPositions, onChanged: (bool value){ - prefs.setBool("showPositions", value); - setState(() { - showPositions = value; - }); - }),), - const Divider(), - ListTile( - onTap: (){ - launchInBrowser(Uri.https("github.com", "dan63047/TetraStats")); - }, - title: Text(t.aboutApp, style: const TextStyle(fontWeight: FontWeight.w500),), - subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)), - trailing: const Icon(Icons.arrow_right) - ), - // Wrap( - // alignment: WrapAlignment.center, - // spacing: 8, - // children: [ - // TextButton(child: Text("Donate to me"), onPressed: (){},),TextButton(child: Text("Donate to NOT me"), onPressed: (){},),TextButton(child: Text("Donate to someone else"), onPressed: (){},), - // ], - // ), - ], - )), - ); - } -} diff --git a/lib/views/sprint_and_blitz_averages.dart b/lib/views/sprint_and_blitz_averages.dart index 0fd8630..4ed9a99 100644 --- a/lib/views/sprint_and_blitz_averages.dart +++ b/lib/views/sprint_and_blitz_averages.dart @@ -40,8 +40,14 @@ class SprintAndBlitzState extends State { final t = Translations.of(context); bool bigScreen = MediaQuery.of(context).size.width >= 368; return Scaffold( - appBar: AppBar( - title: Text(t.sprintAndBlitsViewTitle), + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + floatingActionButton: Padding( + padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0), + child: FloatingActionButton( + onPressed: () => Navigator.pop(context), + tooltip: 'Fuck go back', + child: const Icon(Icons.arrow_back), + ), ), backgroundColor: Colors.black, body: SafeArea( diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart index 2e70d2f..45fab6a 100644 --- a/lib/views/state_view.dart +++ b/lib/views/state_view.dart @@ -49,7 +49,7 @@ class StateState extends State { ), backgroundColor: Colors.black, body: SafeArea( - child: TLThingy(tl: widget.state, userID: widget.state.id, states: []) + child: TetraLeagueThingy(league: widget.state) ) ); } diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart deleted file mode 100644 index 021adf9..0000000 --- a/lib/views/states_view.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetra_league.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart' show teto; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/views/mathes_view.dart'; -import 'package:tetra_stats/views/state_view.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:window_manager/window_manager.dart'; - -class StatesView extends StatefulWidget { - final String nickname; - final String id; - const StatesView({required this.nickname, required this.id, super.key}); - - @override - State createState() => StatesState(); -} - -late String oldWindowTitle; - -class StatesState extends State { - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - //windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}"); - } - super.initState(); - } - - @override - void dispose() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return Scaffold( - appBar: AppBar( - title: Text(t.statesViewTitle(number: "", nickname: widget.nickname)), - actions: [ - IconButton( - onPressed: (){ - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MatchesView(userID: widget.id, username: widget.nickname), - ), - ); - }, icon: const Icon(Icons.list), tooltip: t.viewAllMatches) - ], - ), - backgroundColor: Colors.black, - body: SafeArea( - child: FutureBuilder>(future: teto.getStates(widget.id), builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator(color: Colors.white)); - case ConnectionState.done: - if (snapshot.hasData) { - return ListView.builder( - itemCount: snapshot.data!.length, - prototypeItem: ListTile( - title: Text(""), - subtitle: Text("", style: TextStyle(color: Colors.grey)), - trailing: IconButton(icon: const Icon(Icons.delete_forever), onPressed: (){}), - ), - itemBuilder: (context, index) { - return ListTile( - title: Text(timestamp(snapshot.data![index].timestamp)), - subtitle: Text( - t.statesViewEntry(level: f2.format(snapshot.data![index].tr), games: intf.format(snapshot.data![index].gamesPlayed), glicko: snapshot.data![index].glicko != null ? f2.format(snapshot.data![index].glicko) : "---", rd: snapshot.data![index].rd != null ? f2.format(snapshot.data![index].rd) : "--"), - style: TextStyle(color: Colors.grey), - ), - trailing: IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: () { - teto.deleteState(snapshot.data![index].id+snapshot.data![index].timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(snapshot.data![index].timestamp))))); - })); - }, - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StateView(state: snapshot.data![index]), - ), - ); - }, - ); - }); - } else if (snapshot.hasError) { - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), - ), - ], - ) - ); - } - break; - } - return const Center(child: Text('default case of FutureBuilder', style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center)); - } - )));} - } diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart deleted file mode 100644 index 3c6f41d..0000000 --- a/lib/views/tl_leaderboard_view.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio_constants.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart'; -import 'package:tetra_stats/views/main_view.dart'; -import 'package:tetra_stats/views/rank_averages_view.dart'; -import 'package:tetra_stats/views/ranks_averages_view.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; - -List _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; -Stats _sortBy = Stats.tr; -bool reversed = false; -List _itemCountries = [for (MapEntry e in t.countries.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; -String _country = ""; -late String _oldWindowTitle; -final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); - -class TLLeaderboardView extends StatefulWidget { - const TLLeaderboardView({super.key}); - - @override - State createState() => TLLeaderboardState(); -} - -class TLLeaderboardState extends State { - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.getTitle().then((value) => _oldWindowTitle = value); - super.initState(); - } - - @override - void dispose() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(_oldWindowTitle); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - final NumberFormat f2 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2; - return Scaffold( - appBar: AppBar( - title: Text(t.tlLeaderboard), - actions: [ - IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RankAveragesView(), - maintainState: false, - ), - ); - }, - icon: const Icon(Icons.compress), - tooltip: t.rankAveragesViewTitle, - ), - ], - ), - backgroundColor: Colors.black, - body: SafeArea( - child: FutureBuilder( - future: teto.fetchTLLeaderboard(), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.done: - if (snapshot.hasData){ - final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers != null ? allPlayers.length : 0)}"); - bool bigScreen = MediaQuery.of(context).size.width > 768; - return NestedScrollView( - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceBetween, - children: [ - Text( - "${t.players(n: allPlayers.length)} • ${t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))}", - style: const TextStyle(color: Colors.white, fontSize: 25), - ), - TextButton(onPressed: (){ - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RankView(rank: snapshot.data!.getRankData("")), - ), - ); - }, child: Text(t.everyoneAverages, - style: const TextStyle(fontSize: 25))) - ],) - )), - SliverToBoxAdapter(child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 16, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.sortBy}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton(items: _itemStats, value: _sortBy, onChanged: ((value) { - _sortBy = value; - setState(() {}); - }),), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.reversed}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 5.5, 0, 7.5), - child: Checkbox(value: reversed, - checkColor: Colors.black, - onChanged: ((value) { - reversed = value!; - setState(() {}); - }),), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("${t.country}: ", - style: const TextStyle(color: Colors.white, fontSize: 25)), - DropdownButton(items: _itemCountries, value: _country, onChanged: ((value) { - _country = value; - setState(() {}); - }),), - ], - ), - ], - ), - ),), - const SliverToBoxAdapter(child: Divider()) - ]; - }, - body: ListView.builder( - itemCount: allPlayers!.length, - prototypeItem: ListTile( - leading: Text("0", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)), - title: Text("ehhh...", style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), - trailing: SizedBox(height: bigScreen ? 48 : 36, width: 1,), - subtitle: const Text("eh..."), - ), - itemBuilder: (context, index) { - return ListTile( - leading: Text( - (index+1).toString(), - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9) - ), - title: Text(allPlayers[index].username, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)), - subtitle: (bigScreen || _sortBy != Stats.tr) ? Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}", - style: TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: bigScreen ? null : 13, color: _sortBy == Stats.tr ? Colors.grey : null)) : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(allPlayers[index].tr)} TR", style: const TextStyle(fontSize: 28)), - Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36), - ], - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MainView(player: allPlayers[index].userId), - maintainState: false, - ), - ); - }, - ); - })); - } - if (snapshot.hasError){ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - if (snapshot.stackTrace != null) Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center), - ), - ], - ) - ); - } - return const Text("end of FutureBuilder"); - } - })), - ); - } -} diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index c3229b8..d2bbfda 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -6,7 +6,7 @@ import 'package:tetra_stats/data_objects/beta_league_stats.dart'; import 'package:tetra_stats/data_objects/beta_record.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/views/compare_view.dart' show CompareThingy; +import 'package:tetra_stats/widgets/compare_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/vs_graphs.dart'; diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart deleted file mode 100644 index b7816e5..0000000 --- a/lib/views/tracked_players_view.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart' show teto; -import 'package:tetra_stats/utils/filesizes_converter.dart'; -import 'package:tetra_stats/views/states_view.dart'; -import 'package:window_manager/window_manager.dart'; - -late String oldWindowTitle; - -class TrackedPlayersView extends StatefulWidget { - const TrackedPlayersView({super.key}); - - @override - State createState() => TrackedPlayersState(); -} - -class TrackedPlayersState extends State { - @override - void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.trackedPlayersViewTitle}"); - } - super.initState(); - } - - @override - void dispose() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return Scaffold( - appBar: AppBar( - title: Text(t.trackedPlayersViewTitle), - actions: [ - PopupMenuButton( - icon: const Icon(Icons.settings_backup_restore), - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - value: 1, - child: Text(t.duplicatedFix), - ), - PopupMenuItem( - value: 2, - child: Text(t.compressDB), - ), - ], - onSelected: (value) { - switch (value) { - case 1: - teto.removeDuplicatesFromTLMatches(); - break; - case 2: - teto.compressDB().then((value) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.SpaceSaved(size: bytesToSize(value)))))); - break; - default: - } - }) - ], - ), - backgroundColor: Colors.black, - body: SafeArea( - child: FutureBuilder( - future: teto.getAllPlayers(), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center(child: CircularProgressIndicator(color: Colors.white)); - case ConnectionState.done: - final allPlayers = (snapshot.data != null) ? snapshot.data as Map : {}; - List keys = allPlayers.keys.toList(); - return NestedScrollView( - headerSliverBuilder: (context, value) { - String howManyPlayers(int numberOfPlayers) => Intl.plural( - numberOfPlayers, - zero: t.trackedPlayersZeroEntrys, - one: t.trackedPlayersOneEntry, - other: t.trackedPlayersManyEntrys(numberOfPlayers: numberOfPlayers), - name: 'howManyPeople', - args: [numberOfPlayers], - desc: 'Description of how many people are seen in a place.', - examples: const {'numberOfPeople': 3}, - ); - return [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - howManyPlayers(allPlayers.length), - style: const TextStyle(color: Colors.white, fontSize: 25), - ), - )), - const SliverToBoxAdapter(child: Divider()) - ]; - }, - body: ListView.builder( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - print(index); - return ListTile( - title: Text(allPlayers[keys[index]]??"No nickname (huh?)"), - subtitle: Text(keys[index], style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), - trailing: IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: () { - setState(() {teto.deletePlayer(keys[index]);}); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: allPlayers[keys[index]]??"No nickname (huh?)")))); - }, - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StatesView(nickname: allPlayers[keys[index]]!, id: keys[index]), - ), - ); - }, - ); - })); - } - })), - ); - } -} diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart index acb1851..c2395ad 100644 --- a/lib/views/user_view.dart +++ b/lib/views/user_view.dart @@ -5,7 +5,7 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/views/destination_home.dart'; -import 'package:tetra_stats/views/main_view_tiles.dart'; +import 'package:tetra_stats/views/main_view.dart'; final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); diff --git a/lib/views/zenith_record_view.dart b/lib/views/zenith_record_view.dart deleted file mode 100644 index 7169bf7..0000000 --- a/lib/views/zenith_record_view.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/record_single.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/widgets/zenith_thingy.dart'; - -class ZenithRecordView extends StatelessWidget { - final RecordSingle record; - - const ZenithRecordView({super.key, required this.record}); - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - //bool bigScreen = MediaQuery.of(context).size.width >= 368; - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - title: Text("${ - switch (record.gamemode){ - "zenith" => t.quickPlay, - "zenithex" => "${t.quickPlay} ${t.expert}", - String() => "5000000 Blast", - } - } ${timestamp(record.timestamp)}"), - ), - body: SafeArea( - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: SingleChildScrollView( - child: ZenithThingy(record: record, switchable: false), - ), - ) - ), - ); - } - -} \ No newline at end of file diff --git a/lib/widgets/alpha_league_entry_thingy.dart b/lib/widgets/alpha_league_entry_thingy.dart new file mode 100644 index 0000000..2a3fd9a --- /dev/null +++ b/lib/widgets/alpha_league_entry_thingy.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class AlphaLeagueEntryThingy extends StatelessWidget{ + final TetraLeagueAlphaRecord record; + final String userID; + + const AlphaLeagueEntryThingy(this.record, this.userID); + + @override + Widget build(BuildContext context) { + var accentColor = record.endContext.firstWhere((element) => element.userId == userID).success ? Colors.green : Colors.red; + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + stops: const [0, 0.05], + colors: [accentColor, Colors.transparent] + ) + ), + child: ListTile( + leading: Text("${record.endContext.firstWhere((element) => element.userId == userID).points} : ${record.endContext.firstWhere((element) => element.userId != userID).points}", + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow)), + title: Text("vs. ${record.endContext.firstWhere((element) => element.userId != userID).username}"), + subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey)), + trailing: TrailingStats( + record.endContext.firstWhere((element) => element.userId == userID).secondary, + record.endContext.firstWhere((element) => element.userId == userID).tertiary, + record.endContext.firstWhere((element) => element.userId == userID).extra, + record.endContext.firstWhere((element) => element.userId != userID).secondary, + record.endContext.firstWhere((element) => element.userId != userID).tertiary, + record.endContext.firstWhere((element) => element.userId != userID).extra + ), + //onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: record, initPlayerId: userID))), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/badges_thingy.dart b/lib/widgets/badges_thingy.dart new file mode 100644 index 0000000..b223198 --- /dev/null +++ b/lib/widgets/badges_thingy.dart @@ -0,0 +1,90 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide Badge; +import 'package:tetra_stats/data_objects/badge.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class BadgesThingy extends StatelessWidget{ + final List badges; + + const BadgesThingy({super.key, required this.badges}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), + child: Row( + children: [ + const Text("Badges", style: TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer(), + Text(intf.format(badges.length)) + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var badge in 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 + ? t.obtainDate(date: timestamp(badge.ts!)) + : t.assignedManualy), + ], + ) + ], + ), + ), + actions: [ + TextButton( + child: Text(t.popupActions.ok), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ), + tooltip: badge.label, + icon: Image.asset( + "res/tetrio_badges/${badge.badgeId}.png", + height: 32, + errorBuilder: (context, error, stackTrace) { + return Image.network( + kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png", + height: 32, + errorBuilder:(context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 32, width: 32); + } + ); + }, + ) + ) + ], + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/beta_league_entry_thingy.dart b/lib/widgets/beta_league_entry_thingy.dart new file mode 100644 index 0000000..61d854c --- /dev/null +++ b/lib/widgets/beta_league_entry_thingy.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/views/tl_match_view.dart'; +import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class BetaLeagueEntryThingy extends StatelessWidget{ + final BetaRecord record; + final String userID; + + const BetaLeagueEntryThingy(this.record, this.userID); + + TextSpan matchResult(String result){ + return switch(result){ + "victory" => TextSpan( + text: "Victory", + style: TextStyle(color: Colors.greenAccent) + ), + "defeat" => TextSpan( + text: "Defeat", + style: TextStyle(color: Colors.redAccent) + ), + "tie" => TextSpan( + text: "Tie", + style: TextStyle(color: Colors.white) + ), + "dqvictory" => TextSpan( + text: "Opponent was DQ'ed", + style: TextStyle(color: Colors.lightGreenAccent) + ), + "dqdefeat" => TextSpan( + text: "Player was DQ'ed", + style: TextStyle(color: Colors.red) + ), + "nocontest" => TextSpan( + text: "No Contest", + style: TextStyle(color: Colors.blueAccent) + ), + "nullified" => TextSpan( + text: "Nullified", + style: TextStyle(color: Colors.purpleAccent) + ), + _ => TextSpan( + text: "${result.toUpperCase()}", + style: TextStyle(color: Colors.orangeAccent) + ) + }; + } + + Color deltaColor(double? delta){ + if (delta == null || delta.isNaN) return Colors.grey; + if (delta.isNegative) return Colors.redAccent; + else return Colors.greenAccent; + } + + @override + Widget build(BuildContext context) { + double? deltaTR = (record.extras.league[userID]?[1]?.tr != null && record.extras.league[userID]?[0]?.tr != null) ? record.extras.league[userID]![1]!.tr - record.extras.league[userID]![0]!.tr : null; + double? deltaGlicko = (record.extras.league[userID]?[1]?.glicko != null && record.extras.league[userID]?[0]?.glicko != null) ? record.extras.league[userID]![1]!.glicko - record.extras.league[userID]![0]!.glicko : null; + double? deltaRD = (record.extras.league[userID]?[1]?.rd != null && record.extras.league[userID]?[0]?.rd != null) ? record.extras.league[userID]![1]!.rd - record.extras.league[userID]![0]!.rd : null; + return Card( + child: ListTile( + title: Row( + children: [ + Text( + "${record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).wins} - ${record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).wins} ", + style: TextStyle(fontSize: 26, height: 0.75, fontWeight: FontWeight.bold), + ), + Text( + "vs.\n${record.enemyUsername}", + style: TextStyle(fontSize: 14, height: 0.8, fontWeight: FontWeight.w100), + ), + ], + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + matchResult(record.extras.result), + TextSpan( + text: ", ${timestamp(record.ts)}\n" + ), + TextSpan( + text: deltaTR != null ? "${fDiff.format(deltaTR)} TR" : "??? TR", + style: TextStyle( + color: deltaColor(deltaTR) + ) + ), + TextSpan( + text: ", " + ), + TextSpan( + text: deltaGlicko != null ? "${fDiff.format(deltaGlicko)} Glicko" : "??? Glicko", + style: TextStyle( + color: deltaColor(deltaGlicko) + ) + ), + TextSpan( + text: ", " + ), + TextSpan( + text: deltaRD != null ? "${fDiff.format(deltaRD)} RD" : "??? RD", + style: TextStyle( + color: Colors.grey + ) + ), + ] + ) + ), + ], + ), + ), + trailing: TrailingStats( + record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.apm, + record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.pps, + record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.vs, + record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.apm, + record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.pps, + record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.vs, + ), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: record, initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/compare_thingy.dart b/lib/widgets/compare_thingy.dart new file mode 100644 index 0000000..a5324e1 --- /dev/null +++ b/lib/widgets/compare_thingy.dart @@ -0,0 +1,147 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; + +const TextStyle verdictStyle = TextStyle(fontSize: 14, fontFamily: "Eurostile Round Condensed", color: Colors.grey, height: 1.1); + +class CompareThingy extends StatelessWidget { + final num greenSide; + final num redSide; + final String label; + final bool higherIsBetter; + final int? fractionDigits; + final String? postfix; + final String? prefix; + const CompareThingy( + {super.key, + required this.greenSide, + required this.redSide, + required this.label, + required this.higherIsBetter, + this.fractionDigits, + this.prefix, + this.postfix}); + + String verdict(num greenSide, num redSide, int fraction) { + var f = NumberFormat("+#,###.##;-#,###.##"); + f.maximumFractionDigits = fraction; + return f.format((greenSide - redSide)) + (postfix ?? ""); + } + + @override + Widget build(BuildContext context) { + var f = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode); + f.maximumFractionDigits = fractionDigits ?? 0; + return Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.green, Colors.transparent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + transform: const GradientRotation(0.6), + stops: [ + 0.0, + higherIsBetter + ? greenSide > redSide + ? 0.6 + : 0 + : greenSide < redSide + ? 0.6 + : 0 + ], + ) + ), + child: Text( + (prefix ?? "") + f.format(greenSide) + (postfix ?? ""), + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 1.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 2.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.start, + ), + )), + Column( + children: [ + Text( + label, + style: const TextStyle(fontSize: 22), + textAlign: TextAlign.center, + ), + Text( + verdict(greenSide, redSide, + fractionDigits != null ? fractionDigits! + 2 : 0), + style: verdictStyle, + textAlign: TextAlign.center, + ) + ], + ), + Expanded( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: const [Colors.red, Colors.transparent], + begin: Alignment.centerRight, + end: Alignment.centerLeft, + transform: const GradientRotation(-0.6), + stops: [ + 0.0, + higherIsBetter + ? redSide > greenSide + ? 0.6 + : 0 + : redSide < greenSide + ? 0.6 + : 0 + ], + )), + child: Text( + (prefix ?? "") + f.format(redSide) + (postfix ?? ""), + style: const TextStyle( + fontSize: 22, + shadows: [ + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(0.0, 0.0), + blurRadius: 8.0, + color: Colors.black, + ), + ], + ), + textAlign: TextAlign.end, + ), + )), + ], + ), + ); + } +} diff --git a/lib/widgets/distinguishment_thingy.dart b/lib/widgets/distinguishment_thingy.dart new file mode 100644 index 0000000..6d6952c --- /dev/null +++ b/lib/widgets/distinguishment_thingy.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tetra_stats/data_objects/distinguishment.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; + +class DistinguishmentThingy extends StatelessWidget{ + final Distinguishment distinguishment; + + const DistinguishmentThingy(this.distinguishment, {super.key}); + + List getDistinguishmentTitle(String? text) { + // TWC champions don't have header in their distinguishments + if (distinguishment.type == "twc") return [const TextSpan(text: "TETR.IO World Champion", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.yellowAccent))]; + // In case if it missing for some other reason, return this + if (text == null) return [const TextSpan(text: "Header is missing", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.redAccent))]; + + // Handling placeholders for logos + var exploded = text.split(" "); // wtf PHP reference? + List result = []; + for (String shit in exploded){ + switch (shit) { // if %% thingy was found, insert svg of icon + case "%osk%": + result.add(WidgetSpan(child: Padding( + padding: const EdgeInsets.only(left: 8), + child: SvgPicture.asset("res/icons/osk.svg", height: 28), + ))); + break; + case "%tetrio%": + result.add(WidgetSpan(child: Padding( + padding: const EdgeInsets.only(left: 8), + child: SvgPicture.asset("res/icons/tetrio-logo.svg", height: 28), + ))); + break; + default: // if not, insert text span + result.add(TextSpan(text: " $shit", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white))); + } + } + return result; + } + + /// Distinguishment title is barely predictable thing. + /// Receives [text], which is footer and returns sets of widgets for RichText widget + String getDistinguishmentSubtitle(String? text){ + // TWC champions don't have footer in their distinguishments + if (distinguishment.type == "twc") return "${distinguishment.detail} TETR.IO World Championship"; + // In case if it missing for some other reason, return this + if (text == null) return "Footer is missing"; + // If everything ok, return as it is + return text; + } + + Color getCardTint(String type, String detail){ + switch(type){ + case "staff": + switch(detail){ + case "founder": return const Color(0xAAFD82D4); + case "kagarin": return const Color(0xAAFF0060); + case "team": return const Color(0xAAFACC2E); + case "team-minor": return const Color(0xAAF5BD45); + case "administrator": return const Color(0xAAFF4E8A); + case "globalmod": return const Color(0xAAE878FF); + case "communitymod": return const Color(0xAA4E68FB); + case "alumni": return const Color(0xAA6057DB); + default: return theme.colorScheme.surface; + } + case "champion": + switch (detail){ + case "blitz": + case "40l": return const Color(0xAACCF5F6); + case "league": return const Color(0xAAFFDB31); + } + case "twc": return const Color(0xAAFFDB31); + default: return theme.colorScheme.surface; + } + return theme.colorScheme.surface; + } + + @override + Widget build(BuildContext context) { + return Card( + surfaceTintColor: getCardTint(distinguishment.type, distinguishment.detail??"null"), + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.distinguishment, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: getDistinguishmentTitle(distinguishment.header), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0), + child: Text(getDistinguishmentSubtitle(distinguishment.footer), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/error_thingy.dart b/lib/widgets/error_thingy.dart new file mode 100644 index 0000000..dc317a3 --- /dev/null +++ b/lib/widgets/error_thingy.dart @@ -0,0 +1,84 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/views/destination_home.dart'; + +class ErrorThingy extends StatelessWidget{ + final FetchResults? data; + final String? eText; + + const ErrorThingy({this.data, this.eText}); + + @override + Widget build(BuildContext context) { + IconData icon = Icons.error_outline; + String errText = eText??""; + String? subText; + if (data?.exception != null) switch (data!.exception!.runtimeType){ + case TetrioPlayerNotExist: + icon = Icons.search_off; + errText = t.errors.noSuchUser; + subText = t.errors.noSuchUserSub; + break; + case TetrioDiscordNotExist: + icon = Icons.search_off; + errText = t.errors.discordNotAssigned; + subText = t.errors.discordNotAssignedSub; + case ConnectionIssue: + var err = data!.exception as ConnectionIssue; + errText = t.errors.connection(code: err.code, message: err.message); + break; + case TetrioForbidden: + icon = Icons.remove_circle; + errText = t.errors.forbidden; + subText = t.errors.forbiddenSub(nickname: 'osk'); + break; + case TetrioTooManyRequests: + errText = t.errors.tooManyRequests; + subText = t.errors.tooManyRequestsSub; + break; + case TetrioOskwareBridgeProblem: + errText = t.errors.oskwareBridge; + subText = t.errors.oskwareBridgeSub; + break; + case TetrioInternalProblem: + errText = kIsWeb ? t.errors.internalWebVersion : t.errors.internal; + subText = kIsWeb ? t.errors.internalWebVersionSub : t.errors.internalSub; + break; + case ClientException: + errText = t.errors.clientException; + break; + default: + errText = data!.exception.toString(); + } + return TweenAnimationBuilder( + duration: Durations.medium3, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(0, 50-value*50, 0), + child: Opacity(opacity: value, child: child), + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Icon(icon, size: 128.0, color: Colors.red, shadows: [ + Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red), + Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red), + ]), + Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + if (subText != null) Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(subText, textAlign: TextAlign.center), + ), + Spacer() + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/fake_distinguishment_thingy.dart b/lib/widgets/fake_distinguishment_thingy.dart new file mode 100644 index 0000000..26a9e21 --- /dev/null +++ b/lib/widgets/fake_distinguishment_thingy.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/main.dart'; + +class FakeDistinguishmentThingy extends StatelessWidget{ + final bool banned; + final bool badStanding; + final bool bot; + final String? botMaintainers; + + FakeDistinguishmentThingy({super.key, this.banned = false, this.badStanding = false, this.bot = false, this.botMaintainers}); + + Color getCardTint(){ + if (banned) return Colors.red; + if (badStanding) return Colors.redAccent; + if (bot) return const Color.fromARGB(255, 60, 93, 55); + return theme.colorScheme.surface; + } + + InlineSpan getDistinguishmentTitle() { + String text = ""; + if (banned) text = "banned"; + if (badStanding) text = "bad standing"; + if (bot) text = "bot account"; + return TextSpan(text: text.toUpperCase(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white)); + } + + String getDistinguishmentSubtitle(){ + if (banned) return "Bans are placed when TETR.IO rules or terms of service are broken"; + if (badStanding) return "One or more recent bans on record"; + if (bot) return "Operated by $botMaintainers"; + return ""; + } + + @override + Widget build(BuildContext context) { + return Card( + surfaceTintColor: getCardTint(), + child: Container( + decoration: banned ? const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.transparent, Color.fromARGB(171, 244, 67, 54), Color.fromARGB(171, 244, 67, 54)], + stops: [0.1, 0.9, 0.01], + tileMode: TileMode.mirror, + begin: Alignment.topLeft, + end: AlignmentDirectional(-0.95, -0.95) + ) + ) : null, + child: Column( + children: [ + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [getDistinguishmentTitle()], + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0), + child: Text(getDistinguishmentSubtitle(), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/future_error.dart b/lib/widgets/future_error.dart new file mode 100644 index 0000000..ccab8c8 --- /dev/null +++ b/lib/widgets/future_error.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class FutureError extends StatelessWidget{ + final AsyncSnapshot snapshot; + + FutureError(this.snapshot); + + @override + Widget build(BuildContext context) { + return TweenAnimationBuilder( + duration: Durations.medium3, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(0, 50-value*50, 0), + child: Opacity(opacity: value, child: child), + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Icon(Icons.error_outline, size: 128.0, color: Colors.red, shadows: [ + Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red), + Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red), + ]), + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.left, style: TextStyle(fontFamily: "Monospace")), + ), + Spacer() + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart deleted file mode 100644 index edfad86..0000000 --- a/lib/widgets/gauget_num.dart +++ /dev/null @@ -1,111 +0,0 @@ -// ignore_for_file: curly_braces_in_flow_control_structures - -import 'package:flutter/material.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/leaderboard_position.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/utils/colors_functions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; - -class GaugetNum extends StatelessWidget { - final num playerStat; - final num? oldPlayerStat; - final bool higherIsBetter; - final List ranges; - final double minimum; - final double maximum; - final String playerStatLabel; - final String? okText; - final String? alertTitle; - final List? alertWidgets; - final LeaderboardPosition? pos; - final num? averageStat; - - const GaugetNum( - {super.key, - required this.playerStat, - required this.playerStatLabel, - this.alertWidgets, - this.oldPlayerStat, - required this.higherIsBetter, - required this.minimum, - required this.maximum, - required this.ranges, - this.okText, this.alertTitle, this.pos, this.averageStat}); - - Color getStatColor(){ - if (averageStat == null) return Colors.white; - num percentile = (higherIsBetter ? playerStat / averageStat! : averageStat! / playerStat).abs(); - if (percentile > 1.50) return Colors.purpleAccent; - else if (percentile > 1.20) return Colors.blueAccent; - else if (percentile > 0.90) return Colors.greenAccent; - else if (percentile > 0.70) return Colors.yellowAccent; - else return Colors.redAccent; - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 200, - height: 120, - child: SfRadialGauge( - title: GaugeTitle(text: playerStatLabel), - axes: [RadialAxis( - startAngle: 180, - endAngle: 360, - showLabels: false, - showTicks: false, - radiusFactor: 2.1, - centerY: 0.5, - minimum: minimum, - maximum: maximum, - ranges: ranges, - pointers: [ - NeedlePointer( - value: playerStat as double, - enableAnimation: true, - needleLength: 0.9, - needleStartWidth: 2, - needleEndWidth: 15, - knobStyle: const KnobStyle(color: Colors.transparent), - gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) - ], - annotations: [GaugeAnnotation( - widget: TextButton(child: Text(f3.format(playerStat), - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: getStatColor())), - onPressed: (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(alertTitle??playerStatLabel, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView(child: ListBody(children: alertWidgets!)), - actions: [ - TextButton( - child: Text(okText??t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - )); - },), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05), - if (oldPlayerStat != null || pos != null) GaugeAnnotation( - widget: RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (oldPlayerStat != null) TextSpan(text: comparef.format(playerStat - oldPlayerStat!), style: TextStyle( - color: higherIsBetter ? - oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent : - oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent - ),), - if (oldPlayerStat != null && pos != null) const TextSpan(text: " • "), - if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}", style: TextStyle(color: getColorOfRank(pos!.position))) - ] - ), - ), - positionFactor: 0.05)], - )],), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/gauget_thingy.dart b/lib/widgets/gauget_thingy.dart new file mode 100644 index 0000000..fa5afe7 --- /dev/null +++ b/lib/widgets/gauget_thingy.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/leaderboard_position.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; + +class GaugetThingy extends StatelessWidget{ + final double? value; + final String? subString; + final double min; + final double max; + final double? oldValue; + final double? avgValue; + final bool moreIsBetter; + final double tickInterval; + final String label; + final double sideSize; + final bool percentileFormat; + final int fractionDigits; + final LeaderboardPosition? lbPos; + + const GaugetThingy({super.key, required this.value, this.subString, required this.min, required this.max, this.oldValue, this.avgValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter, this.percentileFormat = false, this.lbPos}); + + @override + Widget build(BuildContext context) { + NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits); + return ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: SizedBox( + height: sideSize, + width: sideSize, + child: SfRadialGauge( + backgroundColor: Colors.black, + axes: [ + RadialAxis( + radiusFactor: 1.01, + minimum: min, + maximum: max, + showTicks: true, + showLabels: false, + interval: tickInterval, + minorTicksPerInterval: 0, + ranges:[ + GaugeRange(startValue: 0, endValue: (value != null && !value!.isNaN) ? value! : 0, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + Text((value != null && !value!.isNaN) ? percentileFormat ? percentage.format(value) : f.format(value) : "---", textAlign: TextAlign.center, style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: (value != null && !value!.isNaN) ? getStatColor(value!, avgValue, moreIsBetter) : Colors.grey))), + angle: 90,positionFactor: 0.10 + ), + GaugeAnnotation(widget: Container(child: + Text(label, textAlign: TextAlign.center, style: TextStyle(height: .9, color: (value != null && !value!.isNaN) ? null : Colors.grey))), + angle: 270,positionFactor: 0.3, verticalAlignment: GaugeAlignment.far, + ), + if (oldValue != null && (value != null && !value!.isNaN)) GaugeAnnotation(widget: Container(child: + Text(comparef2.format(percentileFormat ? (value!-oldValue!) * 100 : value!-oldValue!), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(moreIsBetter ? value!-oldValue! : oldValue!-value!)))), + angle: 90,positionFactor: lbPos != null ? 0.7 : 0.45 + ), + if (subString != null) GaugeAnnotation(widget: Container(child: + Text(subString!, textAlign: TextAlign.center, style: TextStyle(color: (value != null && !value!.isNaN) ? null : Colors.grey))), + angle: 90,positionFactor: lbPos != null ? 0.7 : 0.45 + ), + if (lbPos != null) GaugeAnnotation(widget: Container(child: + Text(lbPos!.position >= 1000 ? "${t.top} ${f2.format(lbPos!.percentage*100)}%" : "№ ${lbPos!.position}", textAlign: TextAlign.center, style: TextStyle(color: (lbPos != null) ? getColorOfRank(lbPos!.position) : Colors.grey))), + angle: 90,positionFactor: 0.45 + ) + ], + ) + ] + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index 3049eeb..7815b0c 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -287,205 +287,212 @@ class Graphs extends StatelessWidget{ double speed = pps / 3.75; double defense = nerdStats.dss * 1.15; double cheese = nerdStats.cheese / 110; - return Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - if (true) Padding( // vs graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.circle, - tickCount: 4, - radarBackgroundColor: Colors.black.withAlpha(170), - radarBorderData: const BorderSide(color: Colors.white24, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.white24, width: 1), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: 'APM', angle: angle, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: 'PPS', angle: angle, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); - case 4: - return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); - case 5: - return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); - case 6: - return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); - case 7: - return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); - case 8: - return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); - case 9: - return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), - borderColor: Theme.of(context).colorScheme.primary, - dataEntries: [ - RadarEntry(value: apm * apmWeight), - RadarEntry(value: pps * ppsWeight), - RadarEntry(value: vs * vsWeight), - RadarEntry(value: nerdStats.app * appWeight), - RadarEntry(value: nerdStats.dss * dssWeight), - RadarEntry(value: nerdStats.dsp * dspWeight), - RadarEntry(value: nerdStats.appdsp * appdspWeight), - RadarEntry(value: nerdStats.vsapm * vsapmWeight), - RadarEntry(value: nerdStats.cheese * cheeseWeight), - RadarEntry(value: nerdStats.gbe * gbeWeight), - ], + return Card( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Center( + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + if (true) Padding( // vs graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 22), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.circle, + tickCount: 4, + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: 'APM', angle: angle, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: 'PPS', angle: angle, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); + case 4: + return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 5: + return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 6: + return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 7: + return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); + case 8: + return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); + case 9: + return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), + borderColor: Theme.of(context).colorScheme.primary, + dataEntries: [ + RadarEntry(value: apm * apmWeight), + RadarEntry(value: pps * ppsWeight), + RadarEntry(value: vs * vsWeight), + RadarEntry(value: nerdStats.app * appWeight), + RadarEntry(value: nerdStats.dss * dssWeight), + RadarEntry(value: nerdStats.dsp * dspWeight), + RadarEntry(value: nerdStats.appdsp * appdspWeight), + RadarEntry(value: nerdStats.vsapm * vsapmWeight), + RadarEntry(value: nerdStats.cheese * cheeseWeight), + RadarEntry(value: nerdStats.gbe * gbeWeight), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 180), + 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 ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 180), - 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( // psq graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.circle, - tickCount: 4, - radarBackgroundColor: Colors.black.withAlpha(170), - radarBorderData: const BorderSide(color: Colors.white24, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.white24, width: 1), - titleTextStyle: const TextStyle(height: 1.1), - radarTouchData: RadarTouchData(), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: 'Opener\n${percentage.format(playstyle.opener)}', angle: 0, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: 'Inf DS\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), - borderColor: Theme.of(context).colorScheme.primary, - dataEntries: [ - RadarEntry(value: playstyle.opener), - RadarEntry(value: playstyle.stride), - RadarEntry(value: playstyle.infds), - RadarEntry(value: playstyle.plonk), - ], + Padding( // psq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 22), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.circle, + tickCount: 4, + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: 'Opener\n${percentage.format(playstyle.opener)}', angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: 'Inf DS\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), + borderColor: Theme.of(context).colorScheme.primary, + dataEntries: [ + RadarEntry(value: playstyle.opener), + RadarEntry(value: playstyle.stride), + RadarEntry(value: playstyle.infds), + RadarEntry(value: playstyle.plonk), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 1), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ) - ], + ), ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional - ), - ), - ), - Padding( // sq graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.circle, - tickCount: 4, - radarBackgroundColor: Colors.black.withAlpha(170), - radarBorderData: const BorderSide(color: Colors.white24, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.white24, width: 1), - titleTextStyle: const TextStyle(height: 1.1), - radarTouchData: RadarTouchData(), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: '${t.graphs.attack}\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: '${t.graphs.speed}\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: '${t.graphs.defense}\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: '${t.graphs.cheese}\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), - borderColor: Theme.of(context).colorScheme.primary, - dataEntries: [ - RadarEntry(value: attack), - RadarEntry(value: speed), - RadarEntry(value: defense), - RadarEntry(value: cheese), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 1.2), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 22), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.circle, + tickCount: 4, + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: '${t.graphs.attack}\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: '${t.graphs.speed}\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: '${t.graphs.defense}\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: '${t.graphs.cheese}\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), + borderColor: Theme.of(context).colorScheme.primary, + dataEntries: [ + RadarEntry(value: attack), + RadarEntry(value: speed), + RadarEntry(value: defense), + RadarEntry(value: cheese), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1.2), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ) ) - ], + ) ) - ) - ) - ) - ], + ], + ), + ), + ), ); } diff --git a/lib/widgets/info_thingy.dart b/lib/widgets/info_thingy.dart new file mode 100644 index 0000000..dfd90c3 --- /dev/null +++ b/lib/widgets/info_thingy.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class InfoThingy extends StatelessWidget{ + final String info; + + const InfoThingy(this.info); + + @override + Widget build(BuildContext context) { + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), + SizedBox(height: 5.0), + Text(info, textAlign: TextAlign.center), + ], + )); + } + +} \ No newline at end of file diff --git a/lib/widgets/nerd_stats_thingy.dart b/lib/widgets/nerd_stats_thingy.dart new file mode 100644 index 0000000..105b689 --- /dev/null +++ b/lib/widgets/nerd_stats_thingy.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/widgets/gauget_thingy.dart'; + +class NerdStatsThingy extends StatelessWidget{ + final NerdStats nerdStats; + final NerdStats? oldNerdStats; + final CutoffTetrio? averages; + final PlayerLeaderboardPosition? lbPos; + + const NerdStatsThingy({super.key, required this.nerdStats, this.oldNerdStats, this.averages, this.lbPos}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 256.0, + width: 256.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: Container( + decoration: BoxDecoration(gradient: RadialGradient(colors: [Colors.black12.withAlpha(100), Colors.black], radius: 0.6)), + child: SfRadialGauge( + axes: [ + RadialAxis( + startAngle: 190, + endAngle: 350, + showLabels: false, + showTicks: true, + radiusFactor: 1, + centerY: 0.5, + minimum: 0, + maximum: 1, + ranges: [ + GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), + GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), + GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), + GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), + GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), + ], + pointers: [ + NeedlePointer( + value: nerdStats.app, + enableAnimation: true, + needleLength: 0.9, + needleStartWidth: 2, + needleEndWidth: 15, + knobStyle: const KnobStyle(color: Colors.transparent), + gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + const TextSpan(text: "APP\n"), + TextSpan(text: f3.format(nerdStats.app), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.app, averages?.nerdStats?.app, true))), + if (lbPos != null) TextSpan(text: lbPos!.app!.position >= 1000 ? "\n${t.top} ${f2.format(lbPos!.app!.percentage*100)}%" : "\n№${lbPos!.app!.position}", style: TextStyle(color: getColorOfRank(lbPos!.app!.position))), + if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.app - oldNerdStats!.app)}", style: TextStyle(color: getDifferenceColor(nerdStats.app - oldNerdStats!.app))) + ] + ))), + angle: 270,positionFactor: 0.5 + )], + ), + RadialAxis( + startAngle: 20, + endAngle: 160, + isInversed: true, + showLabels: false, + showTicks: true, + radiusFactor: 1, + centerY: 0.5, + minimum: 1.8, + maximum: 2.4, + ranges: [ + GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), + GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), + GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), + ], + pointers: [ + NeedlePointer( + value: nerdStats.vsapm, + enableAnimation: true, + needleLength: 0.9, + needleStartWidth: 2, + needleEndWidth: 15, + knobStyle: const KnobStyle(color: Colors.transparent), + gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + const TextSpan(text: "VS/APM\n"), + TextSpan(text: f3.format(nerdStats.vsapm), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.vsapm, averages?.nerdStats?.vsapm, true))), + if (lbPos != null) TextSpan(text: lbPos!.vsapm!.position >= 1000 ? "\n${t.top} ${f2.format(lbPos!.vsapm!.percentage*100)}%" : "\n№${lbPos!.vsapm!.position}", style: TextStyle(color: getColorOfRank(lbPos!.vsapm!.position))), + if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.vsapm - oldNerdStats!.vsapm)}", style: TextStyle(color: getDifferenceColor(nerdStats.vsapm - oldNerdStats!.vsapm))), + ] + ))), + angle: 90,positionFactor: 0.5 + ) + ], + ) + ] + ), + ), + ), + ), + Expanded( + child: Wrap( + alignment: WrapAlignment.center, + spacing: 10.0, + runSpacing: 10.0, + runAlignment: WrapAlignment.start, + children: [ + GaugetThingy(value: nerdStats.dss, oldValue: oldNerdStats?.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dss, lbPos: lbPos?.dss), + GaugetThingy(value: nerdStats.dsp, oldValue: oldNerdStats?.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dsp, lbPos: lbPos?.dsp), + GaugetThingy(value: nerdStats.appdsp, oldValue: oldNerdStats?.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.appdsp, lbPos: lbPos?.appdsp), + GaugetThingy(value: nerdStats.cheese, oldValue: oldNerdStats?.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2, moreIsBetter: false, lbPos: lbPos?.cheese), + GaugetThingy(value: nerdStats.gbe, oldValue: oldNerdStats?.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.gbe, lbPos: lbPos?.gbe), + GaugetThingy(value: nerdStats.nyaapp, oldValue: oldNerdStats?.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.nyaapp, lbPos: lbPos?.nyaapp), + GaugetThingy(value: nerdStats.area, oldValue: oldNerdStats?.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1, moreIsBetter: true, avgValue: averages?.nerdStats?.area, lbPos: lbPos?.area), + ], + ), + ) + ] + ), + ), + ], + ) + ); + } +} \ No newline at end of file diff --git a/lib/widgets/news_thingy.dart b/lib/widgets/news_thingy.dart new file mode 100644 index 0000000..57c9905 --- /dev/null +++ b/lib/widgets/news_thingy.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/news_entry.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class NewsThingy extends StatelessWidget{ + final News news; + + const NewsThingy(this.news, {super.key}); + + ListTile getNewsTile(NewsEntry news){ + Map gametypes = { + "40l": t.sprint, + "blitz": t.blitz, + "5mblast": "5,000,000 Blast", + "zenith": "Quick Play", + "zenithex": "Quick Play Expert", + }; + + // Individuly handle each entry type + switch (news.type) { + case "leaderboard": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + text: t.newsParts.leaderboardStart, + children: [ + TextSpan(text: "№${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: t.newsParts.leaderboardMiddle), + TextSpan(text: "№${gametypes[news.data["gametype"]]}", style: const TextStyle(fontWeight: FontWeight.bold)), + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + ); + case "personalbest": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + text: t.newsParts.personalbest, + children: [ + TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: t.newsParts.personalbestMiddle), + TextSpan(text: switch (news.data["gametype"]){ + "blitz" => NumberFormat.decimalPattern().format(news.data["result"]), + "40l" => get40lTime((news.data["result"]*1000).floor()), + "5mblast" => get40lTime((news.data["result"]*1000).floor()), + "zenith" => "${f2.format(news.data["result"])} m.", + "zenithex" => "${f2.format(news.data["result"])} m.", + _ => "unknown" + }, + style: const TextStyle(fontWeight: FontWeight.bold) + ), + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/icons/improvement-local.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + case "badge": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + text: t.newsParts.badgeStart, + children: [ + TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: t.newsParts.badgeEnd) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/tetrio_badges/${news.data["type"]}.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + case "rankup": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + text: t.newsParts.rankupStart, + children: [ + TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: t.newsParts.rankupEnd) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + case "supporter": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + text: t.newsParts.supporterStart, + children: [ + TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/icons/supporter-tag.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + case "supporter_gift": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + text: t.newsParts.supporterGiftStart, + children: [ + TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/icons/supporter-tag.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + default: // if type is unknown + return ListTile( + title: Text(t.newsParts.unknownNews(type: news.type)), + subtitle: Text(timestamp(news.timestamp)), + ); + } + } + + @override + Widget build(BuildContext context) { + return Card( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ] + ), + if (news.news.isEmpty) const Center(child: Text("Empty list")) + else for (NewsEntry entry in news.news) getNewsTile(entry) + ], + ), + ), + ); + } + +} \ No newline at end of file diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index 2276d8d..4a20225 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -100,7 +100,7 @@ class SingleplayerRecord extends StatelessWidget { if (record!.gamemode == "40l") Wrap( alignment: WrapAlignment.spaceBetween, spacing: 20, - children: [ + children: [ // TODO: replace StatCellNum(playerStat: record!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), StatCellNum(playerStat: record!.stats.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index 1a3fbd0..41904cb 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -1,21 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show prefs; +import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -var fDiff = NumberFormat("+#,###.####;-#,###.####"); +import 'text_timestamp.dart'; class TLRatingThingy extends StatelessWidget{ final String userID; final TetraLeague tlData; final TetraLeague? oldTl; final double? topTR; + final bool? showPositions; final DateTime? lastMatchPlayed; - const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed}); + const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed, this.showPositions}); @override Widget build(BuildContext context) { @@ -39,10 +40,11 @@ class TLRatingThingy extends StatelessWidget{ ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? : Image.asset("res/tetrio_tl_alpha_ranks/${tlData.rank}.png", height: 128), Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ RichText( text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white), + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white, height: 0.9), children: (tlData.gamesPlayed > 9) ? switch(prefs.getInt("ratingMode")){ 1 => [ TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), @@ -62,17 +64,43 @@ class TLRatingThingy extends StatelessWidget{ } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),] ) ), - if (oldTl != null) Text( - switch(prefs.getInt("ratingMode")){ - 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", - 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", - _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" - }, + if (oldTl != null) RichText( textAlign: TextAlign.center, - style: TextStyle( - color: tlData.tr - oldTl!.tr < 0 ? - Colors.red : - Colors.green + softWrap: true, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan(text: switch(prefs.getInt("ratingMode")){ + 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", + 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", + _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" + }, + style: TextStyle( + color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ + 1 => tlData.glicko! - oldTl!.glicko!, + 2 => tlData.percentile - oldTl!.percentile, + _ => tlData.tr - oldTl!.tr + }) + ), + ), + const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), + TextSpan(text: switch(prefs.getInt("ratingMode")){ + 1 => "${fDiff.format(tlData.tr - oldTl!.tr)} TR", + _ => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko" + }, + style: TextStyle( + color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ + 1 => tlData.tr - oldTl!.tr, + _ => tlData.glicko! - oldTl!.glicko! + }) + ), + ), + const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), + TextSpan( + text: "${fDiff.format(tlData.rd! - oldTl!.rd!)} RD", + style: TextStyle(color: getDifferenceColor(oldTl!.rd! - tlData.rd!)) + ) + ], ), ), if (tlData.gamesPlayed > 9) Column( @@ -96,6 +124,20 @@ class TLRatingThingy extends StatelessWidget{ ), ], ), + if (showPositions == true) RichText( + textAlign: TextAlign.start, + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (tlData.standing != -1) TextSpan(text: "№ ${intf.format(tlData.standing)}", style: TextStyle(color: getColorOfRank(tlData.standing))), + if (tlData.standing != -1 || tlData.standingLocal != -1) const TextSpan(text: " • "), + if (tlData.standingLocal != -1) TextSpan(text: "№ ${intf.format(tlData.standingLocal)} local", style: TextStyle(color: getColorOfRank(tlData.standingLocal))), + if (tlData.standing != -1 && tlData.standingLocal != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(tlData.timestamp)), + ] + ), + ), ], ), ], diff --git a/lib/widgets/tl_records_thingy.dart b/lib/widgets/tl_records_thingy.dart new file mode 100644 index 0000000..7217e8d --- /dev/null +++ b/lib/widgets/tl_records_thingy.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/widgets/beta_league_entry_thingy.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; + +class TLRecords extends StatelessWidget { + final String userID; + + /// Widget, that displays Tetra League records. + /// Accepts list of TL records ([data]) and [userID] of player from the view + const TLRecords(this.userID); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: teto.fetchTLStream(userID), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + for (BetaRecord record in snapshot.data!.records) BetaLeagueEntryThingy(record, userID) + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index be40b7c..01e685f 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -1,319 +1,112 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; -import 'package:tetra_stats/data_objects/tetra_league.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/utils/colors_functions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/widgets/gauget_num.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/widgets/tl_progress_bar.dart'; -import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; - - -var intFDiff = NumberFormat("+#,###.000;-#,###.000"); - -class TLThingy extends StatefulWidget { - final TetraLeague tl; - final String userID; - final List states; - final bool showTitle; - final bool bot; - final bool guest; - final double? topTR; - final PlayerLeaderboardPosition? lbPositions; - final TetraLeague? averages; - final double? thatRankCutoff; - final double? thatRankCutoffGlicko; - final double? thatRankTarget; - final double? nextRankCutoff; - final double? nextRankCutoffGlicko; - final double? nextRankTarget; - final DateTime? lastMatchPlayed; - const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget, this.lastMatchPlayed}); - - @override - State createState() => _TLThingyState(); -} - -class _TLThingyState extends State with TickerProviderStateMixin { - late bool oskKagariGimmick; - late TetraLeague? oldTl; - late TetraLeague currentTl; - late RangeValues _currentRangeValues; - late List sortedStates; - -@override - void initState() { - _currentRangeValues = const RangeValues(0, 1); - sortedStates = widget.states.reversed.toList(); - oldTl = sortedStates.elementAtOrNull(1); - currentTl = widget.tl; - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - String decimalSeparator = f2.symbols.DECIMAL_SEP; - List estTRformated = currentTl.estTr != null ? f2.format(currentTl.estTr!.esttr).split(decimalSeparator) : []; - List estTRaccFormated = currentTl.esttracc != null ? intFDiff.format(currentTl.esttracc!).split(".") : []; - if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,)); - 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: [ - if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)), - textAlign: TextAlign.center,), - if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), - labels: RangeLabels( - _currentRangeValues.start.round().toString(), - _currentRangeValues.end.round().toString(), - ), - onChanged: (RangeValues values) { - setState(() { - _currentRangeValues = values; - if (values.start.round() == 0){ - currentTl = widget.tl; - }else{ - currentTl = sortedStates[values.start.round()-1]; - } - if (values.end.round() == 0){ - oldTl = widget.tl; - }else{ - oldTl = sortedStates[values.end.round()-1]; - } - }); - }, - ), - TLRatingThingy(userID: widget.userID, tlData: currentTl, oldTl: oldTl, topTR: widget.topTR, lastMatchPlayed: widget.lastMatchPlayed), - if (currentTl.gamesPlayed > 9) TLProgress( - tlData: currentTl, - previousRankTRcutoff: widget.thatRankCutoff, - previousGlickoCutoff: widget.thatRankCutoffGlicko, - previousRankTRcutoffTarget: widget.thatRankTarget, - nextRankTRcutoff: widget.nextRankCutoff, - nextRankGlickoCutoff: widget.nextRankCutoffGlicko, - nextRankTRcutoffTarget: widget.nextRankTarget, - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 16, 8, 48), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm, pos: widget.lbPositions?.apm, averageStat: widget.averages?.apm), - if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps, pos: widget.lbPositions?.pps, averageStat: widget.averages?.pps, smallDecimal: false), - if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs, pos: widget.lbPositions?.vs, averageStat: widget.averages?.vs), - if (currentTl.standingLocal > 0) StatCellNum(playerStat: currentTl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), - StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed, pos: widget.lbPositions?.gamesPlayed), - StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon, pos: widget.lbPositions?.gamesWon), - StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null, pos: widget.lbPositions?.winrate, averageStat: widget.averages != null ? widget.averages!.winrate * 100 : null), - ], - ), - ), - if (currentTl.nerdStats != null) - Column( - children: [ - Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 35, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - GaugetNum(playerStat: currentTl.nerdStats!.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.app}") - ], oldPlayerStat: oldTl?.nerdStats?.app, pos: widget.lbPositions?.app, - averageStat: widget.averages?.nerdStats?.app), - GaugetNum(playerStat: currentTl.nerdStats!.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") - ], oldPlayerStat: oldTl?.nerdStats?.vsapm, pos: widget.lbPositions?.vsapm, - averageStat: widget.averages?.nerdStats?.vsapm) - ]), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - pos: widget.lbPositions?.dss, - averageStat: widget.averages?.nerdStats?.dss, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dss,), - StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - pos: widget.lbPositions?.dsp, - averageStat: widget.averages?.nerdStats?.dsp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dsp,), - StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - pos: widget.lbPositions?.appdsp, - averageStat: widget.averages?.nerdStats?.appdsp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.appdsp,), - StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - pos: widget.lbPositions?.cheese, - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: false, - oldPlayerStat: oldTl?.nerdStats?.cheese,), - StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - pos: widget.lbPositions?.gbe, - averageStat: widget.averages?.nerdStats?.gbe, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.gbe,), - StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - pos: widget.lbPositions?.nyaapp, - averageStat: widget.averages?.nerdStats?.nyaapp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.nyaapp,), - StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - pos: widget.lbPositions?.area, - averageStat: widget.averages?.nerdStats?.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.area,) - ]), - ) - ], - ), - if (currentTl.estTr != null) - Padding( - padding: const EdgeInsets.fromLTRB(8, 20, 8, 20), - child: Container( - height: 70, - constraints: const BoxConstraints(maxWidth: 500), - child: Stack( - children: [ - Positioned( - left: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.statCellNum.estOfTR, style: const TextStyle(height: 0.1),), - RichText( - text: TextSpan( - text: estTRformated[0], - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), - children: [TextSpan(text: decimalSeparator+estTRformated[1], style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), - children: [ - if (oldTl?.estTr?.esttr != null) TextSpan(text: comparef.format(currentTl.estTr!.esttr - oldTl!.estTr!.esttr), style: TextStyle( - color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent - ),), - if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), - if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.estTr!.position))), - if (widget.lbPositions?.estTr != null || oldTl?.estTr?.esttr != null) const TextSpan(text: " • "), - TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") - ] - ), - ), - ],), - ), - Positioned( - right: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(t.statCellNum.accOfEst, style: const TextStyle(height: 0.1),), - RichText( - text: TextSpan( - text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? estTRaccFormated[0] : "---", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), - children: [ - TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? decimalSeparator+estTRaccFormated[1] : ".---", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) - ] - ), - ), - if ((oldTl?.esttracc != null || widget.lbPositions != null) && currentTl.bestRank != "z") RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), - children: [ - if (oldTl?.esttracc != null) TextSpan(text: comparef.format(currentTl.esttracc! - oldTl!.esttracc!), style: TextStyle( - color: oldTl!.esttracc! > currentTl.esttracc! ? Colors.redAccent : Colors.greenAccent - ),), - if (oldTl?.esttracc != null && widget.lbPositions?.accOfEst != null) const TextSpan(text: " • "), - if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.accOfEst!.percentage*100)}%" : "№${widget.lbPositions!.accOfEst!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.accOfEst!.position))) - ] - ), - ), - ],), - ) - ], - ), - ) - ), - if (currentTl.nerdStats != null) Graphs(currentTl.apm!, currentTl.pps!, currentTl.vs!, currentTl.nerdStats!, currentTl.playstyle!) - ] - ); - }, - ); - }); - } -} +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/widgets/gauget_thingy.dart'; +import 'package:tetra_stats/widgets/tl_progress_bar.dart'; +import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; + +class TetraLeagueThingy extends StatelessWidget{ + final TetraLeague league; + final TetraLeague? toCompare; + final Cutoffs? cutoffs; + final CutoffTetrio? averages; + final PlayerLeaderboardPosition? lbPos; + + const TetraLeagueThingy({super.key, required this.league, this.toCompare, this.cutoffs, this.averages, this.lbPos}); + + @override + Widget build(BuildContext context) { + return Card( + //surfaceTintColor: rankColors[league.rank], + child: Column( + children: [ + TLRatingThingy(userID: league.id, tlData: league, oldTl: toCompare, showPositions: true), + if (league.gamesPlayed > 9) TLProgress( + tlData: league, + previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, + nextRankTRcutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.tr[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, + previousRankTRcutoffTarget: league.rank != "z" ? rankTargets[league.rank] : null, + nextRankTRcutoffTarget: (league.rank != "z" && league.rank != "x+") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(league.rank)+1)] : null, + previousGlickoCutoff: cutoffs != null ? cutoffs!.glicko[league.rank != "z" ? league.rank : league.percentileRank] : null, + nextRankGlickoCutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.glicko[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, + ), + Row( + // spacing: 25.0, + // alignment: WrapAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(league.apm != null ? f2.format(league.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)), + Text(" APM", style: TextStyle(fontSize: 21, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)), + if (toCompare != null) Text(" (${comparef2.format(league.apm!-toCompare!.apm!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.apm!-toCompare!.apm!))), + if (lbPos != null) Text(lbPos?.apm != null ? (lbPos!.apm!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.apm!.percentage*100)}%)" : " (№ ${lbPos!.apm!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.apm != null ? getColorOfRank(lbPos!.apm!.position) : null)) + ]), + TableRow(children: [ + Text(league.pps != null ? f2.format(league.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)), + Text(" PPS", style: TextStyle(fontSize: 21, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)), + if (toCompare != null) Text(" (${comparef2.format(league.pps!-toCompare!.pps!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.pps!-toCompare!.pps!))), + if (lbPos != null) Text(lbPos?.pps != null ? (lbPos!.pps!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.pps!.percentage*100)}%)" : " (№ ${lbPos!.pps!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.pps != null ? getColorOfRank(lbPos!.pps!.position) : null)) + ]), + TableRow(children: [ + Text(league.vs != null ? f2.format(league.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)), + Text(" VS", style: TextStyle(fontSize: 21, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)), + if (toCompare != null) Text(" (${comparef2.format(league.vs!-toCompare!.vs!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.vs!-toCompare!.vs!))), + if (lbPos != null) Text(lbPos?.vs != null ? (lbPos!.vs!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.vs!.percentage*100)}%)" : " (№ ${lbPos!.vs!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.vs != null ? getColorOfRank(lbPos!.vs!.position) : null)) + ]) + ], + ), + ), + ), + GaugetThingy(value: league.winrate, min: 0, max: 1, tickInterval: 0.25, label: "Winrate", sideSize: 128, fractionDigits: 2, moreIsBetter: true, oldValue: toCompare?.winrate, percentileFormat: true, lbPos: lbPos?.winrate), + Expanded( + child: Center( + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + //Text("APM: ", style: TextStyle(fontSize: 21)), + Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Games", style: TextStyle(fontSize: 21)), + if (toCompare != null) Text(" (${comparef2.format(league.gamesPlayed-toCompare!.gamesPlayed)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + if (lbPos != null) Text(lbPos?.gamesPlayed != null ? (lbPos!.gamesPlayed!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.gamesPlayed!.percentage*100)}%)" : " (№ ${lbPos!.gamesPlayed!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.gamesPlayed != null ? getColorOfRank(lbPos!.gamesPlayed!.position) : null)) + ]), + TableRow(children: [ + //Text("PPS: ", style: TextStyle(fontSize: 21)), + Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Won", style: TextStyle(fontSize: 21)), + if (toCompare != null) Text(" (${comparef2.format(league.gamesWon-toCompare!.gamesWon)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + if (lbPos != null) Text(lbPos?.gamesWon != null ? (lbPos!.gamesWon!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.gamesWon!.percentage*100)}%)" : " (№ ${lbPos!.gamesWon!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.gamesWon != null ? getColorOfRank(lbPos!.gamesWon!.position) : null)) + ]), + TableRow(children: [ + //Text("VS: ", style: TextStyle(fontSize: 21)), + Tooltip(child: Text("${league.gxe.isNegative ? "---" : f3.format(league.gxe)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.gxe.isNegative ? Colors.grey : Colors.white)), message: "${f2.format(league.s1tr)} S1 TR"), + Tooltip(child: Text(" GXE", style: TextStyle(fontSize: 21, color: league.gxe.isNegative ? Colors.grey : Colors.white)), message: "Glixare"), + if (toCompare != null) Text(" (${comparef.format(league.gxe-toCompare!.gxe)})", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: getDifferenceColor(league.gxe-toCompare!.gxe))), + if (lbPos != null) Text(lbPos?.glixare != null ? (lbPos!.glixare!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.glixare!.percentage*100)}%)" : " (№ ${lbPos!.glixare!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.glixare != null ? getColorOfRank(lbPos!.glixare!.position) : null)) + ]), + ], + ), + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 957f4d5..3d56a30 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -1,429 +1,354 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/tetrio_player.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart' show teto; -import 'package:tetra_stats/views/compare_view.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'dart:developer' as developer; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; - -const Map xpTableScuffed = { // level: xp required - 05000: 67009018.4885772, - 10000: 763653437.386, - 15000: 2337651144.54149, - 20000: 4572735210.50902, - 25000: 7376166347.04745, - 30000: 10693620096.2168, - 40000: 18728882739.482, - 50000: 28468683855.2853 -}; - -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({super.key, required this.player, required this.showStateTimestamp, required this.setState}); - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - double bannerHeight = bigScreen ? 240 : 120; - double pfpHeight = 128; - int xpTableID = 0; - - while (player.xp > xpTableScuffed.values.toList()[xpTableID]) { - xpTableID++; - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - alignment: Alignment.topCenter, - children: [ - if (player.bannerRevision != null) - Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "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 Container(); - }, - ), - Padding( - padding: EdgeInsets.fromLTRB(8, player.bannerRevision != null ? bannerHeight / 1.4 : 0, 8, bigScreen ? 16 : 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Wrap( - direction: bigScreen ? Axis.horizontal : Axis.vertical, - alignment: WrapAlignment.spaceBetween, - spacing: bigScreen ? 25 : 0, - //mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.center, - clipBehavior: Clip.hardEdge, - children: [ - Wrap( - direction: bigScreen ? Axis.horizontal : Axis.vertical, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: bigScreen ? 20 : 0, - clipBehavior: Clip.hardEdge, - children: [ - Stack( - alignment: Alignment.topCenter, - children: [ - 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(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", - // TODO: osk banner can cause memory leak - 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, pfpHeight - 32, 0, 0), - child: const Icon(Icons.verified), - ) - ], - ), - Column( - children: [ - Text(player.username, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28, - shadows: textShadow, - )), - TextButton( - child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)), - onPressed: () { - copyToClipboard(player.userId); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); - }), - ], - ), - ], - ), - showStateTimestamp - ? Text(t.fetchDate(date: timestamp(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: const Icon( - Icons.person_remove, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ],), - onPressed: () { - teto.deletePlayerToTrack(player.userId).then((value) => setState()); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stoppedBeingTracked))); - }, - ), - Text(t.stopTracking, textAlign: TextAlign.center) - ], - ); - } else { - return Column( - children: [ - IconButton( - icon: const Icon( - Icons.person_add, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ],), - onPressed: () { - teto.addPlayerToTrack(player).then((value) => setState()); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked))); - }, - ), - Text(t.track, textAlign: TextAlign.center) - ], - ); - } - } - }), - Column( - children: [ - IconButton( - icon: const Icon( - Icons.balance, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ],), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CompareView(greenSide: [player, null, null], redSide: const [null, null, null], greenMode: Mode.player, redMode: Mode.player), - ), - ); - }, - ), - Text(t.compare, textAlign: TextAlign.center) - ], - ) - ]) - ]), - ], - ), - ], - ), - ), - ], - ), - if (!["banned", "p1nkl0bst3r"].contains(player.role)) - Wrap( - // mainAxisSize: MainAxisSize.min, - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, // hard WHAT??? - children: [ - if (!player.level.isNegative && !player.level.isNaN) StatCellNum( - playerStat: player.level, - playerStatLabel: t.statCellNum.xpLevel, - isScreenBig: bigScreen, - alertWidgets: [ - Text( - "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(player.xp)} XP", - style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold) - ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: SfLinearGauge( - minimum: 0, - maximum: 1, - interval: 1, - ranges: [ - LinearGaugeRange(startValue: 0, endValue: player.level - player.level.floor(), color: Colors.cyanAccent), - LinearGaugeRange(startValue: 0, endValue: (player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) - ], - // markerPointers: [LinearShapePointer(value: player.level - player.level.floor(), position: LinearElementPosition.inside, shapeType: LinearShapePointerType.triangle, color: Colors.white, height: 20)], - showTicks: true, - showLabels: false - ), - ), - Text("${t.statCellNum.xpProgress}: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"), - Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - player.xp)} ${t.statCellNum.xpLeft})")], - okText: t.popupActions.ok, - higherIsBetter: true, - ), - if (player.gameTime >= Duration.zero) - StatCellNum( - playerStat: player.gameTime.inHours, - playerStatLabel: t.statCellNum.hoursPlayed, - isScreenBig: bigScreen, - alertTitle: t.exactGametime, - alertWidgets: [Text(player.gameTime.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24),)], - higherIsBetter: true,), - if (player.gamesPlayed >= 0) - StatCellNum( - playerStat: player.gamesPlayed, - isScreenBig: bigScreen, - playerStatLabel: t.statCellNum.onlineGames, - higherIsBetter: true,), - if (player.gamesWon >= 0) - StatCellNum( - playerStat: player.gamesWon, - isScreenBig: bigScreen, - playerStatLabel: t.statCellNum.gamesWon, - higherIsBetter: true,), - if (player.friendCount > 0) - StatCellNum( - playerStat: player.friendCount, - isScreenBig: bigScreen, - playerStatLabel: t.statCellNum.friends, - higherIsBetter: true,), - ], - ), - if (player.role == "banned") Text( - t.bigRedBanned, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontWeight: FontWeight.w900, - color: Colors.red, - fontSize: bigScreen ? 60 : 45, - ), - ), - if (player.role == "p1nkl0bst3r") Text( - t.p1nkl0bst3rAlert, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - ) - ), - if (player.badstanding != null && player.badstanding!) - Text( - t.bigRedBadStanding, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontWeight: FontWeight.w900, - color: Colors.red, - fontSize: bigScreen ? 60 : 45, - ), - ), - if (player.role != "p1nkl0bst3r") Padding( - padding: EdgeInsets.only(top: bigScreen ? 8 : 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan(text: "", style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - color: Colors.white, - ), - children: [ - if (player.country != null) TextSpan(text: "${t.countries[player.country]} • "), - TextSpan(text: "${t.playerRole[player.role]}${t.playerRoleAccount}${t.created} ${timestamp(player.registrationTime)}"), - if (player.supporterTier > 0) const TextSpan(text: " • "), - if (player.supporterTier > 0) WidgetSpan(child: Icon(player.supporterTier > 1 ? Icons.star : Icons.star_border, color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - if (player.supporterTier > 0) TextSpan(text: player.supporterTier.toString(), style: TextStyle(color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)) - ] - ) - ), - // Text( - // "${player.country != null ? "${t.countries[player.country]} • " : ""}${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " ${t.botCreatedBy} ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? t.notSupporter : t.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 - ? t.obtainDate(date: timestamp(badge.ts!)) - : t.assignedManualy), - ], - ) - ], - ), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ), - tooltip: badge.label, - icon: Image.asset( - "res/tetrio_badges/${badge.badgeId}.png", - height: 32, - width: 32, - errorBuilder: (context, error, stackTrace) { - developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace); - return Image.network( - kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png", - height: 32, - width: 32, - errorBuilder:(context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 32, width: 32); - } - ); - }, - )) - ], - ), - ], - ); - }); - } -} +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/utils/copy_to_clipboard.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/compare_view_tiles.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:transparent_image/transparent_image.dart'; + +class UserThingy extends StatefulWidget { + final TetrioPlayer player; + final bool showStateTimestamp; + final bool initIsTracking; + final Function setState; + + const UserThingy({super.key, required this.player, required this.initIsTracking, required this.showStateTimestamp, required this.setState}); + + @override + State createState() => _UserThingyState(); +} + +class _UserThingyState extends State with SingleTickerProviderStateMixin { + late AnimationController _addToTrackAnimController; + late Animation _addToTrackAnim; + + @override + void initState(){ + _addToTrackAnimController = AnimationController( + value: widget.initIsTracking ? 1.0 : 0.0, + duration: Durations.extralong4, + vsync: this, + ); + _addToTrackAnim = new Tween( + begin: 0.0, + end: 1.0, + ).animate(new CurvedAnimation( + parent: _addToTrackAnimController, + curve: Cubic(.15,-0.40,.86,-0.39), + reverseCurve: Cubic(0,.99,.99,1.01) + )); + + super.initState(); + } + + @override + void dispose() { + _addToTrackAnimController.dispose(); + super.dispose(); + } + + Color roleColor(String role){ + switch (role){ + case "sysop": + return const Color.fromARGB(255, 23, 165, 133); + case "admin": + return const Color.fromARGB(255, 255, 78, 138); + case "mod": + return const Color.fromARGB(255, 204, 128, 242); + case "halfmod": + return const Color.fromARGB(255, 95, 118, 254); + case "bot": + return const Color.fromARGB(255, 60, 93, 55); + case "banned": + return const Color.fromARGB(255, 248, 28, 28); + default: + return Colors.white10; + } + } + + String fontStyle(int length){ + if (length < 10) return "Eurostile Round Extended"; + else if (length < 13) return "Eurostile Round"; + else return "Eurostile Round Condensed"; + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + return LayoutBuilder(builder: (context, constraints) { + double pfpHeight = 128; + int xpTableID = 0; + + while (widget.player.xp > xpTableScuffed.values.toList()[xpTableID]) { + xpTableID++; + } + + return Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Container( + constraints: const BoxConstraints(maxWidth: 960), + height: widget.player.bannerRevision != null ? 218.0 : 138.0, + child: Stack( + //clipBehavior: Clip.none, + children: [ + // TODO: osk banner can cause memory leak + if (widget.player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${widget.player.userId}&rv=${widget.player.bannerRevision}" : "https://tetr.io/user-content/banners/${widget.player.userId}.jpg?rv=${widget.player.bannerRevision}", + placeholder: kTransparentImage, + fit: BoxFit.cover, + height: 120, + fadeInCurve: Easing.standard, fadeInDuration: Durations.long4 + ), + Positioned( + top: widget.player.bannerRevision != null ? 90.0 : 10.0, + left: 16.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: widget.player.role == "banned" + ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) + : widget.player.avatarRevision != null + ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${widget.player.userId}&rv=${widget.player.avatarRevision}" : "https://tetr.io/user-content/avatars/${widget.player.userId}.jpg?rv=${widget.player.avatarRevision}", + fit: BoxFit.fitHeight, height: 128, placeholder: kTransparentImage, fadeInCurve: Easing.emphasizedDecelerate, fadeInDuration: Durations.long4) + : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight), + ) + ), + Positioned( + top: widget.player.bannerRevision != null ? 120.0 : 40.0, + left: 160.0, + child: Tooltip( + message: "${widget.player.userId}\n(Click to copy user ID)", + child: RichText(text: TextSpan(text: widget.player.username, style: TextStyle( + fontFamily: fontStyle(widget.player.username.length), + fontSize: 28, + ), + recognizer: TapGestureRecognizer()..onTap = (){ + copyToClipboard(widget.player.userId); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); + } + ) + ) + ), + ), + Positioned( + top: widget.player.bannerRevision != null ? 160.0 : 80.0, + left: 160.0, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Chip(label: Text(widget.player.role.toUpperCase(), style: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(widget.player.role))), + ), + RichText( + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: + [ + if (widget.player.friendCount > 0) const WidgetSpan(child: Icon(Icons.person), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (widget.player.friendCount > 0) TextSpan(text: "${intf.format(widget.player.friendCount)} "), + if (widget.player.supporterTier > 0) WidgetSpan(child: Icon(widget.player.supporterTier > 1 ? Icons.star : Icons.star_border, color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (widget.player.supporterTier > 0) TextSpan(text: widget.player.supporterTier.toString(), style: TextStyle(color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)), + ] + ) + ) + ], + ), + ), + Positioned( + top: widget.player.bannerRevision != null ? 193.0 : 113.0, + left: 160.0, + child: SizedBox( + width: 270, + child: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + TextSpan(text: timestamp(widget.player.registrationTime), style: const TextStyle(color: Colors.grey)), + if (widget.player.country != null) TextSpan(text: " • ${t.countries[widget.player.country]}") + ] + ) + ), + ) + ), + Positioned( + top: widget.player.bannerRevision != null ? 126.0 : 46.0, + right: 16.0, + child: RichText( + textAlign: TextAlign.end, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round"), + children: [ + TextSpan(text: "Level ${(widget.player.level.isNegative || widget.player.level.isNaN) ? "---" : intf.format(widget.player.level.floor())}", style: TextStyle(decoration: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: (widget.player.level.isNegative || widget.player.level.isNaN) ? Colors.grey : Colors.white), recognizer: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TapGestureRecognizer()?..onTap = (){ + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text("Level ${intf.format(widget.player.level.floor())}", textAlign: TextAlign.center), + content: SingleChildScrollView( + child: ListBody(children: [ + Text( + "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(widget.player.xp)} XP", + style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold) + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: SfLinearGauge( + minimum: 0, + maximum: 1, + interval: 1, + ranges: [ + LinearGaugeRange(startValue: 0, endValue: widget.player.level - widget.player.level.floor(), color: Colors.cyanAccent), + LinearGaugeRange(startValue: 0, endValue: (widget.player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) + ], + showTicks: true, + showLabels: false + ), + ), + Text("${t.statCellNum.xpProgress}: ${((widget.player.level - widget.player.level.floor()) * 100).toStringAsFixed(2)} %"), + Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((widget.player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - widget.player.xp)} ${t.statCellNum.xpLeft})") + ] + ), + ), + actions: [ + TextButton( + child: const Text("OK"), + onPressed: () {Navigator.of(context).pop();} + ) + ] + ) + ); + }), + const TextSpan(text:"\n"), + TextSpan(text: widget.player.gameTime.isNegative ? "-h --m" : playtime(widget.player.gameTime), style: TextStyle(color: widget.player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: widget.player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !widget.player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ + Duration accountAge = DateTime.timestamp().difference(widget.player.registrationTime); + Duration avgGametimeADay = Duration(microseconds: (widget.player.gameTime.inMicroseconds / accountAge.inDays).floor()); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(t.exactGametime, textAlign: TextAlign.center), + content: SingleChildScrollView( + child: Column( + children: [ + RichText(text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white, fontSize: 28), + children: [ + TextSpan(text: "${intf.format(widget.player.gameTime.inHours)}"), + TextSpan(text: ":${nonsecs.format(widget.player.gameTime.inMinutes%60)}:${nonsecs.format(widget.player.gameTime.inSeconds%60)}"), + TextSpan(text: ".${nonsecs3.format(widget.player.gameTime.inMicroseconds%1000000)}", style: TextStyle(fontSize: 14)) + ] + )), + Text("${playtime(avgGametimeADay)} a day in average"), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text("It's ${f4.format(widget.player.gameTime.inSeconds/31536000)} years,"), + ), + Text("or ${f4.format(widget.player.gameTime.inSeconds/2628000)} months,"), + Text("or ${f4.format(widget.player.gameTime.inSeconds/86400)} days,"), + Text("or ${f2.format(widget.player.gameTime.inMilliseconds/60000)} minutes,"), + Text("or ${intf.format(widget.player.gameTime.inSeconds)} seconds"), + ] + ), + ), + actions: [ + TextButton( + child: const Text("OK"), + onPressed: () {Navigator.of(context).pop();} + ) + ] + ) + ); + }) : null), + const TextSpan(text:"\n"), + TextSpan(text: widget.player.gamesWon > -1 ? intf.format(widget.player.gamesWon) : "---", style: TextStyle(color: widget.player.gamesWon > -1 ? Colors.white : Colors.grey)), + TextSpan(text: "/${widget.player.gamesPlayed > -1 ? intf.format(widget.player.gamesPlayed) : "---"}", style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), + ] + ) + ) + ) + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: AnimatedBuilder( + animation: _addToTrackAnim, + builder: (context, child) { + double firstButtonPosition = 0+(_addToTrackAnim.value as double)*25; + double secondButtonPosition = -25+(_addToTrackAnim.value as double)*25; + double firstButtonOpacity = 1-(_addToTrackAnim.value as double)*2; + double secondButtonOpacity = _addToTrackAnim.value*2-1; + return ElevatedButton.icon( + onPressed: (){ + _addToTrackAnimController.value == 1 ? teto.deletePlayerToTrack(widget.player.userId) : teto.addPlayerToTrack(widget.player); + _addToTrackAnim.isCompleted ? _addToTrackAnimController.reverse() : _addToTrackAnimController.forward(); + }, + icon: _addToTrackAnim.value < 0.5 ? Opacity( + opacity: min(1, firstButtonOpacity), + child: Transform.translate( + offset: Offset(0, _addToTrackAnim.status == AnimationStatus.forward ? firstButtonPosition*4 : firstButtonPosition), + child: Transform.rotate( + angle:_addToTrackAnim.status == AnimationStatus.forward ? (_addToTrackAnim.value as double)*2 : 0, + child: const Icon(Icons.person_add), + ), + ), + ) : Container( + transform: Matrix4.translationValues(secondButtonPosition*5, -secondButtonPosition*25, 0), + child: Opacity( + opacity: max(0, min(1, secondButtonOpacity)), + child: Transform.rotate( + angle:_addToTrackAnim.status == AnimationStatus.reverse ? (1-_addToTrackAnim.value as double)*-20 : 0, + child: const Icon(Icons.person_remove) + ) + ) + ), + label: _addToTrackAnim.value < 0.5 ? Container( + transform: Matrix4.translationValues(0, firstButtonPosition, 0), + child: Opacity( + opacity: max(min(1, firstButtonOpacity), 0), + child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.forward ? "Done!" : "Track") + ) + ) : Container( + transform: Matrix4.translationValues(0, secondButtonPosition, 0), + child: Opacity( + opacity: max(0, min(1, secondButtonOpacity)), + child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.reverse ? "Done! " : "Stop tracking") + ) + ), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0)))))); + }, + )), + Expanded( + child: ElevatedButton.icon( + onPressed: (){ + Navigator.push(context, MaterialPageRoute( + builder: (context) => CompareView(widget.player), + ), + ); + }, + icon: const Icon(Icons.balance), + label: Text(t.compare), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))) + ) + ) + ], + ) + ], + ), + ); + }); + } +} diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index d1954d0..ee0692a 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -1,284 +1,151 @@ import 'package:flutter/material.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/record_extras.dart'; import 'package:tetra_stats/data_objects/record_single.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; -import 'package:tetra_stats/widgets/gauget_num.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; +import 'package:tetra_stats/widgets/gauget_thingy.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; -class ZenithThingy extends StatefulWidget{ - final RecordSingle? record; - final bool switchable; - final bool initEXvalue; - final RecordSingle? recordEX; - final Function? parentZenithToggle; - - const ZenithThingy({super.key, this.record, this.recordEX, this.switchable = true, this.parentZenithToggle, this.initEXvalue = false}); - - @override - State createState() => _ZenithThingyState(); -} - -class _ZenithThingyState extends State { - late RecordSingle? record; - bool ex = false; - - @override - void initState(){ - ex = widget.initEXvalue; - - super.initState(); - if (widget.switchable){ - record = (ex ? widget.recordEX : widget.record); - }else{ - record = widget.record; - ex = widget.record!.gamemode == "zenithex"; - } - } +class ZenithThingy extends StatelessWidget{ + final RecordSingle? zenith; + final bool old; + const ZenithThingy({super.key, required this.zenith, this.old = false}); + @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints){ - bool bigScreen = constraints.maxWidth > 768; - if (record == null) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: "--- m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.grey), - ), - ), - TextButton(onPressed: (){ - if (ex){ - ex = false; - }else{ - ex = true; - } - setState(() { - if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); - record = ex ? widget.recordEX : widget.record; - }); - }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), - ], - ), - ); - } - return Padding(padding: const EdgeInsets.only(top: 8.0), + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), child: Column( children: [ - Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: "${f2.format(record!.stats.zenith!.altitude)} m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - if ((record!.extras as ZenithExtras).mods.isNotEmpty) RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.white), - children: [ - TextSpan(text: "${t.withMods}: "), - for (String mod in (record!.extras as ZenithExtras).mods) TextSpan(text: "${mod.toUpperCase()} "), - ] - ), - ), - RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), - if (record!.rank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), - if (record!.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(widget.record!.timestamp)), - ] - ), - ), - if (widget.switchable) TextButton(onPressed: (){ - if (ex){ - ex = false; - }else{ - ex = true; - } - setState(() { - if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); - record = ex ? widget.recordEX : widget.record; - }); - }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), - Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 20, + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "KO's", isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "Climb speed\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.stats.topBtB, playerStatLabel: "Top B2B\nchain", isScreenBig: bigScreen, higherIsBetter: true) - ], - ), - FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), - LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: 300, - child: Column( + Column( mainAxisSize: MainAxisSize.min, children: [ - Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - const Text("T", style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 65, - height: 1.2, - )), - const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text(getMoreNormalTime(record!.stats.finalTime), style: const TextStyle( - shadows: textShadow, - fontFamily: "Eurostile Round Extended", - fontSize: 36, - fontWeight: FontWeight.w500, - color: Colors.white - )), - ) - ], + RichText( + text: TextSpan( + text: zenith != null ? "${f2.format(zenith!.stats.zenith!.altitude)} m" : "--- m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: (zenith != null && !old) ? Colors.white : Colors.grey), + ), ), - Table( - columnWidths: const { - 0: FixedColumnWidth(36) - }, - children: [ - const TableRow( - children: [ - Text("Floor"), - Text("Split", textAlign: TextAlign.right), - Text("Total", textAlign: TextAlign.right), - ] - ), - for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( - children: [ - Text((i+1).toString()), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), - ] - ) - ], + if (zenith != null) RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (zenith!.rank != -1) TextSpan(text: "№ ${intf.format(zenith!.rank)}", style: TextStyle(color: getColorOfRank(zenith!.rank))), + if (zenith!.rank != -1) const TextSpan(text: " • "), + if (zenith!.countryRank != -1) TextSpan(text: "№ ${intf.format(zenith!.countryRank)} local", style: TextStyle(color: getColorOfRank(zenith!.countryRank))), + if (zenith!.countryRank != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(zenith!.timestamp)), + ] + ), ), ], ), - ), - ), - Column( - children: [ - Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 35, - crossAxisAlignment: WrapCrossAlignment.start, - //clipBehavior: Clip.hardEdge, - children: [ - GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}") - ]), - GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}") - ]) - ]), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - //clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true,), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: false), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true) - ]), - ) + if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) Container(width: 16.0), + if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) for (String mod in (zenith!.extras as ZenithExtras).mods) Image.asset("res/icons/${mod}.png", height: 64.0) ], ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), + if (zenith != null) Row( + children: [ + Expanded( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(f2.format(zenith!.aggregateStats.apm), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" APM", style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(zenith!.aggregateStats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" PPS", style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(zenith!.aggregateStats.vs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" VS", style: TextStyle(fontSize: 21)), + ]) + ], + ), + ), + ), + GaugetThingy(value: zenith!.stats.cps, min: 0, max: 12, tickInterval: 3, label: "Climb\nSpeed", subString: "Peak: ${f2.format(zenith!.stats.zenith!.peakrank)}", sideSize: 128, fractionDigits: 2, moreIsBetter: true), + Expanded( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(intf.format(zenith!.stats.kills), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" KO's", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text(zenith!.stats.topBtB.toString(), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" B2B", style: TextStyle(fontSize: 21)) + ]), + TableRow(children: [ + Text(zenith!.stats.garbage.maxspike_nomult.toString(), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Top spike", style: TextStyle(fontSize: 21)) + ]) + ], + ), + ), + ) + ], + ) else Row( + children: [ + Expanded( + child: Center( + child: Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: [ + const TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" APM", style: TextStyle(fontSize: 21, color: Colors.grey)), + ]), + const TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" PPS", style: TextStyle(fontSize: 21, color: Colors.grey)), + ]), + const TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" VS", style: TextStyle(fontSize: 21, color: Colors.grey)), + ]) + ], + ), + ), + ), + GaugetThingy(value: null, min: 0, max: 12, tickInterval: 3, label: "Climb\nSpeed", subString: "Peak: ---", sideSize: 128, fractionDigits: 0, moreIsBetter: true), + Expanded( + child: Center( + child: Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: [ + const TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" KO's", style: TextStyle(fontSize: 21, color: Colors.grey)) + ]), + const TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" B2B", style: TextStyle(fontSize: 21, color: Colors.grey)) + ]), + const TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: Colors.grey)), + Text(" Top spike", style: TextStyle(fontSize: 21, color: Colors.grey)) + ]) + ], + ), + ), + ) + ], ) - ], - ) - ); - }); + ] + ), + ) + ); } } \ No newline at end of file diff --git a/res/images/info card 1 focus.png b/res/images/info card 1 focus.png new file mode 100644 index 0000000000000000000000000000000000000000..658da5246e4b0c62b955e4debd3f8cdbff90f2a6 GIT binary patch literal 82812 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 literal 0 HcmV?d00001 diff --git a/res/images/info card 1.png b/res/images/info card 1.png new file mode 100644 index 0000000000000000000000000000000000000000..16296281c799571b530835d944be69b5a323344c GIT binary patch literal 70091 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