diff --git a/lib/views/destination_calculator.dart b/lib/views/destination_calculator.dart new file mode 100644 index 0000000..b240c99 --- /dev/null +++ b/lib/views/destination_calculator.dart @@ -0,0 +1,602 @@ +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncfusion_flutter_gauges/gauges.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/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'; + +class DestinationCalculator extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationCalculator({super.key, required this.constraints}); + + @override + State createState() => _DestinationCalculatorState(); +} + +enum CalcCards{ + calc, + damage +} + +class ClearData{ + final String title; + final Lineclears lineclear; + final int lines; + final bool miniSpin; + final bool spin; + bool perfectClear = false; + int id = -1; + + ClearData(this.title, this.lineclear, this.lines, this.miniSpin, this.spin); + + ClearData cloneWith(int i){ + ClearData newOne = ClearData(title, lineclear, lines, miniSpin, spin)..id = i; + return newOne; + } + + bool get difficultClear { + if (lines == 0) return false; + if (lines >= 4 || miniSpin || spin) return true; + else return false; + } + + void togglePC(){ + perfectClear = !perfectClear; + } + + int dealsDamage(int combo, int b2b, int previousB2B, Rules rules){ + if (lines == 0) return 0; + double damage = 0; + + if (spin){ + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); + } else if (miniSpin){ + damage += garbage[lineclear]!; + } else { + if (lines <= 5) damage += garbage[lineclear]!; + else damage += garbage[Lineclears.PENTA]! + (lines - 5); + } + + if (difficultClear && b2b >= 1 && rules.b2b){ + if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log(1 + (b2b) * BACKTOBACK_BONUS_LOG)).floor() + (b2b == 1 ? 0 : (1 + log(1 +(b2b) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? + else damage += 1; // if b2b chaining off + } + + if (rules.combo && rules.comboTable != ComboTables.none) { + if (combo >= 1){ + if (lines == 1 && rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; + else damage *= (1 + COMBO_BONUS * (combo)); + } + if (combo >= 2) { + damage = max(log(1 + COMBO_MINIFIER * (combo) * COMBO_MINIFIER_LOG), damage); + } + } + + if (!difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1){ + damage += rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b); + } + + if (perfectClear) damage += rules.pcDamage; + + return (damage * rules.multiplier).floor(); + } +} + +Map> clearsExisting = { + "No Spin Clears": [ + ClearData("No lineclear (Break Combo)", Lineclears.ZERO, 0, false, false), + ClearData("Single", Lineclears.SINGLE, 1, false, false), + ClearData("Double", Lineclears.DOUBLE, 2, false, false), + ClearData("Triple", Lineclears.TRIPLE, 3, false, false), + ClearData("Quad", Lineclears.QUAD, 4, false, false) + ], + "Spins": [ + ClearData("Spin Zero", Lineclears.TSPIN, 0, false, true), + ClearData("Spin Single", Lineclears.TSPIN_SINGLE, 1, false, true), + ClearData("Spin Double", Lineclears.TSPIN_DOUBLE, 2, false, true), + ClearData("Spin Triple", Lineclears.TSPIN_TRIPLE, 3, false, true), + ClearData("Spin Quad", Lineclears.TSPIN_QUAD, 4, false, true), + ], + "Mini spins": [ + ClearData("Mini Spin Zero", Lineclears.TSPIN_MINI, 0, true, false), + ClearData("Mini Spin Single", Lineclears.TSPIN_MINI_SINGLE, 1, true, false), + ClearData("Mini Spin Double", Lineclears.TSPIN_MINI_DOUBLE, 2, true, false), + ClearData("Mini Spin Triple", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false), + ] +}; + +class Rules{ + bool combo = true; + bool b2b = true; + bool b2bChaining = false; + bool surge = true; + int surgeInitAmount = 4; + int surgeInitAtB2b = 4; + ComboTables comboTable = ComboTables.multiplier; + int pcDamage = 5; + int pcB2B = 1; + double multiplier = 1.0; +} + +const TextStyle mainToggleInRules = TextStyle(fontSize: 18, fontWeight: ui.FontWeight.w800); + +class _DestinationCalculatorState extends State { + double? apm; + double? pps; + double? vs; + NerdStats? nerdStats; + EstTr? estTr; + Playstyle? playstyle; + TextEditingController ppsController = TextEditingController(); + TextEditingController apmController = TextEditingController(); + TextEditingController vsController = TextEditingController(); + + List clears = []; + Map customClearsChoice = { + "No Spin Clears": 5, + "Spins": 5 + }; + int idCounter = 0; + Rules rules = Rules(); + + CalcCards card = CalcCards.calc; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + 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"))); + } + } + + // void calcDamage(){ + // for (ClearData lineclear in clears){ + + // } + // } + + Widget getCalculator(){ + return SingleChildScrollView( + child: Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Stats Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: apmController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("APM"), alignLabelWithHint: true, hintText: "Enter your APM"), + ), + ) + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: ppsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("PPS"), alignLabelWithHint: true, hintText: "Enter your PPS"), + ), + ) + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), + child: TextField( + onSubmitted: (value) => calc(), + controller: vsController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffix: Text("VS"), alignLabelWithHint: true, hintText: "Enter your VS"), + ), + ) + ), + TextButton( + onPressed: () => calc(), + child: Text(t.calc), + ), + ], + ), + ), + ), + if (nerdStats != null) Card( + child: NerdStatsThingy(nerdStats: nerdStats!) + ), + if (playstyle != null) Card( + child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!) + ) + ], + ), + ); + } + + Widget getDamageCalculator(){ + List rSideWidgets = []; + List lSideWidgets = []; + + for (var key in clearsExisting.keys){ + rSideWidgets.add(Text(key)); + for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( + child: ListTile( + title: Text(data.title), + subtitle: Text("${data.dealsDamage(0, 0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), + trailing: Icon(Icons.arrow_forward_ios), + onTap: (){ + setState((){ + clears.add(data.cloneWith(idCounter)); + }); + idCounter++; + }, + ), + )); + if (key != "Mini spins") rSideWidgets.add(Card( + child: ListTile( + title: Text("Custom"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 30.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: "5"), + onChanged: (value) => customClearsChoice[key] = int.parse(value), + )), + Text(" Lines", style: TextStyle(fontSize: 18)), + Icon(Icons.arrow_forward_ios) + ], + ), + onTap: (){ + setState((){ + clears.add(ClearData("${key == "Spins" ? "Spin " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} Lines)", key == "Spins" ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, customClearsChoice[key]!, false, key == "Spins").cloneWith(idCounter)); + }); + idCounter++; + }, + ), + )); + rSideWidgets.add(const Divider()); + } + + int combo = -1; + int b2b = -1; + int previousB2B = -1; + int totalDamage = 0; + int normalDamage = 0; + int comboDamage = 0; + int b2bDamage = 0; + int surgeDamage = 0; + int pcDamage = 0; + + for (ClearData lineclear in clears){ + previousB2B = b2b; + if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; + if (lineclear.lines > 0) combo++; else combo = -1; + int pcDmg = lineclear.perfectClear ? (rules.pcDamage * rules.multiplier).floor() : 0; + int normalDmg = lineclear.dealsDamage(0, 0, 0, rules) - pcDmg; + int surgeDmg = (!lineclear.difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1) ? rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b) : 0; + int b2bDmg = lineclear.dealsDamage(0, b2b, b2b-1, rules) - normalDmg - pcDmg; + int dmg = lineclear.dealsDamage(combo, b2b, previousB2B, rules); + int comboDmg = dmg - normalDmg - b2bDmg - surgeDmg - pcDmg; + lSideWidgets.add( + ListTile( + key: ValueKey(lineclear.id), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)), + if (lineclear.lines > 0) IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.local_parking_outlined, color: lineclear.perfectClear ? Colors.white : Colors.grey.shade800)), + ], + ), + title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), + subtitle: lineclear.lines > 0 ? Text("${dmg == normalDmg ? "No bonuses" : ""}${b2bDmg > 0 ? "+${intf.format(b2bDmg)} for B2B" : ""}${(b2bDmg > 0 && comboDmg > 0) ? ", " : ""}${comboDmg > 0 ? "+${intf.format(comboDmg)} for combo" : ""}${(comboDmg > 0 && lineclear.perfectClear) ? ", " : ""}${lineclear.perfectClear ? "+${intf.format(pcDmg)} for PC" : ""}${(surgeDmg > 0 && (lineclear.perfectClear || comboDmg > 0)) ? ", " : ""}${surgeDmg > 0 ? "Surge released: +${intf.format(surgeDmg)}" : ""}", style: TextStyle(color: Colors.grey)) : null, + trailing: lineclear.lines > 0 ? Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), + ) : null, + ) + ); + totalDamage += dmg; + normalDamage += normalDmg; + comboDamage += comboDmg; + b2bDamage += b2bDmg; + surgeDamage += surgeDmg; + pcDamage += pcDmg; + } + // values for "the bar" + double sec2end = normalDamage.toDouble()+comboDamage.toDouble(); + double sec3end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble(); + double sec4end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble(); + double sec5end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble()+pcDamage.toDouble(); + return Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Damage Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + )), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 350.0, + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: TabBar(tabs: [ + Tab(text: "Actions"), + Tab(text: "Rules"), + ]), + ), + SizedBox( + height: widget.constraints.maxHeight - 164, + child: TabBarView(children: [ + SingleChildScrollView( + child: Column( + children: rSideWidgets, + ), + ), + SingleChildScrollView( + child: Column( + children: [ + Card( + child: ListTile( + title: Text("Multiplier", style: mainToggleInRules), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))], + decoration: InputDecoration(hintText: rules.multiplier.toString()), + onChanged: (value) => setState((){rules.multiplier = double.parse(value);}), + )), + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Combo", style: mainToggleInRules), + trailing: Switch(value: rules.combo, onChanged: (v) => setState((){rules.combo = v;})), + ), + if (rules.combo) ListTile( + title: Text("Combo Table"), + trailing: DropdownButton( + items: [for (var v in ComboTables.values) DropdownMenuItem(value: v.index, child: Text(v.name))], + value: rules.comboTable.index, + onChanged: (v) => setState((){rules.comboTable = ComboTables.values[v!];}), + ), + ) + ], + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Back-To-Back (B2B)", style: mainToggleInRules), + trailing: Switch(value: rules.b2b, onChanged: (v) => setState((){rules.b2b = v;})), + ), + if (rules.b2b) ListTile( + title: Text("Back-To-Back Chaining"), + trailing: Switch(value: rules.b2bChaining, onChanged: (v) => setState((){rules.b2bChaining = v;})), + ), + ], + ), + ), + Card( + child: Column( + children: [ + ListTile( + title: Text("Surge", style: mainToggleInRules), + trailing: Switch(value: rules.surge, onChanged: (v) => setState((){rules.surge = v;})), + ), + if (rules.surge) ListTile( + title: Text("Starts at B2B"), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: rules.surgeInitAtB2b.toString()), + onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}), + )), + ), + if (rules.surge) ListTile( + title: Text("Start amount"), + trailing: SizedBox(width: 90.0, child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(hintText: rules.surgeInitAmount.toString()), + onChanged: (value) => setState((){rules.surgeInitAmount = int.parse(value);}), + )), + ), + ], + ), + ) + ], + ), + ) + ]), + ) + ], + ) + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 350 - 80, + height: widget.constraints.maxHeight - 108, + child: clears.isEmpty ? Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), + SizedBox(height: 5.0), + Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center), + ], + )) : + Card( + child: Column( + children: [ + Expanded( + child: ReorderableListView( + onReorder: (oldIndex, newIndex) { + setState((){ + if (oldIndex < newIndex) { + newIndex -= 1; + } + final ClearData item = clears.removeAt(oldIndex); + clears.insert(newIndex, item); + }); + }, + children: lSideWidgets, + ), + ), + Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 34.0, 0.0), + child: Row( + children: [ + Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), + Spacer(), + Text(intf.format(totalDamage), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text("Lineclears: ${intf.format(normalDamage)}"), + Text("Combo: ${intf.format(comboDamage)}"), + Text("B2B: ${intf.format(b2bDamage)}"), + Text("Surge: ${intf.format(surgeDamage)}"), + Text("PC's: ${intf.format(pcDamage)}") + ], + ), + SfLinearGauge( + minimum: 0, + maximum: totalDamage.toDouble(), + showLabels: false, + showTicks: false, + ranges: [ + LinearGaugeRange( + color: Colors.green, + startValue: 0, + endValue: normalDamage.toDouble(), + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.yellow, + startValue: normalDamage.toDouble(), + endValue: sec2end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.blue, + startValue: sec2end, + endValue: sec3end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.red, + startValue: sec3end, + endValue: sec4end, + position: LinearElementPosition.cross, + ), + LinearGaugeRange( + color: Colors.orange, + startValue: sec4end, + endValue: sec5end, + position: LinearElementPosition.cross, + ), + ], + ), + ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) + ], + ) + ], + ), + ), + ) + ], + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: widget.constraints.maxHeight -32, + child: switch (card){ + CalcCards.calc => getCalculator(), + CalcCards.damage => getDamageCalculator() + } + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: CalcCards.calc, + label: Text('Stats Calculator'), + ), + ButtonSegment( + value: CalcCards.damage, + label: Text('Damage Calculator'), + ), + ], + selected: {card}, + onSelectionChanged: (Set newSelection) { + setState(() { + card = newSelection.first; + });}) + ], + ); + } + +} diff --git a/lib/views/destination_cutoffs.dart b/lib/views/destination_cutoffs.dart new file mode 100644 index 0000000..7c014a9 --- /dev/null +++ b/lib/views/destination_cutoffs.dart @@ -0,0 +1,293 @@ +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'; +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/widgets/text_timestamp.dart'; +import 'package:vector_math/vector_math_64.dart' hide Colors; + +class FetchCutoffsResults{ + late bool success; + CutoffsTetrio? cutoffs; + Exception? exception; + + FetchCutoffsResults(this.success, this.cutoffs, this.exception); +} + +class DestinationCutoffs extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationCutoffs({super.key, required this.constraints}); + + @override + State createState() => _DestinationCutoffsState(); +} + +class _DestinationCutoffsState extends State { + + Future fetch() async { + TetrioPlayerFromLeaderboard top1; + CutoffsTetrio cutoffs; + List requests = await Future.wait([ + teto.fetchCutoffsTetrio(), + teto.fetchTopOneFromTheLeaderboard(), + ]); + cutoffs = requests[0]; + top1 = requests[1]; + cutoffs.data["top1"] = CutoffTetrio( + pos: 1, + percentile: 0.00, + tr: top1.tr, + targetTr: 25000, + apm: top1.apm, + pps: top1.pps, + vs: top1.vs, + count: 1, + countPercentile: 0.0 + ); + return cutoffs; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: fetch(), + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return SingleChildScrollView( + child: Column( + children: [ + Card( + child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + children: [ + Text("Tetra League State", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + Text("as of ${timestamp(snapshot.data!.timestamp)}"), + ], + ), + )), + ), + Padding( + padding: const EdgeInsets.only(bottom:4.0), + child: Card( + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text("Actual"), + ), + Text("Target") + ] + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), + child: SfLinearGauge( + minimum: 0.00000000, + maximum: 25000.0000, + showTicks: false, + showLabels: false, + ranges: [ + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.outside, + startValue: snapshot.data!.data[cutoff]!.tr, + startWidth: 20.0, + endWidth: 20.0, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.tr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr + }, + color: cutoff != "top1" ? rankColors[cutoff] : Colors.grey.shade800, + ), + for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( + position: LinearElementPosition.inside, + startValue: snapshot.data!.data[cutoff]!.targetTr, + endValue: switch (cutoff){ + "top1" => 25000.00, + "x+" => snapshot.data!.data["top1"]!.targetTr, + _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr + }, + color: cutoff != "top1" ? rankColors[cutoff] : null, + ), + for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[cutoff]!.tr < snapshot.data!.data[cutoff]!.targetTr) LinearGaugeRange( + position: LinearElementPosition.cross, + startValue: snapshot.data!.data[cutoff]!.tr, + endValue: snapshot.data!.data[cutoff]!.targetTr, + color: Colors.green, + ), + for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr)LinearGaugeRange( + position: LinearElementPosition.cross, + startValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr, + endValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr, + color: Colors.red, + ), + ], + markerPointers: [ + for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.tr), style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(0, 35, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0), value: snapshot.data!.data[cutoff]!.tr, position: LinearElementPosition.outside, offset: 20), + for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.targetTr), textAlign: ui.TextAlign.right, style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(-15, 0, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0, transformAlignment: Alignment.topRight), value: snapshot.data!.data[cutoff]!.targetTr, position: LinearElementPosition.inside, offset: 6) + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all(color: Colors.grey.shade900), + columnWidths: const { + 0: FixedColumnWidth(48), + 1: FixedColumnWidth(155), + 2: FixedColumnWidth(140), + 3: FixedColumnWidth(160), + 4: FixedColumnWidth(150), + 5: FixedColumnWidth(90), + 6: FixedColumnWidth(130), + 7: FixedColumnWidth(120), + 8: FixedColumnWidth(125), + 9: FixedColumnWidth(70), + }, + children: [ + TableRow( + children: [ + Text("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("Cutoff 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("Target TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("State", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, 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)), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text("More info", 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) if (rank != "top1") 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(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, 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: [ + if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), + TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), + if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) + else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) + ] + )), + ), + 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)) + ] + )) + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { + + },), + ), + ] + ) + ], + ) + ] + ), + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + } + ); + } +} diff --git a/lib/views/destination_graphs.dart b/lib/views/destination_graphs.dart new file mode 100644 index 0000000..6073bac --- /dev/null +++ b/lib/views/destination_graphs.dart @@ -0,0 +1,475 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_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/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/widgets/text_timestamp.dart'; + +class DestinationGraphs extends StatefulWidget{ + final String searchFor; + //final Function setState; + final BoxConstraints constraints; + + const DestinationGraphs({super.key, required this.searchFor, required this.constraints}); + + @override + State createState() => _DestinationGraphsState(); +} + +enum Graph{ + history, + leagueState, + leagueCutoffs +} + +class _DestinationGraphsState extends State { + bool fetchData = false; + bool _gamesPlayedInsteadOfDateAndTime = false; + late ZoomPanBehavior _zoomPanBehavior; + late TooltipBehavior _historyTooltipBehavior; + late TooltipBehavior _tooltipBehavior; + late TooltipBehavior _leagueTooltipBehavior; + String yAxisTitle = ""; + bool _smooth = false; + final List> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; + Graph _graph = Graph.history; + Stats _Ychart = Stats.tr; + Stats _Xchart = Stats.tr; + int _season = currentSeason-1; + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + + @override + void initState(){ + _historyTooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${f4.format(data.stat)} $yAxisTitle", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + ), + ), + Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) + ], + ), + ); + } + ); + _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[_Xchart]}\n${f4.format(data.y)} ${chartsShortTitles[_Ychart]}') + ], + ), + ); + } + ); + _leagueTooltipBehavior = TooltipBehavior( + color: Colors.black, + borderColor: Colors.white, + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${f4.format(point.y)} $yAxisTitle", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), + ), + ), + Text(timestamp(data.ts)) + ], + ), + ); + } + ); + _zoomPanBehavior = ZoomPanBehavior( + enablePinching: true, + enableSelectionZooming: true, + enableMouseWheelZooming : true, + enablePanning: true, + ); + super.initState(); + } + + Future>>> getHistoryData(bool fetchHistory) async { + if(fetchHistory){ + try{ + var history = await teto.fetchAndsaveTLHistory(widget.searchFor); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); + }on TetrioHistoryNotExist{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noHistorySaved))); + }on P1nkl0bst3rForbidden { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); + }on P1nkl0bst3rInternalProblem { + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); + }on P1nkl0bst3rTooManyRequests{ + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); + } + } + + List> states = await Future.wait>([ + teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), + ]); + List>> historyData = []; // [season][metric][spot] + for (int season = 0; season < currentSeason; season++){ + if (states[season].length >= 2){ + Map> statsMap = {}; + for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())]; + historyData.add(statsMap); + }else{ + historyData.add({}); + break; + } + } + fetchData = false; + + return historyData; + } + + Future> getTetraLeagueData(Stats x, Stats y) async { + TetrioPlayersLeaderboard leaderboard = await teto.fetchTLLeaderboard(); + List<_MyScatterSpot> _spots = [ + for (TetrioPlayerFromLeaderboard entry in leaderboard.leaderboard) + _MyScatterSpot( + entry.getStatByEnum(x).toDouble(), + entry.getStatByEnum(y).toDouble(), + entry.userId, + entry.username, + entry.rank, + rankColors[entry.rank]??Colors.white + ) + ]; + return _spots; + } + + Widget getHistoryGraph(){ + return FutureBuilder>>>( + future: getHistoryData(fetchData), + 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){ + List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; + yAxisTitle = chartsShortTitles[_Ychart]!; + // TODO: this graph can Krash + return SfCartesianChart( + tooltipBehavior: _historyTooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), + primaryYAxis: const NumericAxis( + rangePadding: ChartRangePadding.additional, + ), + margin: const EdgeInsets.all(0), + series: [ + if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( + enableTooltip: true, + dataSource: selectedGraph, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (selectedGraph.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ) + else StepLineSeries<_HistoryChartSpot, DateTime>( + enableTooltip: true, + dataSource: selectedGraph, + animationDuration: 0, + opacity: _smooth ? 0 : 1, + xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, + yValueMapper: (_HistoryChartSpot data, _) => data.stat, + color: Theme.of(context).colorScheme.primary, + trendlines:[ + Trendline( + isVisible: _smooth, + period: (selectedGraph.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ), + ], + ); + }else{ return FutureError(snapshot); } + } + } + ); + } + + Widget getLeagueState (){ + return FutureBuilder>( + future: getTetraLeagueData(_Xchart, _Ychart), + 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 SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + //primaryXAxis: CategoryAxis(), + series: [ + ScatterSeries( + enableTooltip: true, + dataSource: snapshot.data, + animationDuration: 0, + pointColorMapper: (data, _) => data.color, + xValueMapper: (data, _) => data.x, + yValueMapper: (data, _) => data.y, + onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: snapshot.data![point.pointIndex!].nickname), maintainState: false)), + ) + ], + ); + }else{ return FutureError(snapshot); } + } + } + ); + } + + Widget getCutoffsHistory(){ + return FutureBuilder>( + future: teto.fetchCutoffsHistory(), + 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){ + yAxisTitle = chartsShortTitles[_Ychart]!; + return SfCartesianChart( + tooltipBehavior: _leagueTooltipBehavior, + zoomPanBehavior: _zoomPanBehavior, + primaryXAxis: const DateTimeAxis(), + primaryYAxis: NumericAxis( + // isInversed: true, + maximum: switch (_Ychart){ + Stats.tr => 25000.0, + Stats.gxe => 100.00, + _ => null + }, + ), + margin: const EdgeInsets.all(0), + series: [ + for (String rank in ranks) StepLineSeries( + enableTooltip: true, + dataSource: snapshot.data, + animationDuration: 0, + //opacity: 0.5, + xValueMapper: (Cutoffs data, _) => data.ts, + yValueMapper: (Cutoffs data, _) => switch (_Ychart){ + Stats.glicko => data.glicko[rank], + Stats.gxe => data.gxe[rank], + _ => data.tr[rank] + }, + color: rankColors[rank]! + ) + ], + ); + }else{ return FutureError(snapshot); } + } + } + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Wrap( + spacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (_graph == Graph.history) Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], + value: _season, + onChanged: (value) { + setState(() { + _season = value!; + }); + } + ), + ], + ), + if (_graph != Graph.leagueCutoffs) Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: switch (_graph){ + Graph.history => [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], + Graph.leagueState => _yAxis, + Graph.leagueCutoffs => [], + }, + value: _graph == Graph.history ? _gamesPlayedInsteadOfDateAndTime : _Xchart, + onChanged: (value) { + setState(() { + if (_graph == Graph.history) + _gamesPlayedInsteadOfDateAndTime = value! as bool; + else _Xchart = value! as Stats; + }); + } + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), + DropdownButton( + items: _graph == Graph.leagueCutoffs ? [DropdownMenuItem(value: Stats.tr, child: Text(chartsShortTitles[Stats.tr]!)), DropdownMenuItem(value: Stats.glicko, child: Text(chartsShortTitles[Stats.glicko]!)), DropdownMenuItem(value: Stats.gxe, child: Text(chartsShortTitles[Stats.gxe]!))] : _yAxis, + value: _Ychart, + onChanged: (value) { + setState(() { + _Ychart = value!; + }); + } + ), + ], + ), + if (_graph != Graph.leagueState) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox(value: _smooth, + checkColor: Colors.black, + onChanged: ((value) { + setState(() { + _smooth = value!; + }); + })), + Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) + ], + ), + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) + ], + ), + ), + Card( + child: SizedBox( + width: MediaQuery.of(context).size.width - 88, + height: MediaQuery.of(context).size.height - 96, + child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), + child: switch (_graph){ + Graph.history => getHistoryGraph(), + Graph.leagueState => getLeagueState(), + Graph.leagueCutoffs => getCutoffsHistory() + }, + ) + ), + ) + ], + ), + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: Graph.history, + label: Text('Player History')), + ButtonSegment( + value: Graph.leagueState, + label: Text('League State')), + ButtonSegment( + value: Graph.leagueCutoffs, + label: Text('League Cutoffs'), + ), + ], + selected: {_graph}, + onSelectionChanged: (Set newSelection) { + setState(() { + _graph = newSelection.first; + switch (newSelection.first){ + case Graph.leagueCutoffs: + case Graph.history: + _Ychart = Stats.tr; + case Graph.leagueState: + _Ychart = Stats.apm; + } + });}) + ], + ); + } +} + +class _HistoryChartSpot{ + final DateTime timestamp; + final int gamesPlayed; + final String rank; + final double stat; + const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat); +} + +class _MyScatterSpot{ + num x; + num y; + String id; + String nickname; + String rank; + Color color; + _MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); +} diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart new file mode 100644 index 0000000..97aa9d5 --- /dev/null +++ b/lib/views/destination_home.dart @@ -0,0 +1,1144 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/record_extras.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_beta_stream.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/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/singleplayer_record_view.dart'; +import 'package:tetra_stats/widgets/finesse_thingy.dart'; +import 'package:tetra_stats/widgets/lineclears_thingy.dart'; +import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class DestinationHome extends StatefulWidget{ + final String searchFor; + final Future dataFuture; + final Future? newsFuture; + final BoxConstraints constraints; + + const DestinationHome({super.key, required this.searchFor, required this.dataFuture, this.newsFuture, required this.constraints}); + + @override + State createState() => _DestinationHomeState(); +} + +class FetchResults{ + bool success; + TetrioPlayer? player; + List states; + Summaries? summaries; + Cutoffs? cutoffs; + Exception? exception; + + FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.exception); +} + +class RecordSummary extends StatelessWidget{ + final RecordSingle? record; + final bool hideRank; + final bool? betterThanRankAverage; + final MapEntry? closestAverage; + final bool? betterThanClosestAverage; + final String? rank; + + const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, this.hideRank = false}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null && record != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96)) + else !hideRank ? Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) : Container(), + if (record != null) Column( + crossAxisAlignment: hideRank ? CrossAxisAlignment.center : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + text: TextSpan( + text: switch(record!.gamemode){ + "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), + "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(record!.stats.zenith!.altitude)} m", + "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", + _ => record!.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white, height: 0.9), + ), + ), + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), + _ => record!.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), + "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), + _ => record!.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), + if (record!.rank != -1 && record!.countryRank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), + const TextSpan(text: "\n"), + TextSpan(text: timestamp(record!.timestamp)), + ] + ), + ), + ], + ) else if (hideRank) RichText(text: const TextSpan( + text: "---", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), + ), + ) + ], + ); + } + +} + +class LeagueCard extends StatelessWidget{ + final TetraLeague league; + final bool showSeasonNumber; + + const LeagueCard({super.key, required this.league, this.showSeasonNumber = false}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (showSeasonNumber) Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("Season ${league.season}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Spacer(), + Text( + "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey)), + ], + ) + else Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + TLRatingThingy(userID: "", tlData: league, showPositions: true), + const Divider(), + Text("${league.apm != null ? f2.format(league.apm) : "-.--"} APM • ${league.pps != null ? f2.format(league.pps) : "-.--"} PPS • ${league.vs != null ? f2.format(league.vs) : "-.--"} VS • ${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP • ${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ); + } + +} + +class _DestinationHomeState extends State with SingleTickerProviderStateMixin { + Cards rightCard = Cards.overview; + CardMod cardMod = CardMod.info; + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + late Map>> modeButtons; + late MapEntry? closestAverageBlitz; + late bool blitzBetterThanClosestAverage; + late MapEntry? closestAverageSprint; + late bool sprintBetterThanClosestAverage; + late AnimationController _transition; + late final Animation _offsetAnimation; + bool? sprintBetterThanRankAverage; + bool? blitzBetterThanRankAverage; + + Widget getOverviewCard(Summaries summaries){ + return Column( + children: [ + const Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Overview", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + ), + ), + ), + LeagueCard(league: summaries.league), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), + const Divider(), + Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), + const Divider(), + Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.zenith, hideRank: true), + const Divider(), + Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + const Divider(), + RecordSummary(record: summaries.zenithEx, hideRank: true,), + const Divider(), + Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), + Text("Score ${intf.format(summaries.zen.score)}"), + Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ), + ), + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("f", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text("${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? + f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: const TextStyle( + //shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + Row( + children: [ + const Text("Total pieces placed:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"), + ], + ), + Row( + children: [ + const Text(" - Placed with perfect finesse:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"), + ], + ) + ], + ), + ), + ), + ), + ], + ), + if (summaries.achievements.isNotEmpty) Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), + child: Column( + children: [ + if (summaries.achievements.firstWhere((e) => e.k == 16).v != null) Row( + children: [ + const Text("Total height climbed in QP"), + const Spacer(), + Text("${f2.format(summaries.achievements.firstWhere((e) => e.k == 16).v!)} m"), + ], + ), + if (summaries.achievements.firstWhere((e) => e.k == 17).v != null) Row( + children: [ + const Text("KO's in QP"), + const Spacer(), + Text(intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)), + ], + ) + ], + ), + ), + ), + ] + ); + } + + Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, List states){ + TetraLeague? toCompare = states.length >= 2 ? states.elementAtOrNull(states.length-2) : null; + return Column( + children: [ + Card( + //surfaceTintColor: rankColors[data.rank], + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${states.last.timestamp} ${states.last.tr}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + TetraLeagueThingy(league: data, toCompare: toCompare, cutoffs: cutoffs), + if (data.nerdStats != null) Card( + //surfaceTintColor: rankColors[data.rank], + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats), + if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) + ], + ); + } + + Widget getPreviousSeasonsList(Map pastLeague){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Previous Seasons", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + for (var key in pastLeague.keys) Card( + child: LeagueCard(league: pastLeague[key]!, showSeasonNumber: true), + ) + ], + ); + } + + Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ + return Column( + children: [ + const Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const TabBar( + tabs: [ + Tab(text: "Recent"), + Tab(text: "Top"), + ], + ), + SizedBox( + height: 400, + child: TabBarView( + children: [ + FutureBuilder( + future: teto.fetchStream(widget.searchFor, recentStream), + 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 (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => "40L", + "blitz" => "BLZ", + "5mblast" => "5MB", + "zenith" => "QP", + "zenithex" => "QPE", + String() => "huh", + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + FutureBuilder( + future: teto.fetchStream(widget.searchFor, topStream), + 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 (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + "#${i+1}", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: const TextStyle(fontSize: 18)), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + ] + ), + ) + ], + ), + ) + ), + ], + ); + } + + Widget getRecentTLrecords(BoxConstraints constraints){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + ], + ), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: FutureBuilder( + future: teto.fetchTLStream(widget.searchFor), + 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 SizedBox(height: constraints.maxHeight - 145, child: TLRecords(userID: widget.searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + ), + ], + ); + } + + Widget getZenithCard(RecordSingle? record){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), + ], + ), + ), + ), + ), + ZenithThingy(zenith: record), + if (record != null) Row( + children: [ + Expanded( + child: Card( + child: Column( + children: [ + FinesseThingy(record.stats.finesse, record.stats.finessePercentage), + LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins, showMoreClears: true), + if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") + ], + ), + ), + ), + Expanded( + child: Card( + child: SizedBox( + width: 300, + height: 318, + child: 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 + )), + ) + ], + ), + SizedBox( + width: 300.0, + child: 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 (record != null) Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + const Spacer() + ], + ), + ), + 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) + ], + ); + } + + Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ + if (record == null) { + return const Card( + child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), + ); + } + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(switch(record.gamemode){ + "40l" => t.sprint, + "blitz" => t.blitz, + "5mblast" => "5,000,000 Blast", + _ => record.gamemode + }, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)) + ], + ), + ), + ), + ), + Card( + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText(text: TextSpan( + text: switch(record.gamemode){ + "40l" => get40lTime(record.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record.stats.score), + "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), + _ => record.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), + _ => record.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z" || rank == "x+") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ + "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), + "blitz" => readableIntDifference(record.stats.score, closestAverage.value), + _ => record.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + 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(record.timestamp)), + ] + ), + ), + ], + ), + ], + ), + Row( + children: [ + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => record.stats.piecesPlaced.toString(), + "blitz" => record.stats.level.toString(), + "5mblast" => NumberFormat.decimalPattern().format(record.stats.spp), + _ => "What if " + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " Pieces", + "blitz" => " Level", + "5mblast" => " SPP", + _ => " i wanted to" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" PPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => f2.format(record.stats.kpp), + "blitz" => f2.format(record.stats.spp), + "5mblast" => record.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " KPP", + "blitz" => " SPP", + "5mblast" => " Pieces", + _ => " no" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" Key presses", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + const Text(" KPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), + ]), + TableRow(children: [ + Text(switch(record.gamemode){ + "40l" => " ", + "blitz" => record.stats.piecesPlaced.toString(), + "5mblast" => record.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), + Text(switch(record.gamemode){ + "40l" => " ", + "blitz" => " Pieces", + "5mblast" => " Pieces", + _ => " no" + }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), + ]) + ], + ), + ), + ], + ) + ], + ), + ), + Card( + child: Center( + child: Column( + children: [ + FinesseThingy(record.stats.finesse, record.stats.finessePercentage), + LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins), + if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") + ], + ), + ), + ) + ] + ); + } + + @override + initState(){ + modeButtons = { + Cards.overview: [ + const ButtonSegment( + value: CardMod.info, + label: Text('General'), + ), + ], + Cards.tetraLeague: [ + const ButtonSegment( + value: CardMod.info, + label: Text('Standing'), + ), + const ButtonSegment( + value: CardMod.ex, // yeah i misusing my own Enum shut the fuck up + label: Text('Previous Seasons'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Recent Matches'), + ), + ], + Cards.quickPlay: [ + const ButtonSegment( + value: CardMod.info, + label: Text('Normal'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ), + const ButtonSegment( + value: CardMod.ex, + label: Text('Expert'), + ), + const ButtonSegment( + value: CardMod.exRecords, + label: Text('Expert Records'), + ) + ], + Cards.blitz: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ) + ], + Cards.sprint: [ + const ButtonSegment( + value: CardMod.info, + label: Text('PB'), + ), + const ButtonSegment( + value: CardMod.records, + label: Text('Records'), + ) + ] + }; + + _transition = AnimationController(vsync: this, duration: Durations.long4); + + // _transition.addListener((){ + // setState(() { + + // }); + // }); + + _offsetAnimation = Tween( + begin: Offset.zero, + end: const Offset(1.5, 0.0), + ).animate(CurvedAnimation( + parent: _transition, + curve: Curves.elasticIn, + )); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: widget.dataFuture, + 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.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + if (!snapshot.data!.success) return FetchResultError(snapshot.data!); + blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; + sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; + if (snapshot.data!.summaries!.sprint != null) { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; + } + if (snapshot.data!.summaries!.blitz != null){ + closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); + blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; + } + return TweenAnimationBuilder( + duration: Durations.long4, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(0, 600-value*600, 0), + child: Opacity(opacity: value, child: child), + ); + }, + child: Row( + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + NewUserThingy(player: snapshot.data!.player!, 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), + if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) + else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), + if (snapshot.data!.player!.bio != null) Card( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], + ), + ), + //if (testNews != null && testNews!.news.isNotEmpty) + Expanded( + child: FutureBuilder( + future: widget.newsFuture, + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Card(child: Center(child: CircularProgressIndicator())); + case ConnectionState.done: + if (snapshot.hasData){ + return NewsThingy(snapshot.data!); + }else if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + } + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: Column( + children: [ + SizedBox( + height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, + child: SingleChildScrollView( + child: SlideTransition( + position: _offsetAnimation, + child: switch (rightCard){ + Cards.overview => getOverviewCard(snapshot.data!.summaries!), + Cards.tetraLeague => switch (cardMod){ + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), + CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), + CardMod.records => getRecentTLrecords(widget.constraints), + _ => const Center(child: Text("huh?")) + }, + Cards.quickPlay => switch (cardMod){ + CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), + CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), + CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), + CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), + }, + Cards.sprint => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + Cards.blitz => switch (cardMod){ + CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + }, + ), + ), + ), + if (modeButtons[rightCard]!.length > 1) SegmentedButton( + showSelectedIcon: false, + selected: {cardMod}, + segments: modeButtons[rightCard]!, + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + //_transition.; + }); + }, + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + const ButtonSegment( + value: Cards.overview, + //label: Text('Overview'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + //label: Text('Tetra League'), + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.quickPlay, + //label: Text('Quick Play'), + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.sprint, + //label: Text('40 Lines'), + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.blitz, + //label: Text('Blitz'), + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ], + selected: {rightCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + cardMod = CardMod.info; + rightCard = newSelection.first; + });}) + ], + ) + ) + ], + ), + ); + } + } + return const Text("End of FutureBuilder"); + }, + ); + } +} diff --git a/lib/views/destination_leaderboards.dart b/lib/views/destination_leaderboards.dart new file mode 100644 index 0000000..431f25e --- /dev/null +++ b/lib/views/destination_leaderboards.dart @@ -0,0 +1,277 @@ +import 'dart:async'; +import 'package:flutter/material.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/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'; + +class DestinationLeaderboards extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationLeaderboards({super.key, required this.constraints}); + + @override + State createState() => _DestinationLeaderboardsState(); +} + +enum Leaderboards{ + tl, + fullTL, + xp, + ar, + sprint, + blitz, + zenith, + zenithex, +} + +class _DestinationLeaderboardsState extends State { + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + final Map leaderboards = { + Leaderboards.tl: "Tetra League (Current Season)", + Leaderboards.fullTL: "Tetra League (Current Season, full one)", + Leaderboards.xp: "XP", + Leaderboards.ar: "Acievement Points", + Leaderboards.sprint: "40 Lines", + Leaderboards.blitz: "Blitz", + Leaderboards.zenith: "Quick Play", + Leaderboards.zenithex: "Quick Play Expert", + }; + Leaderboards _currentLb = Leaderboards.tl; + final StreamController> _dataStreamController = StreamController>.broadcast(); + late final ScrollController _scrollController; + Stream> get dataStream => _dataStreamController.stream; + List list = []; + bool _isFetchingData = false; + String? prisecter; + List _countries = [for (MapEntry e in t.countries.entries) DropdownMenuEntry(value: e.key, label: e.value)]; + List _stats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuEntry(value: e.key, label: e.value)]; + String? _country; + Stats stat = Stats.tr; + + Future _fetchData() async { + if (_isFetchingData) { + // Avoid fetching new data while already fetching + return; + } + try { + _isFetchingData = true; + setState(() {}); + + final items = switch(_currentLb){ + Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter, country: _country), + Leaderboards.fullTL => (await teto.fetchTLLeaderboard()).getStatRankingFromLB(stat, country: _country??""), + Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp", country: _country), + Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar", country: _country), + Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, country: _country), + Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global", country: _country), + Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global", country: _country), + Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global", country: _country), + }; + + list.addAll(items); + + _dataStreamController.add(list); + prisecter = list.last.prisecter.toString(); + } catch (e) { + _dataStreamController.addError(e); + } finally { + // Set to false when data fetching is complete + _isFetchingData = false; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _fetchData(); + _scrollController.addListener(() { + _scrollController.addListener(() { + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.position.pixels; + + if (currentScroll == maxScroll && _currentLb != Leaderboards.fullTL) { + // When the last item is fully visible, load the next page. + _fetchData(); + } + }); + }); + } + + static TextStyle trailingStyle = TextStyle(fontSize: 28); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 350.0, + height: widget.constraints.maxHeight, + child: Column( + children: [ + const Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Spacer() + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: leaderboards.length, + itemBuilder: (BuildContext context, int index) { + return Card( + surfaceTintColor: index == 1 ? Colors.redAccent : theme.colorScheme.primary, + child: ListTile( + title: Text(leaderboards.values.elementAt(index)), + subtitle: index == 1 ? Text("Heavy, but allows you to sort players by their stats", style: TextStyle(color: Colors.grey, fontSize: 12)) : null, + onTap: () { + _currentLb = leaderboards.keys.elementAt(index); + list.clear(); + prisecter = null; + _fetchData(); + }, + ), + ); + } + ), + ), + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 350 - 88, + child: Card( + child: StreamBuilder>( + stream: dataStream, + builder:(context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + return const Center(child: CircularProgressIndicator()); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData){ + return Column( + children: [ + Text(leaderboards[_currentLb]!, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownMenu( + leadingIcon: Icon(Icons.public), + inputDecorationTheme: InputDecorationTheme( + isDense: true, + ), + textStyle: TextStyle(fontSize: 14, height: 0.9), + dropdownMenuEntries: _countries, + initialSelection: "", + onSelected: ((value) { + _country = value as String?; + list.clear(); + prisecter = null; + _isFetchingData = false; + setState((){_fetchData();}); + }) + ), + if (_currentLb == Leaderboards.fullTL) SizedBox(width: 5.0), + if (_currentLb == Leaderboards.fullTL) DropdownMenu( + leadingIcon: Icon(Icons.sort), + inputDecorationTheme: InputDecorationTheme( + isDense: true, + ), + textStyle: TextStyle(fontSize: 14, height: 0.9), + dropdownMenuEntries: _stats, + initialSelection: stat, + onSelected: ((value) { + stat = value; + list.clear(); + prisecter = null; + _isFetchingData = false; + setState((){_fetchData();}); + }) + ) + ], + ), + const Divider(), + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length, + prototypeItem: ListTile( + leading: Text("0"), + title: Text("ehhh...", style: TextStyle(fontSize: 22)), + trailing: SizedBox(height: 36, width: 1), + subtitle: const Text("eh...", style: TextStyle(color: Colors.grey, fontSize: 12)), + ), + itemBuilder: (BuildContext context, int index){ + return ListTile( + leading: Text(intf.format(index+1)), + title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), + trailing: switch (_currentLb){ + Leaderboards.tl => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), + Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) + ], + ), + Leaderboards.fullTL => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), + Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) + ], + ), + Leaderboards.xp => Text("LVL ${f2.format(snapshot.data![index].level)}", style: trailingStyle), + Leaderboards.ar => Text("${intf.format(snapshot.data![index].ar)} AR", style: trailingStyle), + Leaderboards.sprint => Text(get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), style: trailingStyle), + Leaderboards.blitz => Text(intf.format(snapshot.data![index].stats.score), style: trailingStyle), + Leaderboards.zenith => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle), + Leaderboards.zenithex => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle) + }, + subtitle: Text(switch (_currentLb){ + Leaderboards.tl => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", + Leaderboards.fullTL => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", + Leaderboards.xp => "${f2.format(snapshot.data![index].xp)} XP${snapshot.data![index].playtime.isNegative ? "" : ", ${playtime(snapshot.data![index].playtime)} of gametime"}", + Leaderboards.ar => "${snapshot.data![index].ar_counts}", + Leaderboards.sprint => "${intf.format(snapshot.data![index].stats.finesse.faults)} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.kps)} KPS, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${intf.format(snapshot.data![index].stats.piecesPlaced)} P", + Leaderboards.blitz => "lvl ${snapshot.data![index].stats.level}, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${f2.format(snapshot.data![index].stats.spp)} SPP", + Leaderboards.zenith => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B", + Leaderboards.zenithex => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B" + }, style: TextStyle(color: Colors.grey, fontSize: 12)), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UserView(searchFor: snapshot.data![index].userId), + maintainState: false, + ), + ); + }, + ); + } + ), + ), + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/views/destination_saved_data.dart b/lib/views/destination_saved_data.dart new file mode 100644 index 0000000..51936dc --- /dev/null +++ b/lib/views/destination_saved_data.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/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/text_timestamp.dart'; + +class DestinationSavedData extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationSavedData({super.key, required this.constraints}); + + @override + State createState() => _DestinationSavedData(); +} + +class _DestinationSavedData extends State { + String? selectedID; + + Future<(List, List, List)> getDataAbout(String id) async { + return (await teto.getStates(id, season: currentSeason), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id)); + } + + Widget getTetraLeagueListTile(TetraLeague data){ + return ListTile( + title: Text("${timestamp(data.timestamp)}"), + subtitle: Text("${f2.format(data.apm)} APM, ${f2.format(data.pps)} PPS, ${f2.format(data.vs)} VS, ${intf.format(data.gamesPlayed)} games", style: TextStyle(color: Colors.grey)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(data.tr)} TR", style: TextStyle(fontSize: 28)), + Image.asset("res/tetrio_tl_alpha_ranks/${data.rank}.png", height: 36) + ], + ), + leading: IconButton( + onPressed: () { + teto.deleteState(data.id+data.timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(data.timestamp))))); + })); + }, + icon: Icon(Icons.delete_forever) + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StateView(state: data), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: teto.getAllPlayers(), + 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.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + return Row( + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + const Card( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + Text("Saved Data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), + Spacer() + ], + ), + ), + for (String id in snapshot.data!.keys) Card( + child: ListTile( + title: Text(snapshot.data![id]!), + //subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)), + onTap: () => setState(() { + selectedID = id; + }), + ), + ) + ], + ), + ), + SizedBox( + width: widget.constraints.maxWidth - 450 - 80, + child: selectedID != null ? FutureBuilder<(List, List, List)>( + future: getDataAbout(selectedID!), + 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.hasError){ return FutureError(snapshot); } + if (snapshot.hasData){ + return DefaultTabController( + length: 3, + child: Card( + child: Column( + children: [ + Card( + child: TabBar(tabs: [ + Tab(text: "S${currentSeason} TL States"), + Tab(text: "S1 TL States"), + Tab(text: "TL Records") + ]), + ), + SizedBox( + height: widget.constraints.maxHeight - 64, + child: TabBarView(children: [ + ListView.builder( + itemCount: snapshot.data!.$1.length, + itemBuilder: (context, index) { + return getTetraLeagueListTile(snapshot.data!.$1[index]); + },), + ListView.builder( + itemCount: snapshot.data!.$2.length, + itemBuilder: (context, index) { + return getTetraLeagueListTile(snapshot.data!.$2[index]); + },), + ListView.builder( + itemCount: snapshot.data!.$3.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(snapshot.data!.$3[index].toString()), + ); + },), + ] + ), + ) + ], + ), + ), + ); + } + return Text("what?"); + } + } + ) : + Text("Select nickname on the left to see data assosiated with it") + ) + ], + ); + } + } + return const Text("End of FutureBuilder"); + }, + ); + } +} diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index f918201..3933062 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,19 +1,13 @@ import 'dart:async'; -import 'dart:ui' as ui; -import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; -import 'package:flutter/services.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/badge.dart'; import 'package:tetra_stats/data_objects/beta_record.dart'; -import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; import 'package:tetra_stats/data_objects/distinguishment.dart'; import 'package:tetra_stats/data_objects/est_tr.dart'; import 'package:tetra_stats/data_objects/nerd_stats.dart'; @@ -23,35 +17,31 @@ import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; import 'package:tetra_stats/data_objects/playstyle.dart'; import 'package:tetra_stats/data_objects/record_extras.dart'; import 'package:tetra_stats/data_objects/record_single.dart'; -import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; import 'package:tetra_stats/data_objects/summaries.dart'; import 'package:tetra_stats/data_objects/tetra_league.dart'; -import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; -import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_player.dart'; -import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; -import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/singleplayer_record_view.dart'; +import 'package:tetra_stats/views/destination_calculator.dart'; +import 'package:tetra_stats/views/destination_cutoffs.dart'; +import 'package:tetra_stats/views/destination_graphs.dart'; +import 'package:tetra_stats/views/destination_home.dart'; +import 'package:tetra_stats/views/destination_leaderboards.dart'; +import 'package:tetra_stats/views/destination_saved_data.dart'; import 'package:tetra_stats/views/tl_match_view.dart'; import 'package:tetra_stats/views/compare_view_tiles.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; -import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/main.dart'; import 'package:tetra_stats/widgets/tl_progress_bar.dart'; import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:transparent_image/transparent_image.dart'; -import 'package:vector_math/vector_math_64.dart' hide Colors; // TODO: Refactor it @@ -59,6 +49,35 @@ var fDiff = NumberFormat("+#,###.####;-#,###.####"); late Future _data; late Future _newsData; +Future getData(String searchFor) async { + TetrioPlayer player; + try{ + if (searchFor.startsWith("ds:")){ + player = await teto.fetchPlayer(searchFor.substring(3), isItDiscordID: true); // we trying to get him with that + }else{ + player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username + } + }on TetrioPlayerNotExist{ + return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); + } + late Summaries summaries; + late Cutoffs cutoffs; + List requests = await Future.wait([ + teto.fetchSummaries(player.userId), + teto.fetchCutoffsBeanserver(), + ]); + List states = await teto.getStates(player.userId, season: currentSeason); + summaries = requests[0]; + cutoffs = requests[1]; + + bool isTracking = await teto.isPlayerTracking(player.userId); + if (isTracking){ // if tracked - save data to local DB + await teto.storeState(summaries.league); + } + + return FetchResults(true, player, states, summaries, cutoffs, null); + } + class MainView extends StatefulWidget { final String? player; /// The very first view, that user see when he launch this programm. @@ -98,39 +117,10 @@ class _MainState extends State with TickerProviderStateMixin { super.initState(); } - Future _getData() async { - TetrioPlayer player; - try{ - if (_searchFor.startsWith("ds:")){ - player = await teto.fetchPlayer(_searchFor.substring(3), isItDiscordID: true); // we trying to get him with that - }else{ - player = await teto.fetchPlayer(_searchFor); // Otherwise it's probably a user id or username - } - }on TetrioPlayerNotExist{ - return FetchResults(false, null, [], null, null, TetrioPlayerNotExist()); - } - late Summaries summaries; - late Cutoffs cutoffs; - List requests = await Future.wait([ - teto.fetchSummaries(player.userId), - teto.fetchCutoffsBeanserver(), - ]); - List states = await teto.getStates(player.userId, season: currentSeason); - summaries = requests[0]; - cutoffs = requests[1]; - - bool isTracking = await teto.isPlayerTracking(player.userId); - if (isTracking){ // if tracked - save data to local DB - await teto.storeState(summaries.league); - } - - return FetchResults(true, player, states, summaries, cutoffs, null); - } - void changePlayer(String player) { setState(() { _searchFor = player; - _data = _getData(); + _data = getData(_searchFor); _newsData = teto.fetchNews(_searchFor); }); } @@ -206,7 +196,7 @@ class _MainState extends State with TickerProviderStateMixin { ), Expanded( child: switch (destination){ - 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), + 0 => DestinationHome(searchFor: _searchFor, constraints: constraints, dataFuture: _data, newsFuture: _newsData), 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), 2 => DestinationLeaderboards(constraints: constraints), 3 => DestinationCutoffs(constraints: constraints), @@ -221,2860 +211,6 @@ class _MainState extends State with TickerProviderStateMixin { } } -class DestinationSavedData extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationSavedData({super.key, required this.constraints}); - - @override - State createState() => _DestinationSavedData(); -} - -class _DestinationSavedData extends State { - String? selectedID; - - Future<(List, List, List)> getDataAbout(String id) async { - return (await teto.getStates(id, season: currentSeason), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id)); - } - - Widget getTetraLeagueListTile(TetraLeague data){ - return ListTile( - title: Text("${timestamp(data.timestamp)}"), - subtitle: Text("${f2.format(data.apm)} APM, ${f2.format(data.pps)} PPS, ${f2.format(data.vs)} VS, ${intf.format(data.gamesPlayed)} games", style: TextStyle(color: Colors.grey)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(data.tr)} TR", style: TextStyle(fontSize: 28)), - Image.asset("res/tetrio_tl_alpha_ranks/${data.rank}.png", height: 36) - ], - ), - leading: IconButton( - onPressed: () { - teto.deleteState(data.id+data.timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(data.timestamp))))); - })); - }, - icon: Icon(Icons.delete_forever) - ), - ); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: teto.getAllPlayers(), - 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.hasError){ return FutureError(snapshot); } - if (snapshot.hasData){ - return Row( - children: [ - SizedBox( - width: 450, - child: Column( - children: [ - const Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Text("Saved Data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), - Spacer() - ], - ), - ), - for (String id in snapshot.data!.keys) Card( - child: ListTile( - title: Text(snapshot.data![id]!), - //subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)), - onTap: () => setState(() { - selectedID = id; - }), - ), - ) - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: selectedID != null ? FutureBuilder<(List, List, List)>( - future: getDataAbout(selectedID!), - 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.hasError){ return FutureError(snapshot); } - if (snapshot.hasData){ - return DefaultTabController( - length: 3, - child: Card( - child: Column( - children: [ - Card( - child: TabBar(tabs: [ - Tab(text: "S${currentSeason} TL States"), - Tab(text: "S1 TL States"), - Tab(text: "TL Records") - ]), - ), - SizedBox( - height: widget.constraints.maxHeight - 64, - child: TabBarView(children: [ - ListView.builder( - itemCount: snapshot.data!.$1.length, - itemBuilder: (context, index) { - return getTetraLeagueListTile(snapshot.data!.$1[index]); - },), - ListView.builder( - itemCount: snapshot.data!.$2.length, - itemBuilder: (context, index) { - return getTetraLeagueListTile(snapshot.data!.$2[index]); - },), - ListView.builder( - itemCount: snapshot.data!.$3.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(snapshot.data!.$3[index].toString()), - ); - },), - ] - ), - ) - ], - ), - ), - ); - } - return Text("what?"); - } - } - ) : - Text("Select nickname on the left to see data assosiated with it") - ) - ], - ); - } - } - return const Text("End of FutureBuilder"); - }, - ); - } -} - -class DestinationCalculator extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationCalculator({super.key, required this.constraints}); - - @override - State createState() => _DestinationCalculatorState(); -} - -enum CalcCards{ - calc, - damage -} - -class ClearData{ - final String title; - final Lineclears lineclear; - final int lines; - final bool miniSpin; - final bool spin; - bool perfectClear = false; - int id = -1; - - ClearData(this.title, this.lineclear, this.lines, this.miniSpin, this.spin); - - ClearData cloneWith(int i){ - ClearData newOne = ClearData(title, lineclear, lines, miniSpin, spin)..id = i; - return newOne; - } - - bool get difficultClear { - if (lines == 0) return false; - if (lines >= 4 || miniSpin || spin) return true; - else return false; - } - - void togglePC(){ - perfectClear = !perfectClear; - } - - int dealsDamage(int combo, int b2b, int previousB2B, Rules rules){ - if (lines == 0) return 0; - double damage = 0; - - if (spin){ - if (lines <= 5) damage += garbage[lineclear]!; - else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5); - } else if (miniSpin){ - damage += garbage[lineclear]!; - } else { - if (lines <= 5) damage += garbage[lineclear]!; - else damage += garbage[Lineclears.PENTA]! + (lines - 5); - } - - if (difficultClear && b2b >= 1 && rules.b2b){ - if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log(1 + (b2b) * BACKTOBACK_BONUS_LOG)).floor() + (b2b == 1 ? 0 : (1 + log(1 +(b2b) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ??? - else damage += 1; // if b2b chaining off - } - - if (rules.combo && rules.comboTable != ComboTables.none) { - if (combo >= 1){ - if (lines == 1 && rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))]; - else damage *= (1 + COMBO_BONUS * (combo)); - } - if (combo >= 2) { - damage = max(log(1 + COMBO_MINIFIER * (combo) * COMBO_MINIFIER_LOG), damage); - } - } - - if (!difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1){ - damage += rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b); - } - - if (perfectClear) damage += rules.pcDamage; - - return (damage * rules.multiplier).floor(); - } -} - -Map> clearsExisting = { - "No Spin Clears": [ - ClearData("No lineclear (Break Combo)", Lineclears.ZERO, 0, false, false), - ClearData("Single", Lineclears.SINGLE, 1, false, false), - ClearData("Double", Lineclears.DOUBLE, 2, false, false), - ClearData("Triple", Lineclears.TRIPLE, 3, false, false), - ClearData("Quad", Lineclears.QUAD, 4, false, false) - ], - "Spins": [ - ClearData("Spin Zero", Lineclears.TSPIN, 0, false, true), - ClearData("Spin Single", Lineclears.TSPIN_SINGLE, 1, false, true), - ClearData("Spin Double", Lineclears.TSPIN_DOUBLE, 2, false, true), - ClearData("Spin Triple", Lineclears.TSPIN_TRIPLE, 3, false, true), - ClearData("Spin Quad", Lineclears.TSPIN_QUAD, 4, false, true), - ], - "Mini spins": [ - ClearData("Mini Spin Zero", Lineclears.TSPIN_MINI, 0, true, false), - ClearData("Mini Spin Single", Lineclears.TSPIN_MINI_SINGLE, 1, true, false), - ClearData("Mini Spin Double", Lineclears.TSPIN_MINI_DOUBLE, 2, true, false), - ClearData("Mini Spin Triple", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false), - ] -}; - -class Rules{ - bool combo = true; - bool b2b = true; - bool b2bChaining = false; - bool surge = true; - int surgeInitAmount = 4; - int surgeInitAtB2b = 4; - ComboTables comboTable = ComboTables.multiplier; - int pcDamage = 5; - int pcB2B = 1; - double multiplier = 1.0; -} - -const TextStyle mainToggleInRules = TextStyle(fontSize: 18, fontWeight: ui.FontWeight.w800); - -class _DestinationCalculatorState extends State { - double? apm; - double? pps; - double? vs; - NerdStats? nerdStats; - EstTr? estTr; - Playstyle? playstyle; - TextEditingController ppsController = TextEditingController(); - TextEditingController apmController = TextEditingController(); - TextEditingController vsController = TextEditingController(); - - List clears = []; - Map customClearsChoice = { - "No Spin Clears": 5, - "Spins": 5 - }; - int idCounter = 0; - Rules rules = Rules(); - - CalcCards card = CalcCards.calc; - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - 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"))); - } - } - - // void calcDamage(){ - // for (ClearData lineclear in clears){ - - // } - // } - - Widget getCalculator(){ - return SingleChildScrollView( - child: Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text("Stats Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - )), - ), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), - child: TextField( - onSubmitted: (value) => calc(), - controller: apmController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("APM"), alignLabelWithHint: true, hintText: "Enter your APM"), - ), - ) - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), - child: TextField( - onSubmitted: (value) => calc(), - controller: ppsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("PPS"), alignLabelWithHint: true, hintText: "Enter your PPS"), - ), - ) - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), - child: TextField( - onSubmitted: (value) => calc(), - controller: vsController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(suffix: Text("VS"), alignLabelWithHint: true, hintText: "Enter your VS"), - ), - ) - ), - TextButton( - onPressed: () => calc(), - child: Text(t.calc), - ), - ], - ), - ), - ), - if (nerdStats != null) Card( - child: NerdStatsThingy(nerdStats: nerdStats!) - ), - if (playstyle != null) Card( - child: GraphsThingy(nerdStats: nerdStats!, playstyle: playstyle!, apm: apm!, pps: pps!, vs: vs!) - ) - ], - ), - ); - } - - Widget getDamageCalculator(){ - List rSideWidgets = []; - List lSideWidgets = []; - - for (var key in clearsExisting.keys){ - rSideWidgets.add(Text(key)); - for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card( - child: ListTile( - title: Text(data.title), - subtitle: Text("${data.dealsDamage(0, 0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)), - trailing: Icon(Icons.arrow_forward_ios), - onTap: (){ - setState((){ - clears.add(data.cloneWith(idCounter)); - }); - idCounter++; - }, - ), - )); - if (key != "Mini spins") rSideWidgets.add(Card( - child: ListTile( - title: Text("Custom"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 30.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration(hintText: "5"), - onChanged: (value) => customClearsChoice[key] = int.parse(value), - )), - Text(" Lines", style: TextStyle(fontSize: 18)), - Icon(Icons.arrow_forward_ios) - ], - ), - onTap: (){ - setState((){ - clears.add(ClearData("${key == "Spins" ? "Spin " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} Lines)", key == "Spins" ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, customClearsChoice[key]!, false, key == "Spins").cloneWith(idCounter)); - }); - idCounter++; - }, - ), - )); - rSideWidgets.add(const Divider()); - } - - int combo = -1; - int b2b = -1; - int previousB2B = -1; - int totalDamage = 0; - int normalDamage = 0; - int comboDamage = 0; - int b2bDamage = 0; - int surgeDamage = 0; - int pcDamage = 0; - - for (ClearData lineclear in clears){ - previousB2B = b2b; - if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1; - if (lineclear.lines > 0) combo++; else combo = -1; - int pcDmg = lineclear.perfectClear ? (rules.pcDamage * rules.multiplier).floor() : 0; - int normalDmg = lineclear.dealsDamage(0, 0, 0, rules) - pcDmg; - int surgeDmg = (!lineclear.difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1) ? rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b) : 0; - int b2bDmg = lineclear.dealsDamage(0, b2b, b2b-1, rules) - normalDmg - pcDmg; - int dmg = lineclear.dealsDamage(combo, b2b, previousB2B, rules); - int comboDmg = dmg - normalDmg - b2bDmg - surgeDmg - pcDmg; - lSideWidgets.add( - ListTile( - key: ValueKey(lineclear.id), - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)), - if (lineclear.lines > 0) IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.local_parking_outlined, color: lineclear.perfectClear ? Colors.white : Colors.grey.shade800)), - ], - ), - title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"), - subtitle: lineclear.lines > 0 ? Text("${dmg == normalDmg ? "No bonuses" : ""}${b2bDmg > 0 ? "+${intf.format(b2bDmg)} for B2B" : ""}${(b2bDmg > 0 && comboDmg > 0) ? ", " : ""}${comboDmg > 0 ? "+${intf.format(comboDmg)} for combo" : ""}${(comboDmg > 0 && lineclear.perfectClear) ? ", " : ""}${lineclear.perfectClear ? "+${intf.format(pcDmg)} for PC" : ""}${(surgeDmg > 0 && (lineclear.perfectClear || comboDmg > 0)) ? ", " : ""}${surgeDmg > 0 ? "Surge released: +${intf.format(surgeDmg)}" : ""}", style: TextStyle(color: Colors.grey)) : null, - trailing: lineclear.lines > 0 ? Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), - ) : null, - ) - ); - totalDamage += dmg; - normalDamage += normalDmg; - comboDamage += comboDmg; - b2bDamage += b2bDmg; - surgeDamage += surgeDmg; - pcDamage += pcDmg; - } - // values for "the bar" - double sec2end = normalDamage.toDouble()+comboDamage.toDouble(); - double sec3end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble(); - double sec4end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble(); - double sec5end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble()+pcDamage.toDouble(); - return Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text("Damage Calucator", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - )), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 350.0, - child: DefaultTabController(length: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: TabBar(tabs: [ - Tab(text: "Actions"), - Tab(text: "Rules"), - ]), - ), - SizedBox( - height: widget.constraints.maxHeight - 164, - child: TabBarView(children: [ - SingleChildScrollView( - child: Column( - children: rSideWidgets, - ), - ), - SingleChildScrollView( - child: Column( - children: [ - Card( - child: ListTile( - title: Text("Multiplier", style: mainToggleInRules), - trailing: SizedBox(width: 90.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))], - decoration: InputDecoration(hintText: rules.multiplier.toString()), - onChanged: (value) => setState((){rules.multiplier = double.parse(value);}), - )), - ), - ), - Card( - child: Column( - children: [ - ListTile( - title: Text("Combo", style: mainToggleInRules), - trailing: Switch(value: rules.combo, onChanged: (v) => setState((){rules.combo = v;})), - ), - if (rules.combo) ListTile( - title: Text("Combo Table"), - trailing: DropdownButton( - items: [for (var v in ComboTables.values) DropdownMenuItem(value: v.index, child: Text(v.name))], - value: rules.comboTable.index, - onChanged: (v) => setState((){rules.comboTable = ComboTables.values[v!];}), - ), - ) - ], - ), - ), - Card( - child: Column( - children: [ - ListTile( - title: Text("Back-To-Back (B2B)", style: mainToggleInRules), - trailing: Switch(value: rules.b2b, onChanged: (v) => setState((){rules.b2b = v;})), - ), - if (rules.b2b) ListTile( - title: Text("Back-To-Back Chaining"), - trailing: Switch(value: rules.b2bChaining, onChanged: (v) => setState((){rules.b2bChaining = v;})), - ), - ], - ), - ), - Card( - child: Column( - children: [ - ListTile( - title: Text("Surge", style: mainToggleInRules), - trailing: Switch(value: rules.surge, onChanged: (v) => setState((){rules.surge = v;})), - ), - if (rules.surge) ListTile( - title: Text("Starts at B2B"), - trailing: SizedBox(width: 90.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration(hintText: rules.surgeInitAtB2b.toString()), - onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}), - )), - ), - if (rules.surge) ListTile( - title: Text("Start amount"), - trailing: SizedBox(width: 90.0, child: TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration(hintText: rules.surgeInitAmount.toString()), - onChanged: (value) => setState((){rules.surgeInitAmount = int.parse(value);}), - )), - ), - ], - ), - ) - ], - ), - ) - ]), - ) - ], - ) - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 350 - 80, - height: widget.constraints.maxHeight - 108, - child: clears.isEmpty ? Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), - SizedBox(height: 5.0), - Text("Click on the actions on the left to add them here", textAlign: ui.TextAlign.center), - ], - )) : - Card( - child: Column( - children: [ - Expanded( - child: ReorderableListView( - onReorder: (oldIndex, newIndex) { - setState((){ - if (oldIndex < newIndex) { - newIndex -= 1; - } - final ClearData item = clears.removeAt(oldIndex); - clears.insert(newIndex, item); - }); - }, - children: lSideWidgets, - ), - ), - Divider(), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 34.0, 0.0), - child: Row( - children: [ - Text("Total damage:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)), - Spacer(), - Text(intf.format(totalDamage), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100)) - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text("Lineclears: ${intf.format(normalDamage)}"), - Text("Combo: ${intf.format(comboDamage)}"), - Text("B2B: ${intf.format(b2bDamage)}"), - Text("Surge: ${intf.format(surgeDamage)}"), - Text("PC's: ${intf.format(pcDamage)}") - ], - ), - SfLinearGauge( - minimum: 0, - maximum: totalDamage.toDouble(), - showLabels: false, - showTicks: false, - ranges: [ - LinearGaugeRange( - color: Colors.green, - startValue: 0, - endValue: normalDamage.toDouble(), - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.yellow, - startValue: normalDamage.toDouble(), - endValue: sec2end, - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.blue, - startValue: sec2end, - endValue: sec3end, - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.red, - startValue: sec3end, - endValue: sec4end, - position: LinearElementPosition.cross, - ), - LinearGaugeRange( - color: Colors.orange, - startValue: sec4end, - endValue: sec5end, - position: LinearElementPosition.cross, - ), - ], - ), - ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0)))))) - ], - ) - ], - ), - ), - ) - ], - ) - ], - ); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - SizedBox( - height: widget.constraints.maxHeight -32, - child: switch (card){ - CalcCards.calc => getCalculator(), - CalcCards.damage => getDamageCalculator() - } - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: CalcCards.calc, - label: Text('Stats Calculator'), - ), - ButtonSegment( - value: CalcCards.damage, - label: Text('Damage Calculator'), - ), - ], - selected: {card}, - onSelectionChanged: (Set newSelection) { - setState(() { - card = newSelection.first; - });}) - ], - ); - } - -} - -class FetchCutoffsResults{ - late bool success; - CutoffsTetrio? cutoffs; - Exception? exception; - - FetchCutoffsResults(this.success, this.cutoffs, this.exception); -} - -class DestinationCutoffs extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationCutoffs({super.key, required this.constraints}); - - @override - State createState() => _DestinationCutoffsState(); -} - -class _DestinationCutoffsState extends State { - - Future fetch() async { - TetrioPlayerFromLeaderboard top1; - CutoffsTetrio cutoffs; - List requests = await Future.wait([ - teto.fetchCutoffsTetrio(), - teto.fetchTopOneFromTheLeaderboard(), - ]); - cutoffs = requests[0]; - top1 = requests[1]; - cutoffs.data["top1"] = CutoffTetrio( - pos: 1, - percentile: 0.00, - tr: top1.tr, - targetTr: 25000, - apm: top1.apm, - pps: top1.pps, - vs: top1.vs, - count: 1, - countPercentile: 0.0 - ); - return cutoffs; - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: fetch(), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.hasData){ - return SingleChildScrollView( - child: Column( - children: [ - Card( - child: Center(child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Column( - children: [ - Text("Tetra League State", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - Text("as of ${timestamp(snapshot.data!.timestamp)}"), - ], - ), - )), - ), - Padding( - padding: const EdgeInsets.only(bottom:4.0), - child: Card( - child: Column( - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Text("Actual"), - ), - Text("Target") - ] - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), - child: SfLinearGauge( - minimum: 0.00000000, - maximum: 25000.0000, - showTicks: false, - showLabels: false, - ranges: [ - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.outside, - startValue: snapshot.data!.data[cutoff]!.tr, - startWidth: 20.0, - endWidth: 20.0, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.tr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr - }, - color: cutoff != "top1" ? rankColors[cutoff] : Colors.grey.shade800, - ), - for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange( - position: LinearElementPosition.inside, - startValue: snapshot.data!.data[cutoff]!.targetTr, - endValue: switch (cutoff){ - "top1" => 25000.00, - "x+" => snapshot.data!.data["top1"]!.targetTr, - _ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr - }, - color: cutoff != "top1" ? rankColors[cutoff] : null, - ), - for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[cutoff]!.tr < snapshot.data!.data[cutoff]!.targetTr) LinearGaugeRange( - position: LinearElementPosition.cross, - startValue: snapshot.data!.data[cutoff]!.tr, - endValue: snapshot.data!.data[cutoff]!.targetTr, - color: Colors.green, - ), - for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr)LinearGaugeRange( - position: LinearElementPosition.cross, - startValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr, - endValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr, - color: Colors.red, - ), - ], - markerPointers: [ - for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.tr), style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(0, 35, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0), value: snapshot.data!.data[cutoff]!.tr, position: LinearElementPosition.outside, offset: 20), - for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.targetTr), textAlign: ui.TextAlign.right, style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(-15, 0, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0, transformAlignment: Alignment.topRight), value: snapshot.data!.data[cutoff]!.targetTr, position: LinearElementPosition.inside, offset: 6) - ], - ), - ), - ), - ], - ), - ], - ), - ), - ), - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - border: TableBorder.all(color: Colors.grey.shade900), - columnWidths: const { - 0: FixedColumnWidth(48), - 1: FixedColumnWidth(155), - 2: FixedColumnWidth(140), - 3: FixedColumnWidth(160), - 4: FixedColumnWidth(150), - 5: FixedColumnWidth(90), - 6: FixedColumnWidth(130), - 7: FixedColumnWidth(120), - 8: FixedColumnWidth(125), - 9: FixedColumnWidth(70), - }, - children: [ - TableRow( - children: [ - Text("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("Cutoff 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("Target TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("State", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, 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)), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text("More info", 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) if (rank != "top1") 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(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, 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: [ - if (rank == "x+") TextSpan(text: "№ 1 is ${f2.format(snapshot.data!.data["top1"]!.tr)} TR", style: const TextStyle(color: Colors.white60, shadows: null)) - else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? "Inflated from ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not inflated", style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)), - TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)), - if (rank == "d") TextSpan(text: "Well...", style: const TextStyle(color: Colors.white60, shadows: null)) - else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? "Deflated untill ${NumberFormat.compact().format(snapshot.data!.data[rank]!.targetTr)} TR" : "Not deflated", style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null)) - ] - )), - ), - 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)) - ] - )) - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: TextButton(child: Text("View", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), onPressed: () { - - },), - ), - ] - ) - ], - ) - ] - ), - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return Text("huh?"); - } - ); - } -} - -class DestinationLeaderboards extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationLeaderboards({super.key, required this.constraints}); - - @override - State createState() => _DestinationLeaderboardsState(); -} - -enum Leaderboards{ - tl, - fullTL, - xp, - ar, - sprint, - blitz, - zenith, - zenithex, -} - -class _DestinationLeaderboardsState extends State { - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - final Map leaderboards = { - Leaderboards.tl: "Tetra League (Current Season)", - Leaderboards.fullTL: "Tetra League (Current Season, full one)", - Leaderboards.xp: "XP", - Leaderboards.ar: "Acievement Points", - Leaderboards.sprint: "40 Lines", - Leaderboards.blitz: "Blitz", - Leaderboards.zenith: "Quick Play", - Leaderboards.zenithex: "Quick Play Expert", - }; - Leaderboards _currentLb = Leaderboards.tl; - final StreamController> _dataStreamController = StreamController>.broadcast(); - late final ScrollController _scrollController; - Stream> get dataStream => _dataStreamController.stream; - List list = []; - bool _isFetchingData = false; - String? prisecter; - List _countries = [for (MapEntry e in t.countries.entries) DropdownMenuEntry(value: e.key, label: e.value)]; - List _stats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuEntry(value: e.key, label: e.value)]; - String? _country; - Stats stat = Stats.tr; - - Future _fetchData() async { - if (_isFetchingData) { - // Avoid fetching new data while already fetching - return; - } - try { - _isFetchingData = true; - setState(() {}); - - final items = switch(_currentLb){ - Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter, country: _country), - Leaderboards.fullTL => (await teto.fetchTLLeaderboard()).getStatRankingFromLB(stat, country: _country??""), - Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp", country: _country), - Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar", country: _country), - Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, country: _country), - Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global", country: _country), - Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global", country: _country), - Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global", country: _country), - }; - - list.addAll(items); - - _dataStreamController.add(list); - prisecter = list.last.prisecter.toString(); - } catch (e) { - _dataStreamController.addError(e); - } finally { - // Set to false when data fetching is complete - _isFetchingData = false; - setState(() {}); - } - } - - @override - void initState() { - super.initState(); - _scrollController = ScrollController(); - _fetchData(); - _scrollController.addListener(() { - _scrollController.addListener(() { - final maxScroll = _scrollController.position.maxScrollExtent; - final currentScroll = _scrollController.position.pixels; - - if (currentScroll == maxScroll && _currentLb != Leaderboards.fullTL) { - // When the last item is fully visible, load the next page. - _fetchData(); - } - }); - }); - } - - static TextStyle trailingStyle = TextStyle(fontSize: 28); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - width: 350.0, - height: widget.constraints.maxHeight, - child: Column( - children: [ - const Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), - Spacer() - ], - ), - ), - Expanded( - child: ListView.builder( - itemCount: leaderboards.length, - itemBuilder: (BuildContext context, int index) { - return Card( - surfaceTintColor: index == 1 ? Colors.redAccent : theme.colorScheme.primary, - child: ListTile( - title: Text(leaderboards.values.elementAt(index)), - subtitle: index == 1 ? Text("Heavy, but allows you to sort players by their stats", style: TextStyle(color: Colors.grey, fontSize: 12)) : null, - onTap: () { - _currentLb = leaderboards.keys.elementAt(index); - list.clear(); - prisecter = null; - _fetchData(); - }, - ), - ); - } - ), - ), - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 350 - 88, - child: Card( - child: StreamBuilder>( - stream: dataStream, - builder:(context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - return const Center(child: CircularProgressIndicator()); - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.hasData){ - return Column( - children: [ - Text(leaderboards[_currentLb]!, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DropdownMenu( - leadingIcon: Icon(Icons.public), - inputDecorationTheme: InputDecorationTheme( - isDense: true, - ), - textStyle: TextStyle(fontSize: 14, height: 0.9), - dropdownMenuEntries: _countries, - initialSelection: "", - onSelected: ((value) { - _country = value as String?; - list.clear(); - prisecter = null; - _isFetchingData = false; - setState((){_fetchData();}); - }) - ), - if (_currentLb == Leaderboards.fullTL) SizedBox(width: 5.0), - if (_currentLb == Leaderboards.fullTL) DropdownMenu( - leadingIcon: Icon(Icons.sort), - inputDecorationTheme: InputDecorationTheme( - isDense: true, - ), - textStyle: TextStyle(fontSize: 14, height: 0.9), - dropdownMenuEntries: _stats, - initialSelection: stat, - onSelected: ((value) { - stat = value; - list.clear(); - prisecter = null; - _isFetchingData = false; - setState((){_fetchData();}); - }) - ) - ], - ), - const Divider(), - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: list.length, - prototypeItem: ListTile( - leading: Text("0"), - title: Text("ehhh...", style: TextStyle(fontSize: 22)), - trailing: SizedBox(height: 36, width: 1), - subtitle: const Text("eh...", style: TextStyle(color: Colors.grey, fontSize: 12)), - ), - itemBuilder: (BuildContext context, int index){ - return ListTile( - // TODO: make it clickable - leading: Text(intf.format(index+1)), - title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), - trailing: switch (_currentLb){ - Leaderboards.tl => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), - Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) - ], - ), - Leaderboards.fullTL => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle), - Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36) - ], - ), - Leaderboards.xp => Text("LVL ${f2.format(snapshot.data![index].level)}", style: trailingStyle), - Leaderboards.ar => Text("${intf.format(snapshot.data![index].ar)} AR", style: trailingStyle), - Leaderboards.sprint => Text(get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), style: trailingStyle), - Leaderboards.blitz => Text(intf.format(snapshot.data![index].stats.score), style: trailingStyle), - Leaderboards.zenith => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle), - Leaderboards.zenithex => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle) - }, - subtitle: Text(switch (_currentLb){ - Leaderboards.tl => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", - Leaderboards.fullTL => "${f2.format(snapshot.data![index].apm)} APM, ${f2.format(snapshot.data![index].pps)} PPS, ${f2.format(snapshot.data![index].vs)} VS, ${f2.format(snapshot.data![index].nerdStats.app)} APP, ${f2.format(snapshot.data![index].nerdStats.vsapm)} VS/APM", - Leaderboards.xp => "${f2.format(snapshot.data![index].xp)} XP${snapshot.data![index].playtime.isNegative ? "" : ", ${playtime(snapshot.data![index].playtime)} of gametime"}", - Leaderboards.ar => "${snapshot.data![index].ar_counts}", - Leaderboards.sprint => "${intf.format(snapshot.data![index].stats.finesse.faults)} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.kps)} KPS, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${intf.format(snapshot.data![index].stats.piecesPlaced)} P", - Leaderboards.blitz => "lvl ${snapshot.data![index].stats.level}, ${f2.format(snapshot.data![index].stats.pps)} PPS, ${f2.format(snapshot.data![index].stats.spp)} SPP", - Leaderboards.zenith => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B", - Leaderboards.zenithex => "${f2.format(snapshot.data![index].aggregateStats.apm)} APM, ${f2.format(snapshot.data![index].aggregateStats.pps)} PPS, ${intf.format(snapshot.data![index].stats.kills)} KO's, ${f2.format(snapshot.data![index].stats.cps)} climb speed (${f2.format(snapshot.data![index].stats.zenith!.peakrank)} peak), ${intf.format(snapshot.data![index].stats.topBtB)} B2B" - }, style: TextStyle(color: Colors.grey, fontSize: 12)), - ); - } - ), - ), - ], - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return Text("huh?"); - }, - ), - ), - ), - ], - ); - } -} - -class DestinationGraphs extends StatefulWidget{ - final String searchFor; - //final Function setState; - final BoxConstraints constraints; - - const DestinationGraphs({super.key, required this.searchFor, required this.constraints}); - - @override - State createState() => _DestinationGraphsState(); -} - -enum Graph{ - history, - leagueState, - leagueCutoffs -} - -class _DestinationGraphsState extends State { - bool fetchData = false; - bool _gamesPlayedInsteadOfDateAndTime = false; - late ZoomPanBehavior _zoomPanBehavior; - late TooltipBehavior _historyTooltipBehavior; - late TooltipBehavior _tooltipBehavior; - late TooltipBehavior _leagueTooltipBehavior; - String yAxisTitle = ""; - bool _smooth = false; - final List> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; - Graph _graph = Graph.history; - Stats _Ychart = Stats.tr; - Stats _Xchart = Stats.tr; - int _season = currentSeason-1; - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - - @override - void initState(){ - _historyTooltipBehavior = TooltipBehavior( - color: Colors.black, - borderColor: Colors.white, - enable: true, - animationDuration: 0, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "${f4.format(data.stat)} $yAxisTitle", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), - ), - ), - Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) - ], - ), - ); - } - ); - _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[_Xchart]}\n${f4.format(data.y)} ${chartsShortTitles[_Ychart]}') - ], - ), - ); - } - ); - _leagueTooltipBehavior = TooltipBehavior( - color: Colors.black, - borderColor: Colors.white, - enable: true, - animationDuration: 0, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "${f4.format(point.y)} $yAxisTitle", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), - ), - ), - Text(timestamp(data.ts)) - ], - ), - ); - } - ); - _zoomPanBehavior = ZoomPanBehavior( - enablePinching: true, - enableSelectionZooming: true, - enableMouseWheelZooming : true, - enablePanning: true, - ); - super.initState(); - } - - Future>>> getHistoryData(bool fetchHistory) async { - if(fetchHistory){ - try{ - var history = await teto.fetchAndsaveTLHistory(widget.searchFor); - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); - }on TetrioHistoryNotExist{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noHistorySaved))); - }on P1nkl0bst3rForbidden { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden))); - }on P1nkl0bst3rInternalProblem { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal))); - }on P1nkl0bst3rTooManyRequests{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests))); - } - } - - List> states = await Future.wait>([ - teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), - ]); - List>> historyData = []; // [season][metric][spot] - for (int season = 0; season < currentSeason; season++){ - if (states[season].length >= 2){ - Map> statsMap = {}; - for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())]; - historyData.add(statsMap); - }else{ - historyData.add({}); - break; - } - } - fetchData = false; - - return historyData; - } - - Future> getTetraLeagueData(Stats x, Stats y) async { - TetrioPlayersLeaderboard leaderboard = await teto.fetchTLLeaderboard(); - List<_MyScatterSpot> _spots = [ - for (TetrioPlayerFromLeaderboard entry in leaderboard.leaderboard) - _MyScatterSpot( - entry.getStatByEnum(x).toDouble(), - entry.getStatByEnum(y).toDouble(), - entry.userId, - entry.username, - entry.rank, - rankColors[entry.rank]??Colors.white - ) - ]; - return _spots; - } - - Widget getHistoryGraph(){ - return FutureBuilder>>>( - future: getHistoryData(fetchData), - 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){ - List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; - yAxisTitle = chartsShortTitles[_Ychart]!; - // TODO: this graph can Krash - return SfCartesianChart( - tooltipBehavior: _historyTooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, - ), - margin: const EdgeInsets.all(0), - series: [ - if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( - enableTooltip: true, - dataSource: selectedGraph, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (selectedGraph.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ) - else StepLineSeries<_HistoryChartSpot, DateTime>( - enableTooltip: true, - dataSource: selectedGraph, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (selectedGraph.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ), - ], - ); - }else{ return FutureError(snapshot); } - } - } - ); - } - - Widget getLeagueState (){ - return FutureBuilder>( - future: getTetraLeagueData(_Xchart, _Ychart), - 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 SfCartesianChart( - tooltipBehavior: _tooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - //primaryXAxis: CategoryAxis(), - series: [ - ScatterSeries( - enableTooltip: true, - dataSource: snapshot.data, - animationDuration: 0, - pointColorMapper: (data, _) => data.color, - xValueMapper: (data, _) => data.x, - yValueMapper: (data, _) => data.y, - onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: snapshot.data![point.pointIndex!].nickname), maintainState: false)), - ) - ], - ); - }else{ return FutureError(snapshot); } - } - } - ); - } - - Widget getCutoffsHistory(){ - return FutureBuilder>( - future: teto.fetchCutoffsHistory(), - 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){ - yAxisTitle = chartsShortTitles[_Ychart]!; - return SfCartesianChart( - tooltipBehavior: _leagueTooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: const DateTimeAxis(), - primaryYAxis: NumericAxis( - // isInversed: true, - maximum: switch (_Ychart){ - Stats.tr => 25000.0, - Stats.gxe => 100.00, - _ => null - }, - ), - margin: const EdgeInsets.all(0), - series: [ - for (String rank in ranks) StepLineSeries( - enableTooltip: true, - dataSource: snapshot.data, - animationDuration: 0, - //opacity: 0.5, - xValueMapper: (Cutoffs data, _) => data.ts, - yValueMapper: (Cutoffs data, _) => switch (_Ychart){ - Stats.glicko => data.glicko[rank], - Stats.gxe => data.gxe[rank], - _ => data.tr[rank] - }, - color: rankColors[rank]! - ) - ], - ); - }else{ return FutureError(snapshot); } - } - } - ); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (_graph == Graph.history) Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Season:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))], - value: _season, - onChanged: (value) { - setState(() { - _season = value!; - }); - } - ), - ], - ), - if (_graph != Graph.leagueCutoffs) Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: switch (_graph){ - Graph.history => [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], - Graph.leagueState => _yAxis, - Graph.leagueCutoffs => [], - }, - value: _graph == Graph.history ? _gamesPlayedInsteadOfDateAndTime : _Xchart, - onChanged: (value) { - setState(() { - if (_graph == Graph.history) - _gamesPlayedInsteadOfDateAndTime = value! as bool; - else _Xchart = value! as Stats; - }); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: _graph == Graph.leagueCutoffs ? [DropdownMenuItem(value: Stats.tr, child: Text(chartsShortTitles[Stats.tr]!)), DropdownMenuItem(value: Stats.glicko, child: Text(chartsShortTitles[Stats.glicko]!)), DropdownMenuItem(value: Stats.gxe, child: Text(chartsShortTitles[Stats.gxe]!))] : _yAxis, - value: _Ychart, - onChanged: (value) { - setState(() { - _Ychart = value!; - }); - } - ), - ], - ), - if (_graph != Graph.leagueState) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox(value: _smooth, - checkColor: Colors.black, - onChanged: ((value) { - setState(() { - _smooth = value!; - }); - })), - Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - ), - Card( - child: SizedBox( - width: MediaQuery.of(context).size.width - 88, - height: MediaQuery.of(context).size.height - 96, - child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), - child: switch (_graph){ - Graph.history => getHistoryGraph(), - Graph.leagueState => getLeagueState(), - Graph.leagueCutoffs => getCutoffsHistory() - }, - ) - ), - ) - ], - ), - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: Graph.history, - label: Text('Player History')), - ButtonSegment( - value: Graph.leagueState, - label: Text('League State')), - ButtonSegment( - value: Graph.leagueCutoffs, - label: Text('League Cutoffs'), - ), - ], - selected: {_graph}, - onSelectionChanged: (Set newSelection) { - setState(() { - _graph = newSelection.first; - switch (newSelection.first){ - case Graph.leagueCutoffs: - case Graph.history: - _Ychart = Stats.tr; - case Graph.leagueState: - _Ychart = Stats.apm; - } - });}) - ], - ); - } -} - -class _HistoryChartSpot{ - final DateTime timestamp; - final int gamesPlayed; - final String rank; - final double stat; - const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat); -} - -class _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); -} - -class DestinationHome extends StatefulWidget{ - final String searchFor; - //final Function setState; - final BoxConstraints constraints; - - const DestinationHome({super.key, required this.searchFor, required this.constraints}); - - @override - State createState() => _DestinationHomeState(); -} - -class FetchResults{ - bool success; - TetrioPlayer? player; - List states; - Summaries? summaries; - Cutoffs? cutoffs; - Exception? exception; - - FetchResults(this.success, this.player, this.states, this.summaries, this.cutoffs, this.exception); -} - -class RecordSummary extends StatelessWidget{ - final RecordSingle? record; - final bool hideRank; - final bool? betterThanRankAverage; - final MapEntry? closestAverage; - final bool? betterThanClosestAverage; - final String? rank; - - const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, this.hideRank = false}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (closestAverage != null && record != null) Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96)) - else !hideRank ? Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) : Container(), - if (record != null) Column( - crossAxisAlignment: hideRank ? CrossAxisAlignment.center : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RichText( - textAlign: hideRank ? TextAlign.center : TextAlign.start, - text: TextSpan( - text: switch(record!.gamemode){ - "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), - "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), - "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(record!.stats.zenith!.altitude)} m", - "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", - _ => record!.stats.score.toString() - }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white, height: 0.9), - ), - ), - RichText( - textAlign: hideRank ? TextAlign.center : TextAlign.start, - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ - "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), - "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), - _ => record!.stats.score.toString() - }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ - "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), - "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), - _ => record!.stats.score.toString() - }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )), - if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), - if (record!.rank != -1 && record!.countryRank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), - const TextSpan(text: "\n"), - TextSpan(text: timestamp(record!.timestamp)), - ] - ), - ), - ], - ) else if (hideRank) RichText(text: const TextSpan( - text: "---", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), - ), - ) - ], - ); - } - -} - -class LeagueCard extends StatelessWidget{ - final TetraLeague league; - final bool showSeasonNumber; - - const LeagueCard({super.key, required this.league, this.showSeasonNumber = false}); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (showSeasonNumber) Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text("Season ${league.season}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - Spacer(), - Text( - "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey)), - ], - ) - else Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - TLRatingThingy(userID: "", tlData: league, showPositions: true), - const Divider(), - Text("${league.apm != null ? f2.format(league.apm) : "-.--"} APM • ${league.pps != null ? f2.format(league.pps) : "-.--"} PPS • ${league.vs != null ? f2.format(league.vs) : "-.--"} VS • ${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} APP • ${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ); - } - -} - -class _DestinationHomeState extends State with SingleTickerProviderStateMixin { - Cards rightCard = Cards.overview; - CardMod cardMod = CardMod.info; - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - late Map>> modeButtons; - late MapEntry? closestAverageBlitz; - late bool blitzBetterThanClosestAverage; - late MapEntry? closestAverageSprint; - late bool sprintBetterThanClosestAverage; - late AnimationController _transition; - late final Animation _offsetAnimation; - bool? sprintBetterThanRankAverage; - bool? blitzBetterThanRankAverage; - - Widget getOverviewCard(Summaries summaries){ - return Column( - children: [ - const Card( - child: Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Overview", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - ), - ), - ), - LeagueCard(league: summaries.league), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), - const Divider(), - Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), - const Divider(), - Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.zenith, hideRank: true), - const Divider(), - Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(), - RecordSummary(record: summaries.zenithEx, hideRank: true,), - const Divider(), - Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), - Text("Score ${intf.format(summaries.zen.score)}"), - Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - const Text("f", style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 65, - height: 1.2, - )), - const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text("${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? - f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: const TextStyle( - //shadows: textShadow, - fontFamily: "Eurostile Round Extended", - fontSize: 36, - fontWeight: FontWeight.w500, - color: Colors.white - )), - ) - ], - ), - Row( - children: [ - const Text("Total pieces placed:"), - const Spacer(), - Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"), - ], - ), - Row( - children: [ - const Text(" - Placed with perfect finesse:"), - const Spacer(), - Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"), - ], - ) - ], - ), - ), - ), - ), - ], - ), - if (summaries.achievements.isNotEmpty) Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), - child: Column( - children: [ - if (summaries.achievements.firstWhere((e) => e.k == 16).v != null) Row( - children: [ - const Text("Total height climbed in QP"), - const Spacer(), - Text("${f2.format(summaries.achievements.firstWhere((e) => e.k == 16).v!)} m"), - ], - ), - if (summaries.achievements.firstWhere((e) => e.k == 17).v != null) Row( - children: [ - const Text("KO's in QP"), - const Spacer(), - Text(intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)), - ], - ) - ], - ), - ), - ), - ] - ); - } - - Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, List states){ - TetraLeague? toCompare = states.length >= 2 ? states.elementAtOrNull(states.length-2) : null; - return Column( - children: [ - Card( - //surfaceTintColor: rankColors[data.rank], - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${states.last.timestamp} ${states.last.tr}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - TetraLeagueThingy(league: data, toCompare: toCompare, cutoffs: cutoffs), - if (data.nerdStats != null) Card( - //surfaceTintColor: rankColors[data.rank], - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!, oldNerdStats: toCompare?.nerdStats), - if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) - ], - ); - } - - Widget getPreviousSeasonsList(Map pastLeague){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Previous Seasons", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - for (var key in pastLeague.keys) Card( - child: LeagueCard(league: pastLeague[key]!, showSeasonNumber: true), - ) - ], - ); - } - - Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ - return Column( - children: [ - const Card( - child: Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - Card( - clipBehavior: Clip.antiAlias, - child: DefaultTabController(length: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const TabBar( - tabs: [ - Tab(text: "Recent"), - Tab(text: "Top"), - ], - ), - SizedBox( - height: 400, - child: TabBarView( - children: [ - FutureBuilder( - future: teto.fetchStream(widget.searchFor, recentStream), - 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 (int i = 0; i < snapshot.data!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), - leading: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => "40L", - "blitz" => "BLZ", - "5mblast" => "5MB", - "zenith" => "QP", - "zenithex" => "QPE", - String() => "huh", - }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), - "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - String() => "huh", - }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) - ) - ], - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - }, - ), - FutureBuilder( - future: teto.fetchStream(widget.searchFor, topStream), - 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 (int i = 0; i < snapshot.data!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), - leading: Text( - "#${i+1}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), - "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - String() => "huh", - }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) - ) - ], - ); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - }, - ), - ] - ), - ) - ], - ), - ) - ), - ], - ); - } - - Widget getRecentTLrecords(BoxConstraints constraints){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - ), - ), - ), - Card( - clipBehavior: Clip.antiAlias, - child: FutureBuilder( - future: teto.fetchTLStream(widget.searchFor), - 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 SizedBox(height: constraints.maxHeight - 145, child: _TLRecords(userID: widget.searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); - } - if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - }, - ), - ), - ], - ); - } - - Widget getZenithCard(RecordSingle? record){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), - ], - ), - ), - ), - ), - ZenithThingy(zenith: record), - if (record != null) Row( - children: [ - Expanded( - child: Card( - child: Column( - children: [ - FinesseThingy(record.stats.finesse, record.stats.finessePercentage), - LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins, showMoreClears: true), - if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") - ], - ), - ), - ), - Expanded( - child: Card( - child: SizedBox( - width: 300, - height: 318, - child: 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 - )), - ) - ], - ), - SizedBox( - width: 300.0, - child: 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 (record != null) Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - 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) - ], - ); - } - - Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ - if (record == null) { - return const Card( - child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), - ); - } - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(switch(record.gamemode){ - "40l" => t.sprint, - "blitz" => t.blitz, - "5mblast" => "5,000,000 Blast", - _ => record.gamemode - }, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)) - ], - ), - ), - ), - ), - Card( - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RichText(text: TextSpan( - text: switch(record.gamemode){ - "40l" => get40lTime(record.stats.finalTime.inMicroseconds), - "blitz" => NumberFormat.decimalPattern().format(record.stats.score), - "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), - _ => record.stats.score.toString() - }, - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), - "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), - _ => record.stats.score.toString() - }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if ((rank == null || rank == "z" || rank == "x+") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), - "blitz" => readableIntDifference(record.stats.score, closestAverage.value), - _ => record.stats.score.toString() - }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage.key.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )), - 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(record.timestamp)), - ] - ), - ), - ], - ), - ], - ), - Row( - children: [ - Expanded( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => record.stats.piecesPlaced.toString(), - "blitz" => record.stats.level.toString(), - "5mblast" => NumberFormat.decimalPattern().format(record.stats.spp), - _ => "What if " - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " Pieces", - "blitz" => " Level", - "5mblast" => " SPP", - _ => " i wanted to" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" PPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => f2.format(record.stats.kpp), - "blitz" => f2.format(record.stats.spp), - "5mblast" => record.stats.piecesPlaced.toString(), - _ => "but god said" - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " KPP", - "blitz" => " SPP", - "5mblast" => " Pieces", - _ => " no" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - Expanded( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Key presses", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" KPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => " ", - "blitz" => record.stats.piecesPlaced.toString(), - "5mblast" => record.stats.piecesPlaced.toString(), - _ => "but god said" - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " ", - "blitz" => " Pieces", - "5mblast" => " Pieces", - _ => " no" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - ], - ) - ], - ), - ), - Card( - child: Center( - child: Column( - children: [ - FinesseThingy(record.stats.finesse, record.stats.finessePercentage), - LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins), - if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") - ], - ), - ), - ) - ] - ); - } - - @override - initState(){ - modeButtons = { - Cards.overview: [ - const ButtonSegment( - value: CardMod.info, - label: Text('General'), - ), - ], - Cards.tetraLeague: [ - const ButtonSegment( - value: CardMod.info, - label: Text('Standing'), - ), - const ButtonSegment( - value: CardMod.ex, // yeah i misusing my own Enum shut the fuck up - label: Text('Previous Seasons'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Recent Matches'), - ), - ], - Cards.quickPlay: [ - const ButtonSegment( - value: CardMod.info, - label: Text('Normal'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ), - const ButtonSegment( - value: CardMod.ex, - label: Text('Expert'), - ), - const ButtonSegment( - value: CardMod.exRecords, - label: Text('Expert Records'), - ) - ], - Cards.blitz: [ - const ButtonSegment( - value: CardMod.info, - label: Text('PB'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ) - ], - Cards.sprint: [ - const ButtonSegment( - value: CardMod.info, - label: Text('PB'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ) - ] - }; - - _transition = AnimationController(vsync: this, duration: Durations.long4); - - // _transition.addListener((){ - // setState(() { - - // }); - // }); - - _offsetAnimation = Tween( - begin: Offset.zero, - end: const Offset(1.5, 0.0), - ).animate(CurvedAnimation( - parent: _transition, - curve: Curves.elasticIn, - )); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _data, - 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.hasError){ return FutureError(snapshot); } - if (snapshot.hasData){ - if (!snapshot.data!.success) return FetchResultError(snapshot.data!); - blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; - sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; - if (snapshot.data!.summaries!.sprint != null) { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; - } - if (snapshot.data!.summaries!.blitz != null){ - closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); - blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; - } - return TweenAnimationBuilder( - duration: Durations.long4, - tween: Tween(begin: 0, end: 1), - curve: Easing.standard, - builder: (context, value, child) { - return Container( - transform: Matrix4.translationValues(0, 600-value*600, 0), - child: Opacity(opacity: value, child: child), - ); - }, - child: Row( - children: [ - SizedBox( - width: 450, - child: Column( - children: [ - NewUserThingy(player: snapshot.data!.player!, 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), - if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) - else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), - if (snapshot.data!.player!.bio != null) Card( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ], - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), - ) - ], - ), - ), - //if (testNews != null && testNews!.news.isNotEmpty) - Expanded( - child: FutureBuilder( - future: _newsData, - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Card(child: Center(child: CircularProgressIndicator())); - case ConnectionState.done: - if (snapshot.hasData){ - return NewsThingy(snapshot.data!); - }else if (snapshot.hasError){ return FutureError(snapshot); } - } - return const Text("what?"); - } - ), - ) - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: Column( - children: [ - SizedBox( - height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, - child: SingleChildScrollView( - child: SlideTransition( - position: _offsetAnimation, - child: switch (rightCard){ - Cards.overview => getOverviewCard(snapshot.data!.summaries!), - Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, snapshot.data!.states), - CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague), - CardMod.records => getRecentTLrecords(widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.quickPlay => switch (cardMod){ - CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), - CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), - CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), - CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), - }, - Cards.sprint => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.blitz => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - }, - ), - ), - ), - if (modeButtons[rightCard]!.length > 1) SegmentedButton( - showSelectedIcon: false, - selected: {cardMod}, - segments: modeButtons[rightCard]!, - onSelectionChanged: (p0) { - setState(() { - cardMod = p0.first; - //_transition.; - }); - }, - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: Cards.overview, - //label: Text('Overview'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Cards.tetraLeague, - //label: Text('Tetra League'), - icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.quickPlay, - //label: Text('Quick Play'), - icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.sprint, - //label: Text('40 Lines'), - icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.blitz, - //label: Text('Blitz'), - icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ], - selected: {rightCard}, - onSelectionChanged: (Set newSelection) { - setState(() { - cardMod = CardMod.info; - rightCard = newSelection.first; - });}) - ], - ) - ) - ], - ), - ); - } - } - return const Text("End of FutureBuilder"); - }, - ); - } -} - class NewsThingy extends StatelessWidget{ final News news; @@ -4256,7 +1392,7 @@ class ZenithThingy extends StatelessWidget{ } -class _TLRecords extends StatelessWidget { +class TLRecords extends StatelessWidget { final String userID; final Function changePlayer; final List data; @@ -4265,7 +1401,7 @@ class _TLRecords extends StatelessWidget { /// Widget, that displays Tetra League records. /// Accepts list of TL records ([data]) and [userID] of player from the view - const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere}); + const TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere}); @override Widget build(BuildContext context) { diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart new file mode 100644 index 0000000..5040240 --- /dev/null +++ b/lib/views/user_view.dart @@ -0,0 +1,59 @@ +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'; +import 'package:tetra_stats/views/destination_home.dart'; +import 'package:tetra_stats/views/main_view_tiles.dart'; + +final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); + +class UserView extends StatefulWidget { + final String searchFor; + const UserView({super.key, required this.searchFor}); + + @override + State createState() => UserState(); +} + +late String oldWindowTitle; + +class UserState extends State { + late ScrollController _scrollController; + + @override + void initState() { + _scrollController = ScrollController(); + if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ + // windowManager.getTitle().then((value) => oldWindowTitle = value); + // windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}"); + } + super.initState(); + } + + @override + void dispose() { + _scrollController.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("Search For"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return DestinationHome(searchFor: widget.searchFor, dataFuture: getData(widget.searchFor), newsFuture: teto.fetchNews(widget.searchFor), constraints: constraints); + } + ) + ) + ); + } +}