diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 01d92fa..da75d04 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -845,10 +845,11 @@ class TetrioPlayersLeaderboard { TetrioPlayersLeaderboard(this.type, this.leaderboard); - TetrioPlayersLeaderboard.fromJson(Map json, String type, DateTime ts) { - type = type; + TetrioPlayersLeaderboard.fromJson(List json, String t, DateTime ts) { + type = t; timestamp = ts; - for (Map entry in json['users']) { + leaderboard = []; + for (Map entry in json) { leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts)); } } @@ -862,9 +863,42 @@ class TetrioPlayerFromLeaderboard { String? country; late bool supporter; late bool verified; - late TetraLeagueAlpha league; + late DateTime timestamp; + late int gamesPlayed; + late int gamesWon; + late double rating; + late double glicko; + late double rd; + late String rank; + late String bestRank; + late double apm; + late double pps; + late double vs; + late bool decaying; - TetrioPlayerFromLeaderboard(this.userId, this.username, this.role, this.xp, this.country, this.supporter, this.verified, this.league); + TetrioPlayerFromLeaderboard( + this.userId, + this.username, + this.role, + this.xp, + this.country, + this.supporter, + this.verified, + this.timestamp, + this.gamesPlayed, + this.gamesWon, + this.rating, + this.glicko, + this.rd, + this.rank, + this.bestRank, + this.apm, + this.pps, + this.vs, + this.decaying); + + get app => apm / (pps * 60); + get vsapm => vs / apm; TetrioPlayerFromLeaderboard.fromJson(Map json, DateTime ts) { userId = json['_id']; @@ -874,6 +908,17 @@ class TetrioPlayerFromLeaderboard { country = json['country ']; supporter = json['supporter']; verified = json['verified']; - league = TetraLeagueAlpha.fromJson(json['league'], ts); + timestamp = ts; + gamesPlayed = json['league']['gamesplayed']; + gamesWon = json['league']['gameswon']; + rating = json['league']['rating'].toDouble(); + glicko = json['league']['glicko'].toDouble(); + rd = json['league']['rd'].toDouble(); + rank = json['league']['rank']; + bestRank = json['league']['bestrank']; + apm = json['league']['apm'].toDouble(); + pps = json['league']['pps'].toDouble(); + vs = json['league']['vs'].toDouble(); + decaying = json['league']['decaying']; } } diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index ace3d54..047642a 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; +import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; @@ -49,6 +52,7 @@ class TetrioService extends DB { Map> _players = {}; final Map _playersCache = {}; final Map> _recordsCache = {}; + final Map _leaderboardsCache = {}; final Map _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; @@ -91,6 +95,39 @@ class TetrioService extends DB { return player.username; } + Future fetchTLLeaderboard() async { + try{ + var cached = _leaderboardsCache.entries.firstWhere((element) => element.value.type == "league"); + if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ + developer.log("fetchTLLeaderboard: Leaderboard retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); + return cached.value; + }else{ + _leaderboardsCache.remove(cached.key); + developer.log("fetchTLLeaderboard: Leaderboard expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); + } + }catch(e){ + developer.log("fetchTLLeaderboard: Trying to retrieve leaderboard", name: "services/tetrio_crud"); + } + + var url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); + final response = await http.get(url); + if (response.statusCode == 200) { + var rawJson = jsonDecode(response.body); + if (rawJson['success']) { + TetrioPlayersLeaderboard leaderboard = TetrioPlayersLeaderboard.fromJson(rawJson['data']['users'], "league", DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at'])); + developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + _leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + return leaderboard; + } else { + developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); + throw Exception("User doesn't exist"); + } + } else { + developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); + throw Exception('Failed to fetch player'); + } + } + Future getTLStream(String userID) async { try{ var cached = _tlStreamsCache.entries.firstWhere((element) => element.value.userId == userID); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index ea2aab6..d9403a0 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView; import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; import 'package:tetra_stats/widgets/stat_sell_num.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; @@ -235,6 +236,8 @@ class _MainState extends State with SingleTickerProviderStateMixin { ), ], onSelected: (value) { + if (value == "tll") {teto.fetchTLLeaderboard(); + return;} Navigator.pushNamed(context, value); }, ), @@ -420,6 +423,20 @@ class _NavDrawerState extends State { Navigator.of(context).pop(); }, ), + ), + SliverToBoxAdapter( + child: ListTile( + leading: const Icon(Icons.leaderboard), + title: const Text("Tetra League leaderboard"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TLLeaderboardView(), + ), + ); + }, + ), ) ]; }, diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart new file mode 100644 index 0000000..1b326c1 --- /dev/null +++ b/lib/views/tl_leaderboard_view.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/services/tetrio_crud.dart'; +import 'package:tetra_stats/views/states_view.dart'; + +final TetrioService teto = TetrioService(); + +class TLLeaderboardView extends StatefulWidget { + const TLLeaderboardView({Key? key}) : super(key: key); + + @override + State createState() => TLLeaderboardState(); +} + +final DateFormat dateFormat = DateFormat.yMMMd().add_Hms(); +final NumberFormat f2 = NumberFormat.decimalPatternDigits(decimalDigits: 2); + +class TLLeaderboardState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Tetra League Leaderboard"), + ), + backgroundColor: Colors.black, + body: SafeArea( + child: FutureBuilder( + future: teto.fetchTLLeaderboard(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const Center(child: Text('Fetching...')); + case ConnectionState.done: + final allPlayers = snapshot.data?.leaderboard; + return NestedScrollView( + headerSliverBuilder: (context, value) { + String howManyPlayers(int numberOfPlayers) => Intl.plural( + numberOfPlayers, + zero: 'Empty list. Press "Track" button in previous view to add current player here', + one: 'There is only one player', + other: 'There are $numberOfPlayers players', + name: 'howManyPeople', + args: [numberOfPlayers], + desc: 'Description of how many people are seen in a place.', + examples: const {'numberOfPeople': 3}, + ); + return [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + howManyPlayers(allPlayers.length), + style: const TextStyle(color: Colors.white, fontSize: 25), + ), + )), + const SliverToBoxAdapter(child: Divider()) + ]; + }, + body: ListView.builder( + itemCount: allPlayers!.length, + itemBuilder: (context, index) { + return ListTile( + leading: Text((index+1).toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), + title: Text("${allPlayers[index].username}", style: const TextStyle(fontFamily: "Eurostile Round Extended")), + subtitle: Text( + "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].app)} APP, ${f2.format(allPlayers[index].vsapm)} VS/APM"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${f2.format(allPlayers[index].rating)} TR", style: const TextStyle(fontSize: 28)), + Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: 48), + ], + ), + onTap: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => StatesView(states: allPlayers!), + // ), + // ); + }, + ); + })); + } + })), + ); + } +}