diff --git a/lib/data_objects/record_single.dart b/lib/data_objects/record_single.dart index 92eb304..6106a85 100644 --- a/lib/data_objects/record_single.dart +++ b/lib/data_objects/record_single.dart @@ -3,9 +3,11 @@ import 'package:tetra_stats/data_objects/aggregate_stats.dart'; import 'package:tetra_stats/data_objects/record_extras.dart'; import 'package:tetra_stats/data_objects/results_stats.dart'; +import 'package:tetra_stats/data_objects/tetrio_prisecter.dart'; class RecordSingle { late String? userId; + late String username; late String replayId; late String ownId; late String gamemode; @@ -15,8 +17,9 @@ class RecordSingle { late int countryRank; late AggregateStats aggregateStats; late RecordExtras extras; + late Prisecter prisecter; - RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); + RecordSingle({required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); RecordSingle.fromJson(Map json, int ran, int cran) { ownId = json['_id']; @@ -24,10 +27,14 @@ class RecordSingle { stats = ResultsStats.fromJson(json['results']['stats']); replayId = json['replayid']; timestamp = DateTime.parse(json['ts']); - if (json['user'] != null) userId = json['user']['id']; + if (json['user'] != null) { + userId = json['user']['id']; + username = json['user']['username']; + } rank = ran; countryRank = cran; aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']); + prisecter = Prisecter.fromJson(json['p']); var ex = json['extras'] as Map; switch (ex.keys.firstOrNull){ case "zenith": diff --git a/lib/data_objects/tetrio_player_from_leaderboard.dart b/lib/data_objects/tetrio_player_from_leaderboard.dart index ad820dc..80ba9b8 100644 --- a/lib/data_objects/tetrio_player_from_leaderboard.dart +++ b/lib/data_objects/tetrio_player_from_leaderboard.dart @@ -6,6 +6,7 @@ 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/data_objects/tetrio_prisecter.dart'; class TetrioPlayerFromLeaderboard { late String userId; @@ -33,6 +34,8 @@ class TetrioPlayerFromLeaderboard { late int gamesWonTotal; late Duration playtime; late int ar; + late Map ar_counts; + late Prisecter prisecter; TetrioPlayerFromLeaderboard( this.userId, @@ -91,6 +94,11 @@ class TetrioPlayerFromLeaderboard { gamesWonTotal = json['gameswon'] as int; playtime = Duration(microseconds: (json['gametime'].toDouble() * 1000000).floor()); ar = json['ar']; + ar_counts = {}; + for (var entry in json['ar_counts'].keys){ + ar_counts[entry.toString()] = json['ar_counts'][entry]; + } + prisecter = Prisecter.fromJson(json['p']); 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/data_objects/tetrio_prisecter.dart b/lib/data_objects/tetrio_prisecter.dart new file mode 100644 index 0000000..6595cb1 --- /dev/null +++ b/lib/data_objects/tetrio_prisecter.dart @@ -0,0 +1,18 @@ +class Prisecter { + late final num pri; + late final num sec; + late final num ter; + + Prisecter(this.pri, this.sec, this.ter); + + @override + String toString() { + return "${pri}:${sec}:${ter}"; + } + + Prisecter.fromJson(Map json){ + pri = json['pri']; + sec = json['sec']; + ter = json['ter']; + } +} \ No newline at end of file diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 5e042ea..be4466f 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -798,8 +798,7 @@ class TetrioService extends DB { } } - Future> fetchTetrioLeaderboard({double? after, String? lb}) async { - const int lbLength = 100; + Future> fetchTetrioLeaderboard({String? prisecter, String? lb}) async { // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); // if (cached != null) return cached; @@ -809,7 +808,7 @@ class TetrioService extends DB { } else { url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', { "limit": "100", - if (after != null && after != -1) "after": "$after:0:0" + if (prisecter != null) "after": prisecter }); } try{ @@ -853,6 +852,57 @@ class TetrioService extends DB { } } + Future> fetchTetrioRecordsLeaderboard({String? prisecter, String? lb}) async{ + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); + } else { + url = Uri.https('ch.tetr.io', 'api/records/${lb??"40l_global"}', { + "limit": "100", + if (prisecter != null) "after": prisecter + }); + } + try{ + final response = await client.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(RecordSingle.fromJson(entry, -1, -1)); + } + 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 c30ccf9..578473c 100644 --- a/lib/views/main_view_tiles.dart +++ b/lib/views/main_view_tiles.dart @@ -200,6 +200,7 @@ class _MainState extends State with TickerProviderStateMixin { 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), 2 => DestinationLeaderboards(constraints: constraints), + 3 => DestinationCutoffs(constraints: constraints), _ => Text("Unknown destination $destination") }, ) @@ -215,14 +216,17 @@ class DestinationCutoffs extends StatefulWidget{ const DestinationCutoffs({super.key, required this.constraints}); @override - State createState() => _DestinationCutoffsState(); + State createState() => _DestinationCutoffsState(); } -class _DestinationCutoffsState extends State { +class _DestinationCutoffsState extends State { @override Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); + return Column( + children: [ + Card(), + ] + ); } } @@ -238,19 +242,31 @@ class DestinationLeaderboards extends StatefulWidget{ enum Leaderboards{ tl, xp, - ar + ar, + sprint, + blitz, + zenith, + zenithex, } class _DestinationLeaderboardsState extends State { //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - final Map leaderboards = {Leaderboards.tl: "Tetra League", Leaderboards.xp: "XP", Leaderboards.ar: "Acievement Points"}; + final Map leaderboards = { + Leaderboards.tl: "Tetra League", + 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>(); + final StreamController> _dataStreamController = StreamController>(); late final ScrollController _scrollController; - Stream> get dataStream => _dataStreamController.stream; - List list = []; + Stream> get dataStream => _dataStreamController.stream; + List list = []; bool _isFetchingData = false; - double after = 25000.00; + String? prisecter; Future _fetchData() async { if (_isFetchingData) { @@ -262,19 +278,19 @@ class _DestinationLeaderboardsState extends State { 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"), + Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter), + Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp"), + Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar"), + Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter), + Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz_global"), + Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith_global"), + Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex_global"), }; 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(), - }; + prisecter = list.last.prisecter.toString(); } catch (e) { _dataStreamController.addError(e); } finally { @@ -332,11 +348,7 @@ class _DestinationLeaderboardsState extends State { onTap: () { _currentLb = leaderboards.keys.elementAt(index); list.clear(); - after = switch (_currentLb){ - Leaderboards.tl => 25000.00, - Leaderboards.xp => -1.00, - Leaderboards.ar => -1.00, - }; + prisecter = null; _fetchData(); }, ), @@ -350,7 +362,7 @@ class _DestinationLeaderboardsState extends State { SizedBox( width: widget.constraints.maxWidth - 350 - 88, child: Card( - child: StreamBuilder>( + child: StreamBuilder>( stream: dataStream, builder:(context, snapshot) { switch (snapshot.connectionState){ @@ -371,12 +383,25 @@ class _DestinationLeaderboardsState extends State { itemBuilder: (BuildContext context, int index){ return ListTile( leading: Text(intf.format(index+1)), - title: Text(snapshot.data![index].username), + title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)), 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), - }), + Leaderboards.tl => "${f2.format(snapshot.data![index].tr)} TR", + Leaderboards.xp => "LVL ${f2.format(snapshot.data![index].level)}", + Leaderboards.ar => "${intf.format(snapshot.data![index].ar)} AR", + Leaderboards.sprint => get40lTime(snapshot.data![index].stats.finalTime.inMicroseconds), + Leaderboards.blitz => intf.format(snapshot.data![index].stats.score), + Leaderboards.zenith => "${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", + Leaderboards.zenithex => "${f2.format(snapshot.data![index].stats.zenith!.altitude)} m" + }, style: TextStyle(fontSize: 28)), + 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.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.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)), ); } ),