diff --git a/lib/data_objects/tetra_league.dart b/lib/data_objects/tetra_league.dart index 773c9ab..86e5e29 100644 --- a/lib/data_objects/tetra_league.dart +++ b/lib/data_objects/tetra_league.dart @@ -150,7 +150,11 @@ class TetraLeague { apm ?? 0, pps ?? 0, vs ?? 0, - decaying); + decaying, + -1, + -1, + Duration(seconds: -1), + -1); num? getStatByEnum(Stats stat){ switch (stat) { diff --git a/lib/data_objects/tetrio_player_from_leaderboard.dart b/lib/data_objects/tetrio_player_from_leaderboard.dart index 28a7fab..ad820dc 100644 --- a/lib/data_objects/tetrio_player_from_leaderboard.dart +++ b/lib/data_objects/tetrio_player_from_leaderboard.dart @@ -1,5 +1,7 @@ // ignore_for_file: hash_and_equals +import 'dart:math'; + 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'; @@ -27,6 +29,10 @@ class TetrioPlayerFromLeaderboard { late NerdStats nerdStats; late EstTr estTr; late Playstyle playstyle; + late int gamesPlayedTotal; + late int gamesWonTotal; + late Duration playtime; + late int ar; TetrioPlayerFromLeaderboard( this.userId, @@ -46,13 +52,19 @@ class TetrioPlayerFromLeaderboard { this.apm, this.pps, this.vs, - this.decaying){ + this.decaying, + this.gamesPlayedTotal, + this.gamesWonTotal, + this.playtime, + this.ar){ 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); } double get winrate => gamesWon / gamesPlayed; + double get winrateTotal => gamesWonTotal / gamesWonTotal; + double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1; double get esttracc => estTr.esttr - tr; double get s1tr => gxe * 250; @@ -66,7 +78,7 @@ class TetrioPlayerFromLeaderboard { gamesPlayed = json['league']['gamesplayed'] as int; gamesWon = json['league']['gameswon'] as int; tr = json['league']['tr'] != null ? json['league']['tr'].toDouble() : 0; - gxe = json['league']['gxe']??-1; + gxe = json['league']['gxe']?.toDouble(); glicko = json['league']['glicko']?.toDouble(); rd = json['league']['rd']?.toDouble(); rank = json['league']['rank']; @@ -75,6 +87,10 @@ class TetrioPlayerFromLeaderboard { pps = json['league']['pps'] != null ? json['league']['pps'].toDouble() : 0.00; vs = json['league']['vs'] != null ? json['league']['vs'].toDouble(): 0.00; decaying = json['league']['decaying']; + gamesPlayedTotal = json['gamesplayed'] as int; + gamesWonTotal = json['gameswon'] as int; + playtime = Duration(microseconds: (json['gametime'].toDouble() * 1000000).floor()); + ar = json['ar']; 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); diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 4ac790a..5e042ea 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -798,34 +798,60 @@ class TetrioService extends DB { } } - // Stream fetchFullLeaderboard() async* { - // late double after; - // int lbLength = 100; - // TetrioPlayersLeaderboard leaderboard = await fetchTLLeaderboard(); - // after = leaderboard.leaderboard.last.tr; - // while (lbLength == 100){ - // TetrioPlayersLeaderboard pseudoLb = await fetchTLLeaderboard(after: after); - // leaderboard.addPlayers(pseudoLb.leaderboard); - // lbLength = pseudoLb.leaderboard.length; - // after = pseudoLb.leaderboard.last.tr; - // yield leaderboard; - // } - // } - - // i want to know progress, so i trying to figure out this thing: - // Stream fetchTLLeaderboardAsStream() async { - // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); - // if (cached != null) return cached; + Future> fetchTetrioLeaderboard({double? after, String? lb}) async { + const int lbLength = 100; + // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); + // if (cached != null) return cached; - // Uri url; - // if (kIsWeb) { - // url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); - // } else { - // url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); - // } + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); + } else { + url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', { + "limit": "100", + if (after != null && after != -1) "after": "$after:0:0" + }); + } + try{ + final response = await client.get(url); - // Stream stream = http.StreamedRequest("GET", url); - // } + switch (response.statusCode) { + case 200: + _lbPositions.clear(); + var rawJson = jsonDecode(response.body); + if (rawJson['success']) { // if api confirmed that everything ok + List leaderboard = []; + for (Map entry in rawJson['data']['entries']) { + leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at']))); + } + developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + //_cache.store(leaderboard, rawJson['cache']['cached_until']); + return leaderboard; + } else { // idk how to hit that one + developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); + throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side? + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); + } + } TetrioPlayersLeaderboard? getCachedLeaderboard(){ return _cache.get("league", TetrioPlayersLeaderboard); diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart index 9b80ab3..c30ccf9 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Badge; @@ -207,6 +209,23 @@ class _MainState extends State with TickerProviderStateMixin { } } +class DestinationCutoffs extends StatefulWidget{ + final BoxConstraints constraints; + + const DestinationCutoffs({super.key, required this.constraints}); + + @override + State createState() => _DestinationCutoffsState(); +} + +class _DestinationCutoffsState extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } +} + class DestinationLeaderboards extends StatefulWidget{ final BoxConstraints constraints; @@ -216,10 +235,72 @@ class DestinationLeaderboards extends StatefulWidget{ State createState() => _DestinationLeaderboardsState(); } +enum Leaderboards{ + tl, + xp, + ar +} + class _DestinationLeaderboardsState extends State { - Cards rightCard = Cards.tetraLeague; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - final List leaderboards = ["Tetra League", "Quick Play", "Quick Play Expert"]; + final Map leaderboards = {Leaderboards.tl: "Tetra League", Leaderboards.xp: "XP", Leaderboards.ar: "Acievement Points"}; + Leaderboards _currentLb = Leaderboards.tl; + final StreamController> _dataStreamController = StreamController>(); + late final ScrollController _scrollController; + Stream> get dataStream => _dataStreamController.stream; + List list = []; + bool _isFetchingData = false; + double after = 25000.00; + + 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(after: after), + Leaderboards.xp => await teto.fetchTetrioLeaderboard(after: after, lb: "xp"), + Leaderboards.ar => await teto.fetchTetrioLeaderboard(after: after, lb: "ar"), + }; + + list.addAll(items); + + _dataStreamController.add(list); + after = switch (_currentLb){ + Leaderboards.tl => list.last.tr, + Leaderboards.xp => list.last.xp, + Leaderboards.ar => list.last.ar.toDouble(), + }; + } 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) { + // When the last item is fully visible, load the next page. + _fetchData(); + } + }); + }); + } @override Widget build(BuildContext context) { @@ -247,7 +328,17 @@ class _DestinationLeaderboardsState extends State { return Card( surfaceTintColor: theme.colorScheme.primary, child: ListTile( - title: Text(leaderboards[index]), + title: Text(leaderboards.values.elementAt(index)), + onTap: () { + _currentLb = leaderboards.keys.elementAt(index); + list.clear(); + after = switch (_currentLb){ + Leaderboards.tl => 25000.00, + Leaderboards.xp => -1.00, + Leaderboards.ar => -1.00, + }; + _fetchData(); + }, ), ); } @@ -258,11 +349,45 @@ class _DestinationLeaderboardsState extends State { ), SizedBox( width: widget.constraints.maxWidth - 350 - 88, - child: const Card( - child: Column( - children: [ - - ], + 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)), + const Divider(color: Color.fromARGB(50, 158, 158, 158)), + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length, + itemBuilder: (BuildContext context, int index){ + return ListTile( + leading: Text(intf.format(index+1)), + title: Text(snapshot.data![index].username), + trailing: Text(switch (_currentLb){ + Leaderboards.tl => f2.format(snapshot.data![index].tr), + Leaderboards.xp => f2.format(snapshot.data![index].level), + Leaderboards.ar => intf.format(snapshot.data![index].ar), + }), + ); + } + ), + ), + ], + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return Text("huh?"); + }, ), ), ), @@ -297,13 +422,11 @@ class _DestinationGraphsState extends State { late TooltipBehavior _leagueTooltipBehavior; String yAxisTitle = ""; bool _smooth = false; - //final List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; 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; - //late List>>> historyData; //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); @override @@ -365,7 +488,6 @@ class _DestinationGraphsState extends State { animationDuration: 0, builder: (dynamic data, dynamic point, dynamic series, int pointIndex, int seriesIndex) { - print(point); return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -454,7 +576,7 @@ class _DestinationGraphsState extends State { case ConnectionState.active: return const Center(child: CircularProgressIndicator()); case ConnectionState.done: - if (snapshot.hasData && snapshot.data!.isNotEmpty){ + if (snapshot.hasData){ List<_HistoryChartSpot> selectedGraph = snapshot.data![_season][_Ychart]!; yAxisTitle = chartsShortTitles[_Ychart]!; return SfCartesianChart( @@ -500,20 +622,7 @@ class _DestinationGraphsState extends State { ), ], ); - }else{ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + }else{ return FutureError(snapshot); } } } ); @@ -546,20 +655,7 @@ class _DestinationGraphsState extends State { ) ], ); - }else{ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + }else{ return FutureError(snapshot); } } } ); @@ -580,9 +676,14 @@ class _DestinationGraphsState extends State { return SfCartesianChart( tooltipBehavior: _leagueTooltipBehavior, zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, + 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: [ @@ -590,34 +691,18 @@ class _DestinationGraphsState extends State { enableTooltip: true, dataSource: snapshot.data, animationDuration: 0, - //opacity: _smooth ? 0 : 1, + //opacity: 0.5, xValueMapper: (Cutoffs data, _) => data.ts, - yValueMapper: (Cutoffs data, _) => data.tr[rank], - color: rankColors[rank], - // trendlines:[ - // Trendline( - // isVisible: _smooth, - // period: (selectedGraph.length/175).floor(), - // type: TrendlineType.movingAverage, - // color: Theme.of(context).colorScheme.primary) - // ], + yValueMapper: (Cutoffs data, _) => switch (_Ychart){ + Stats.glicko => data.glicko[rank], + Stats.gxe => data.gxe[rank], + _ => data.tr[rank] + }, + color: rankColors[rank]! ) ], ); - }else{ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + }else{ return FutureError(snapshot); } } } ); @@ -679,7 +764,7 @@ class _DestinationGraphsState extends State { children: [ const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), DropdownButton( - items: _yAxis, + 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(() { @@ -1254,20 +1339,7 @@ class _DestinationHomeState extends State with SingleTickerProv ], ); } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); }, @@ -1306,20 +1378,7 @@ class _DestinationHomeState extends State with SingleTickerProv ], ); } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); }, @@ -1366,20 +1425,7 @@ class _DestinationHomeState extends State with SingleTickerProv 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 Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); }, @@ -1760,20 +1806,7 @@ class _DestinationHomeState extends State with SingleTickerProv case ConnectionState.active: return const Center(child: CircularProgressIndicator()); case ConnectionState.done: - if (snapshot.hasError){ - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), - ), - ], - ) - ); - } + if (snapshot.hasError){ return FutureError(snapshot); } if (snapshot.hasData){ 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; @@ -1837,13 +1870,7 @@ class _DestinationHomeState extends State with SingleTickerProv case ConnectionState.done: if (snapshot.hasData){ return NewsThingy(snapshot.data!); - }else if (snapshot.hasError){ - return Card(child: Column(children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Text(snapshot.stackTrace.toString()) - ] - )); - } + }else if (snapshot.hasError){ return FutureError(snapshot); } } return const Text("what?"); } @@ -3318,4 +3345,26 @@ class TLRatingThingy extends StatelessWidget{ ], ); } +} + +class FutureError extends StatelessWidget{ + final AsyncSnapshot snapshot; + + FutureError(this.snapshot); + + @override + Widget build(BuildContext context) { + return Center(child: + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(snapshot.stackTrace.toString(), textAlign: TextAlign.center), + ), + ], + ) + ); + } } \ No newline at end of file