Big refactoring, upated design of nerd stats, info center ideas
This commit is contained in:
parent
647d1f87f4
commit
6c8e7b9147
|
@ -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<int, double> 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<String> ranks = [
|
||||
"d",
|
||||
"d+",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> copyToClipboard(String text) async {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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<StatefulWidget> createState() => CalcState();
|
||||
}
|
||||
|
||||
class CalcState extends State<CalcView> {
|
||||
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)));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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<StatefulWidget> createState() => CustomizationState();
|
||||
}
|
||||
|
||||
class CustomizationState extends State<CustomizationView> {
|
||||
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<DropdownMenuItem<AppLocale>>? locales =
|
||||
<DropdownMenuItem<AppLocale>>[];
|
||||
for (var v in AppLocale.values) {
|
||||
locales.add(DropdownMenuItem<AppLocale>(
|
||||
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: <Widget>[
|
||||
ElevatedButton(
|
||||
child: const Text('Set'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
context.findAncestorStateOfType<MyAppState>()?.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>[
|
||||
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: <DropdownMenuItem>[
|
||||
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;
|
||||
});
|
||||
}),)
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<DestinationCalculator> {
|
|||
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")
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<DestinationGraphs> {
|
|||
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,
|
||||
|
|
|
@ -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<DestinationHome> 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<DestinationHome> 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<DestinationHome> 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),
|
||||
|
|
|
@ -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<DestinationInfo> 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<DestinationInfo> {
|
||||
@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()
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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<DestinationSettings> with SingleTickerP
|
|||
late bool showPositions;
|
||||
late bool showAverages;
|
||||
late bool updateInBG;
|
||||
final TextEditingController _playertext = TextEditingController();
|
||||
late AnimationController _defaultNicknameAnimController;
|
||||
late Animation _goodDefaultNicknameAnim;
|
||||
late Animation _badDefaultNicknameAnim;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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<StatefulWidget> createState() => MatchesState();
|
||||
}
|
||||
|
||||
class MatchesState extends State<MatchesView> {
|
||||
|
||||
@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)))],
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 = <DropdownMenuItem>[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<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
|
||||
List<_MyScatterSpot> _spots = [];
|
||||
Stats _sortBy = Stats.tr;
|
||||
late List<TetrioPlayerFromLeaderboard> they;
|
||||
bool _reversed = false;
|
||||
List<DropdownMenuItem> _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<StatefulWidget> createState() => RankState();
|
||||
}
|
||||
|
||||
class RankState extends State<RankView> 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<int> hoveredPointId = ValueNotifier<int>(-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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<StatefulWidget> createState() => RanksAverages();
|
||||
}
|
||||
|
||||
late String oldWindowTitle;
|
||||
|
||||
class RanksAverages extends State<RankAveragesView> {
|
||||
|
||||
@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<CutoffsTetrio?>(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");
|
||||
}
|
||||
})
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<StatefulWidget> createState() => SettingsState();
|
||||
}
|
||||
|
||||
class SettingsState extends State<SettingsView> {
|
||||
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<void> _setDefaultNickname(String? n) async {
|
||||
if (n != null) {
|
||||
try {
|
||||
defaultNickname = await teto.getNicknameByID(n);
|
||||
} on TetrioPlayerNotExist {
|
||||
defaultNickname = n;
|
||||
}
|
||||
} else {
|
||||
defaultNickname = "dan63047";
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _setPlayer(String player) async {
|
||||
await prefs.setString('player', player);
|
||||
await _setDefaultNickname(player);
|
||||
}
|
||||
|
||||
Future<void> _removePlayer() async {
|
||||
await prefs.remove('player');
|
||||
await _setDefaultNickname("6098518e3d5155e6ec429cdc");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
List<DropdownMenuItem<AppLocale>>? locales = <DropdownMenuItem<AppLocale>>[];
|
||||
for (var v in AppLocale.values){
|
||||
locales.add(DropdownMenuItem<AppLocale>(
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <String>['db'],
|
||||
);
|
||||
openFile(acceptedTypeGroups: <XTypeGroup>[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: <Widget>[
|
||||
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: (){},),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -40,8 +40,14 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
|||
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(
|
||||
|
|
|
@ -49,7 +49,7 @@ class StateState extends State<StateView> {
|
|||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: TLThingy(tl: widget.state, userID: widget.state.id, states: [])
|
||||
child: TetraLeagueThingy(league: widget.state)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<StatefulWidget> createState() => StatesState();
|
||||
}
|
||||
|
||||
late String oldWindowTitle;
|
||||
|
||||
class StatesState extends State<StatesView> {
|
||||
@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<List<TetraLeague>>(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));
|
||||
}
|
||||
)));}
|
||||
}
|
|
@ -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<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
|
||||
Stats _sortBy = Stats.tr;
|
||||
bool reversed = false;
|
||||
List<DropdownMenuItem> _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<StatefulWidget> createState() => TLLeaderboardState();
|
||||
}
|
||||
|
||||
class TLLeaderboardState extends State<TLLeaderboardView> {
|
||||
@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");
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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<StatefulWidget> createState() => TrackedPlayersState();
|
||||
}
|
||||
|
||||
class TrackedPlayersState extends State<TrackedPlayersView> {
|
||||
@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) => <PopupMenuEntry>[
|
||||
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<String, String> : <String, String>{};
|
||||
List<String> 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]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}));
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<Badge> 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<void>(
|
||||
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: <Widget>[
|
||||
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);
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>[
|
||||
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>[
|
||||
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,
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<InlineSpan> 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<InlineSpan> 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<double>(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()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<double>(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()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<GaugeRange> ranges;
|
||||
final double minimum;
|
||||
final double maximum;
|
||||
final String playerStatLabel;
|
||||
final String? okText;
|
||||
final String? alertTitle;
|
||||
final List<Widget>? 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: <Widget>[
|
||||
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)],
|
||||
)],),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<String, String> 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)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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)),
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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?");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<TetraLeague> 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<TLThingy> createState() => _TLThingyState();
|
||||
}
|
||||
|
||||
class _TLThingyState extends State<TLThingy> with TickerProviderStateMixin {
|
||||
late bool oskKagariGimmick;
|
||||
late TetraLeague? oldTl;
|
||||
late TetraLeague currentTl;
|
||||
late RangeValues _currentRangeValues;
|
||||
late List<TetraLeague> 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<String> estTRformated = currentTl.estTr != null ? f2.format(currentTl.estTr!.esttr).split(decimalSeparator) : [];
|
||||
List<String> 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))
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<int, double> 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<void> 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>[
|
||||
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>[
|
||||
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>[
|
||||
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<void>(
|
||||
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: <Widget>[
|
||||
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<UserThingy> createState() => _UserThingyState();
|
||||
}
|
||||
|
||||
class _UserThingyState extends State<UserThingy> 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: <Widget>[
|
||||
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: <Widget>[
|
||||
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)))))
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ZenithThingy> createState() => _ZenithThingyState();
|
||||
}
|
||||
|
||||
class _ZenithThingyState extends State<ZenithThingy> {
|
||||
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))
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
});
|
||||
]
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
Loading…
Reference in New Issue