From 9aa67686da1da2975d37c17dafc551481fcc9f77 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 3 May 2024 01:26:12 +0300 Subject: [PATCH] New p1nkl0bst3r's API endpoint + TL progress bar design For some reason TL progress bar behaves differently on android --- lib/data_objects/tetrio.dart | 7 ++-- lib/services/tetrio_crud.dart | 62 ++++++++++++++++++++++++++++ lib/utils/numers_formats.dart | 1 + lib/views/main_view.dart | 17 ++++---- lib/widgets/tl_progress_bar.dart | 69 ++++++++++++++++++++++---------- lib/widgets/tl_thingy.dart | 6 +-- 6 files changed, 127 insertions(+), 35 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index e503ea5..d34a9f4 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -756,16 +756,15 @@ class EstTr { srarea = (_apm * 0) + (_pps * 135) + (_vs * 0) + (_app * 290) + (_dss * 0) + (_dsp * 700) + (_gbe * 0); statrank = 11.2 * atan((srarea - 93) / 130) + 1; if (statrank <= 0) statrank = 0.001; - estglicko = (4.0867 * srarea + 186.68); + //estglicko = (4.0867 * srarea + 186.68); double ntemp = _pps*(150+(((_vs/_apm) - 1.66)*35))+_app*290+_dsp*700; + estglicko = 0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4; esttr = 25000 / ( 1 + pow(10, ( ( ( - 1500-( - 0.000013*pow(ntemp, 3) - 0.0196 *pow(ntemp, 2) + (12.645*ntemp)-1005.4 - ) + 1500-estglicko )*pi )/sqrt( ( diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index a78532f..d4fd561 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -77,6 +77,7 @@ class TetrioService extends DB { final Map _lbPositions = {}; final Map> _newsCache = {}; final Map> _topTRcache = {}; + final Map>> _cutoffsCache = {}; final Map _tlStreamsCache = {}; /// Thing, that sends every request to the API endpoints final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); @@ -297,6 +298,67 @@ class TetrioService extends DB { // Sidenote: as you can see, fetch functions looks and works pretty much same way, as described above, // so i'm going to document only unique differences between them + Future>> fetchCutoffs() async { + try{ + var cached = _cutoffsCache.entries.first; + if (DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true).isAfter(DateTime.now())){ // if not expired + developer.log("fetchCutoffs: Cutoffs retrieved from cache, that expires ${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)}", name: "services/tetrio_crud"); + return cached.value; + }else{ // if cache expired + _topTRcache.remove(cached.key); + developer.log("fetchCutoffs: Cutoffs expired (${DateTime.fromMillisecondsSinceEpoch(int.parse(cached.key.toString()), isUtc: true)})", name: "services/tetrio_crud"); + } + }catch(e){ // actually going to obtain + developer.log("fetchCutoffs: Trying to retrieve Cutoffs", name: "services/tetrio_crud"); + } + + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "PeakTR"}); + } else { + url = Uri.https('api.p1nkl0bst3r.xyz', 'rankcutoff', {"users": null}); + } + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + Map rawData = jsonDecode(response.body); + Map data = rawData["cutoffs"] as Map; + Map trCutoffs = {}; + Map glickoCutoffs = {}; + for (String rank in data.keys){ + trCutoffs[rank] = data[rank]["rating"]; + glickoCutoffs[rank] = data[rank]["glicko"]; + } + _cutoffsCache[(rawData["ts"] + 300000).toString()] = [trCutoffs, glickoCutoffs]; + return [trCutoffs, glickoCutoffs]; + case 404: + developer.log("fetchCutoffs: Cutoffs are gone", name: "services/tetrio_crud", error: response.statusCode); + return []; + // if not 200 or 404 - throw a unique for each code exception + case 403: + throw P1nkl0bst3rForbidden(); + case 429: + throw P1nkl0bst3rTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw P1nkl0bst3rInternalProblem(); + default: + developer.log("fetchCutoffs: Failed to fetch top Cutoffs", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { // If local http client fails + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); // just assuming, that our end user don't have acess to the internet + } + } + /// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data. Future> fetchAndsaveTLHistory(String id) async { diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index 412820d..bd6c119 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -2,6 +2,7 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; +final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2; final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); final NumberFormat f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final NumberFormat f3 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 3); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 2124a3f..7be92d5 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -182,12 +182,13 @@ class _MainState extends State with TickerProviderStateMixin { teto.fetchTLStream(_searchFor), teto.fetchRecords(_searchFor), teto.fetchNews(_searchFor), + prefs.getBool("showPositions") != true ? teto.fetchCutoffs() : Future.delayed(Duration.zero, ()=>[]), if (me.tlSeason1.gamesPlayed > 9) teto.fetchTopTR(_searchFor) // can retrieve this only if player has TR ]); tlStream = requests[0] as TetraLeagueAlphaStream; records = requests[1] as Map; news = requests[2] as List; - topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR + topTR = requests.elementAtOrNull(4) as double?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); if (prefs.getBool("showPositions") == true){ @@ -198,15 +199,17 @@ class _MainState extends State with TickerProviderStateMixin { meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); } - if (me.tlSeason1.gamesPlayed > 9) { - thatRankCutoff = everyone!.cutoffs[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; - thatRankGlickoCutoff = everyone!.cutoffsGlicko[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; - nextRankCutoff = everyone!.cutoffs[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; - nextRankGlickoCutoff = everyone!.cutoffsGlicko[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; + } + Map cutoffs = prefs.getBool("showPositions") == true ? everyone!.cutoffs : requests[3][0]; + Map cutoffsGlicko = prefs.getBool("showPositions") == true ? everyone!.cutoffsGlicko : requests[3][1]; + if (me.tlSeason1.gamesPlayed > 9) { + thatRankCutoff = cutoffs[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; + thatRankGlickoCutoff = cutoffsGlicko[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; + nextRankCutoff = cutoffs[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; + nextRankGlickoCutoff = cutoffsGlicko[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; nextRankCutoff = nextRankCutoff??25000; nextRankGlickoCutoff = nextRankGlickoCutoff??double.infinity; } - } if (everyone != null && me.tlSeason1.gamesPlayed > 9) rankAverages = everyone?.averages[me.tlSeason1.percentileRank]?[0]; diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index 41df77d..d3c549c 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -33,10 +33,49 @@ class TLProgress extends StatelessWidget{ Widget build(BuildContext context) { final glickoForWin = rate(tlData.glicko!, tlData.rd!, 0.06, [[tlData.glicko!, tlData.rd!, 1]], {})[0]-tlData.glicko!; return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column( mainAxisSize: MainAxisSize.min, children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 48, + child: Stack( + alignment: AlignmentDirectional.bottomCenter, + fit: StackFit.expand, + children: [ + Positioned(left: 0, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + children: [ + if (tlData.prevAt > 0) TextSpan(text: "№ ${f0.format(tlData.prevAt)}"), + if (tlData.prevAt > 0 && previousRankTRcutoff != null) TextSpan(text: "\n"), + if (previousRankTRcutoff != null) TextSpan(text: "${intf.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) TextSpan(text: "\n"), + if (previousGlickoCutoff != null) TextSpan(text: "~${f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)} defeats") + ] + ) + ), + ), + Positioned(right: 0, + child: RichText( + textAlign: TextAlign.right, + text: TextSpan( + style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round", fontSize: 12), + children: [ + if (tlData.nextAt > 0) TextSpan(text: "№ ${f0.format(tlData.nextAt)}"), + if (tlData.nextAt > 0 && nextRankTRcutoff != null) TextSpan(text: "\n"), + if (nextRankTRcutoff != null) TextSpan(text: "${intf.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.rating)}) TR"), + if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) TextSpan(text: "\n"), + if (nextRankGlickoCutoff != null) TextSpan(text: "~${f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)} victories") + ] + ) + ), + ) + ],), + ), SfLinearGauge( minimum: 0, maximum: 1, @@ -49,34 +88,22 @@ class TLProgress extends StatelessWidget{ ], markerPointers: [ LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), - LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}")) + if (tlData.standing != -1) LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.rating)! : getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(tlData.standing)}", style: TextStyle(fontSize: 14),)) ], isMirrored: true, showTicks: true, - axisLabelStyle: TextStyle(), - onGenerateLabels: () => [ - LinearAxisLabel(text: "${tlData.prevAt > 0 ? "№ ${f0.format(tlData.prevAt)}" : ""}\n ${intf.format(previousRankTRcutoff)} TR", value: 0), - LinearAxisLabel(text: "${tlData.nextAt > 0 ? "№ ${f0.format(tlData.nextAt)}" : ""}\n ${intf.format(nextRankTRcutoff)} TR", value: 1), - ], + // onGenerateLabels: () => [ + // LinearAxisLabel(text: "${tlData.prevAt > 0 ? "№ ${f0.format(tlData.prevAt)}" : ""}\n ${intf.format(previousRankTRcutoff)} TR", value: 0), + // LinearAxisLabel(text: "${tlData.nextAt > 0 ? "№ ${f0.format(tlData.nextAt)}" : ""}\n ${intf.format(nextRankTRcutoff)} TR", value: 1), + // ], // labelFormatterCallback: (value) { // if (value == "0") return "${f0.format(previousRankPosition)}\n 26,700 TR"; // else return f0.format(nextRankPosition); // }, - showLabels: true - ), - Container( - width: MediaQuery.of(context).size.width, - height: 20, - child: Stack( - fit: StackFit.expand, - children: [ - Positioned(child: Text("${f2.format(tlData.rating-previousRankTRcutoff!)} (${f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)} losses)"), left: 0,), - Positioned(child: Text("${f2.format(nextRankTRcutoff!-tlData.rating)} (${f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)} wins)"), right: 0,) - ],), + showLabels: false ) - - ], - ) + ] + ), ); } } \ No newline at end of file diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 0bceac8..3c02508 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -31,7 +31,7 @@ class TLThingy extends StatefulWidget { final double? nextRankCutoff; final double? nextRankCutoffGlicko; final double? nextRankTarget; - const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff = 25000, this.thatRankCutoff = 0, this.thatRankCutoffGlicko = 0, this.nextRankCutoffGlicko = double.infinity, this.nextRankTarget = 25000, this.thatRankTarget = 0}); + const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget}); @override State createState() => _TLThingyState(); @@ -342,9 +342,9 @@ class _TLThingyState extends State { if (oldTl?.estTr?.esttr != null) TextSpan(text: comparef.format(currentTl.estTr!.esttr - oldTl!.estTr!.esttr), style: TextStyle( color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent ),), - if (oldTl?.estTr?.esttr != null || widget.lbPositions?.estTr != null) const TextSpan(text: " • "), + if (oldTl?.estTr?.esttr != null) const TextSpan(text: " • "), if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.estTr!.position))), - if (widget.lbPositions?.estTr != null) const TextSpan(text: " • "), + if (widget.lbPositions?.estTr != null || oldTl?.estTr?.esttr != null) const TextSpan(text: " • "), TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") ] ),