Lazy loading for a leaderboards in redesign was implemented

This commit is contained in:
dan63047 2024-09-15 01:05:50 +03:00
parent a2a85ce151
commit c3214f5ba9
4 changed files with 255 additions and 160 deletions

View File

@ -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) {

View File

@ -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);

View File

@ -798,34 +798,60 @@ class TetrioService extends DB {
}
}
// Stream<TetrioPlayersLeaderboard> 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<TetrioPlayersLeaderboard> fetchTLLeaderboardAsStream() async {
Future<List<TetrioPlayerFromLeaderboard>> 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<TetrioPlayersLeaderboard> 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<TetrioPlayerFromLeaderboard> leaderboard = [];
for (Map<String, dynamic> 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);

View File

@ -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<MainView> with TickerProviderStateMixin {
}
}
class DestinationCutoffs extends StatefulWidget{
final BoxConstraints constraints;
const DestinationCutoffs({super.key, required this.constraints});
@override
State<DestinationLeaderboards> createState() => _DestinationCutoffsState();
}
class _DestinationCutoffsState extends State<DestinationLeaderboards> {
@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<DestinationLeaderboards> createState() => _DestinationLeaderboardsState();
}
enum Leaderboards{
tl,
xp,
ar
}
class _DestinationLeaderboardsState extends State<DestinationLeaderboards> {
Cards rightCard = Cards.tetraLeague;
//Duration postSeasonLeft = seasonStart.difference(DateTime.now());
final List<String> leaderboards = ["Tetra League", "Quick Play", "Quick Play Expert"];
final Map<Leaderboards, String> leaderboards = {Leaderboards.tl: "Tetra League", Leaderboards.xp: "XP", Leaderboards.ar: "Acievement Points"};
Leaderboards _currentLb = Leaderboards.tl;
final StreamController<List<TetrioPlayerFromLeaderboard>> _dataStreamController = StreamController<List<TetrioPlayerFromLeaderboard>>();
late final ScrollController _scrollController;
Stream<List<TetrioPlayerFromLeaderboard>> get dataStream => _dataStreamController.stream;
List<TetrioPlayerFromLeaderboard> list = [];
bool _isFetchingData = false;
double after = 25000.00;
Future<void> _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<DestinationLeaderboards> {
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<DestinationLeaderboards> {
),
SizedBox(
width: widget.constraints.maxWidth - 350 - 88,
child: const Card(
child: Column(
child: Card(
child: StreamBuilder<List<TetrioPlayerFromLeaderboard>>(
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<DestinationGraphs> {
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<DropdownMenuItem<Stats>> _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<List<DropdownMenuItem<List<_HistoryChartSpot>>>> historyData;
//Duration postSeasonLeft = seasonStart.difference(DateTime.now());
@override
@ -365,7 +488,6 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
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<DestinationGraphs> {
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<DestinationGraphs> {
),
],
);
}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<DestinationGraphs> {
)
],
);
}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<DestinationGraphs> {
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: <CartesianSeries>[
@ -590,34 +691,18 @@ class _DestinationGraphsState extends State<DestinationGraphs> {
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>[
// 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<DestinationGraphs> {
children: [
const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))),
DropdownButton<Stats>(
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<DestinationHome> 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<DestinationHome> 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<DestinationHome> 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<DestinationHome> 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<DestinationHome> 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?");
}
@ -3319,3 +3346,25 @@ 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),
),
],
)
);
}
}