commit
8d0a315ae0
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Tell me what is wrong with app
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**How to reproduce**
|
||||
What did you do to got it, something like:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Technical information**
|
||||
- Platform [Windows, Linux or Android]
|
||||
- App Version
|
||||
- Screen size (if it's visual bug)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -0,0 +1,63 @@
|
|||
name: Bug Report
|
||||
description: Tell me what is wrong with app by filling this form.
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please, make sure that your issue haven't been reported before!
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Describe the issue you are experiencing right now
|
||||
placeholder: This thing doesn't work as intended under certain circumstances
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproducing
|
||||
attributes:
|
||||
label: How did that happened?
|
||||
description: Describe in details what to do to get this issue
|
||||
placeholder: "Steps to reproduce:\n1. Go here\n2. Click this\n3. Do that\netc..."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expectations
|
||||
attributes:
|
||||
label: What did you expected?
|
||||
description: What should have happened instead?
|
||||
placeholder: There is supposed to be ... instead
|
||||
- type: checkboxes
|
||||
id: platform
|
||||
attributes:
|
||||
label: On which platforms you encountered this issue?
|
||||
description: Tick the ones, where this issue can be reproduced
|
||||
options:
|
||||
- label: Web (ts.dan63.by)
|
||||
- label: Windows
|
||||
- label: Linux
|
||||
- label: Android
|
||||
- label: Web-beta (tsbeta.dan63.by)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What version of Tetra Stats did you used?
|
||||
description: You can find that info in Information Center -> About Tetra Stats
|
||||
placeholder: "2.0.0"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Have anything more to say about that issue?
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: My repo have [Code of Conduct](https://example.com), which means that you should behave well.
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: What do you wanna see in the app
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is it related to a problem?**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Elaborate about your feature**
|
||||
A clear and concise description of what you want to see.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
|
@ -0,0 +1,36 @@
|
|||
name: Feature request
|
||||
description: Tell me what you want to see in this app by filling this form.
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: If your request does exist or it's similar to existing one, it's better to support existing one issue!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is it related to a problem?
|
||||
description: Is your feature solves some problem?
|
||||
placeholder: I don't like how i can't see or do this or that
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: Elaborate about your feature
|
||||
description: Describe in details what you want to see
|
||||
placeholder: A thing, that allows us to see or do that! It's small and fluffy (what?)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: What makes you think that is a good idea, or maybe, where did you saw that feature
|
||||
placeholder: MinoMuncher can do this and that and I think in could be a good addition to Tetra Stats
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: My repo have [Code of Conduct](https://example.com), which means that you should behave well.
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
discussionCategory: autobuilded-releases
|
||||
artifacts: "build/windows/x64/runner/Release/TetraStats-${{github.ref_name}}-windows.zip"
|
||||
tag: Auto-${{ github.run_number }}
|
||||
body: Builded with GitHub Action workflow
|
||||
body: Build with GitHub Action workflow
|
||||
token: ${{ secrets.TOKEN }}
|
||||
build-and-release-linux:
|
||||
name: Build Linux App
|
||||
|
@ -71,7 +71,7 @@ jobs:
|
|||
discussionCategory: autobuilded-releases
|
||||
artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
|
||||
tag: Auto-${{ github.run_number }}
|
||||
body: Builded with GitHub Action workflow
|
||||
body: Build with GitHub Action workflow
|
||||
token: ${{ secrets.TOKEN }}
|
||||
# build-and-release-android:
|
||||
# name: Build Android App
|
||||
|
@ -96,5 +96,5 @@ jobs:
|
|||
# discussionCategory: autobuilded-releases
|
||||
# artifacts: "build/app/outputs/flutter-apk/*"
|
||||
# tag: Auto-${{ github.run_number }}
|
||||
# body: Builded with GitHub Action workflow
|
||||
# body: Build with GitHub Action workflow
|
||||
# token: ${{ secrets.TOKEN }}
|
19
README.md
19
README.md
|
@ -2,26 +2,27 @@
|
|||
|
||||
Track your and other players stats in TETR.IO
|
||||
|
||||
Tetra Stats works with TETR.IO Tetra Channel API, providing data from it and calculating some addtitional metrics, based on this data.
|
||||
|
||||
You can [download an app](https://github.com/dan63047/TetraStats/releases), or [use web version](https://ts.dan63.by).
|
||||
|
||||
![Screenshot of the app 1](https://imgur.com/e8CYvj3.png)
|
||||
|
||||
# Available functionality
|
||||
- Advanced stats for players
|
||||
- Charts for analyzing players Tetra League standing and Tetra League itself
|
||||
- Ranks cutoffs
|
||||
- Minimums, averages, and maximums for every stat of every rank, as well, as whole leaderboard
|
||||
- Chart for analyzing tetra league state
|
||||
- Full and sortable Tetra League leagerboard
|
||||
- Stats and Damage Calculator
|
||||
- Local database, that can store players data
|
||||
- Comparison to players, rank averages, and player stats from the past
|
||||
- Stats Calculator
|
||||
- Player history in charts
|
||||
- Tetra League matches history
|
||||
- Time-weighted stats in Tetra League matches
|
||||
|
||||
# Special thanks
|
||||
- **kerrmunism** — formulas
|
||||
- **p1nkl0bst3r** — providing players history and peak TR
|
||||
- **neko_ab4093** — Simplified Chinese localization
|
||||
- **osk** and his team — TETR.IO
|
||||
|
||||
## Legal note
|
||||
I do NOT own any assets located in `/res/*`, excluding app icon (`/res/icons/app.png`) and localization (`/res/i18n/*`), which is distributed under GNU license (as well, as this software)
|
||||
## Legal notes
|
||||
Tetra Stats is not associated with TETR.IO or osk in any capacity.
|
||||
|
||||
I do NOT own any assets located in `/res/*`, excluding app icon (`/res/icons/app.png`), localization (`/res/i18n/*`) and images (`/res/images/*`), which is distributed under GNU license (as well, as this software)
|
|
@ -7,7 +7,10 @@
|
|||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
errors:
|
||||
use_build_context_synchronously: ignore
|
||||
in ignoreclude: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
|
|
|
@ -51,6 +51,7 @@ android {
|
|||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.dan63.tetra_stats"
|
||||
testApplicationId "com.dan63.tetra_stats.dev_build"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
files:
|
||||
- source: /res/i18n/strings.i18n.json
|
||||
ignore:
|
||||
- /res/i18n/old_*.json
|
||||
translation: /res/i18n/strings_%locale%.i18n.%file_extension%
|
|
@ -0,0 +1,103 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class Achievement {
|
||||
late int k;
|
||||
int? o;
|
||||
late int rt;
|
||||
late int vt;
|
||||
late int min;
|
||||
late int deci;
|
||||
late String name;
|
||||
late String object;
|
||||
late String category;
|
||||
late bool hidden;
|
||||
late int art;
|
||||
late bool nolb;
|
||||
late String desc;
|
||||
late String n;
|
||||
String? sId;
|
||||
double? v;
|
||||
late int? a;
|
||||
DateTime? t;
|
||||
int? pos;
|
||||
int? total;
|
||||
int? rank;
|
||||
|
||||
Achievement(
|
||||
{required this.k,
|
||||
this.o,
|
||||
required this.rt,
|
||||
required this.vt,
|
||||
required this.min,
|
||||
required this.deci,
|
||||
required this.name,
|
||||
required this.object,
|
||||
required this.category,
|
||||
required this.hidden,
|
||||
required this.art,
|
||||
required this.nolb,
|
||||
required this.desc,
|
||||
required this.n,
|
||||
this.sId,
|
||||
this.v,
|
||||
required this.a,
|
||||
this.t,
|
||||
this.pos,
|
||||
this.total,
|
||||
this.rank});
|
||||
|
||||
@override
|
||||
String toString(){
|
||||
return "${name}: ${v}";
|
||||
}
|
||||
|
||||
Achievement.fromJson(Map<String, dynamic> json) {
|
||||
k = json['k'];
|
||||
o = json['o'];
|
||||
rt = json['rt'];
|
||||
vt = json['vt'];
|
||||
min = json['min'];
|
||||
deci = json['deci'];
|
||||
name = json['name'];
|
||||
object = json['object'];
|
||||
category = json['category'];
|
||||
hidden = json['hidden'];
|
||||
art = json['art'];
|
||||
nolb = json['nolb'];
|
||||
desc = json['desc'];
|
||||
n = json['n'];
|
||||
sId = json['_id'];
|
||||
v = json['v']?.toDouble();
|
||||
a = json['a'];
|
||||
t = json['t'] != null ? DateTime.parse(json['t']) : null;
|
||||
pos = json['pos'];
|
||||
total = json['total'];
|
||||
rank = json['rank'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['k'] = k;
|
||||
data['o'] = o;
|
||||
data['rt'] = rt;
|
||||
data['vt'] = vt;
|
||||
data['min'] = min;
|
||||
data['deci'] = deci;
|
||||
data['name'] = name;
|
||||
data['object'] = object;
|
||||
data['category'] = category;
|
||||
data['hidden'] = hidden;
|
||||
data['art'] = art;
|
||||
data['nolb'] = nolb;
|
||||
data['desc'] = desc;
|
||||
data['n'] = n;
|
||||
data['_id'] = sId;
|
||||
data['v'] = v;
|
||||
data['a'] = a;
|
||||
data['t'] = t.toString();
|
||||
data['pos'] = pos;
|
||||
data['total'] = total;
|
||||
data['rank'] = rank;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
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';
|
||||
|
||||
class AggregateStats{
|
||||
late double apm;
|
||||
late double pps;
|
||||
late double vs;
|
||||
late NerdStats nerdStats;
|
||||
late EstTr estTr;
|
||||
late Playstyle playstyle;
|
||||
|
||||
AggregateStats(this.apm, this.pps, this.vs){
|
||||
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);
|
||||
}
|
||||
|
||||
AggregateStats.precalculated(this.apm, this.pps, this.vs, this.nerdStats, this.playstyle);
|
||||
|
||||
AggregateStats.fromJson(Map<String, dynamic> json){
|
||||
apm = json['apm'] != null ? json['apm'].toDouble() : 0.00;
|
||||
pps = json['apm'] != null ? json['pps'].toDouble() : 0.00;
|
||||
vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00;
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class Badge {
|
||||
late String badgeId;
|
||||
late String label;
|
||||
DateTime? ts;
|
||||
|
||||
Badge({required this.badgeId, required this.label, this.ts});
|
||||
|
||||
Badge.fromJson(Map<String, dynamic> json) {
|
||||
badgeId = json['id'];
|
||||
label = json['label'];
|
||||
ts = (json['ts'] != null && json['ts'] is String) ? DateTime.parse(json['ts']) : null; // man i love osk
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = badgeId;
|
||||
data['label'] = label;
|
||||
data['ts'] = ts?.toString();
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "Badge $label ($badgeId)";
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => badgeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Badge other) => badgeId == other.badgeId;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/beta_league_stats.dart';
|
||||
|
||||
class BetaLeagueLeaderboardEntry{
|
||||
late String id;
|
||||
late String username;
|
||||
late int naturalorder;
|
||||
late int wins;
|
||||
late BetaLeagueStats stats;
|
||||
|
||||
BetaLeagueLeaderboardEntry({required this.id, required this.username, required this.naturalorder, required this.wins, required this.stats});
|
||||
|
||||
BetaLeagueLeaderboardEntry.fromJson(Map<String, dynamic> json){
|
||||
id = json['id'];
|
||||
username = json['username'];
|
||||
naturalorder = json['naturalorder'];
|
||||
wins = json['wins'];
|
||||
stats = BetaLeagueStats.fromJson(json['stats']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/beta_league_leaderboard_entry.dart';
|
||||
import 'package:tetra_stats/data_objects/beta_league_round.dart';
|
||||
|
||||
class BetaLeagueResults{
|
||||
List<BetaLeagueLeaderboardEntry> leaderboard = [];
|
||||
List<List<BetaLeagueRound>> rounds = [];
|
||||
|
||||
BetaLeagueResults({required this.leaderboard, required this.rounds});
|
||||
|
||||
BetaLeagueResults.fromJson(Map<String, dynamic> json){
|
||||
for (var lbEntry in json['leaderboard']) {
|
||||
leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry));
|
||||
}
|
||||
for (var roundEntry in json['rounds']){
|
||||
List<BetaLeagueRound> round = [];
|
||||
for (var r in roundEntry) {
|
||||
round.add(BetaLeagueRound.fromJson(r));
|
||||
}
|
||||
rounds.add(round);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/beta_league_stats.dart';
|
||||
|
||||
class BetaLeagueRound{
|
||||
late String id;
|
||||
late String username;
|
||||
late bool active;
|
||||
late int naturalorder;
|
||||
late bool alive;
|
||||
late Duration lifetime;
|
||||
late BetaLeagueStats stats;
|
||||
|
||||
BetaLeagueRound({required this.id, required this.username, required this.active, required this.naturalorder, required this.alive, required this.lifetime, required this.stats});
|
||||
|
||||
BetaLeagueRound.fromJson(Map<String, dynamic> json){
|
||||
id = json['id'];
|
||||
username = json['username'];
|
||||
active = json['active'];
|
||||
naturalorder = json['naturalorder'];
|
||||
alive = json['alive'];
|
||||
lifetime = Duration(milliseconds: json['lifetime']);
|
||||
stats = BetaLeagueStats.fromJson(json['stats']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
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';
|
||||
|
||||
class BetaLeagueStats{
|
||||
late double apm;
|
||||
late double pps;
|
||||
late double vs;
|
||||
late int garbageSent;
|
||||
late int garbageReceived;
|
||||
late int kills;
|
||||
late double altitude;
|
||||
late int rank;
|
||||
int? targetingFactor;
|
||||
int? targetingRace;
|
||||
late NerdStats nerdStats;
|
||||
late EstTr estTr;
|
||||
late Playstyle playstyle;
|
||||
|
||||
BetaLeagueStats({required this.apm, required this.pps, required this.vs, required this.garbageSent, required this.garbageReceived, required this.kills, required this.altitude, required this.rank}){
|
||||
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);
|
||||
}
|
||||
|
||||
BetaLeagueStats.fromJson(Map<String, dynamic> json){
|
||||
apm = json['apm'] != null ? json['apm'].toDouble() : 0.00;
|
||||
pps = json['apm'] != null ? json['pps'].toDouble() : 0.00;
|
||||
vs = json['apm'] != null ? json['vsscore'].toDouble() : 0.00;
|
||||
garbageSent = json['garbagesent'];
|
||||
garbageReceived = json['garbagereceived'];
|
||||
kills = json['kills'];
|
||||
altitude = json['altitude'].toDouble();
|
||||
rank = json['rank'];
|
||||
targetingFactor = json['targetingfactor'];
|
||||
targetingRace = json['targetinggrace'];
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/beta_league_results.dart';
|
||||
import 'package:tetra_stats/data_objects/record_extras.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_prisecter.dart';
|
||||
|
||||
class BetaRecord{
|
||||
late String id;
|
||||
late String replayID;
|
||||
late String gamemode;
|
||||
late DateTime ts;
|
||||
late String enemyUsername;
|
||||
late String enemyID;
|
||||
late BetaLeagueResults results;
|
||||
late LeagueExtras extras;
|
||||
late Prisecter prisecter;
|
||||
|
||||
BetaRecord({required this.id, required this.replayID, required this.gamemode, required this.ts, required this.enemyUsername, required this.enemyID, required this.results});
|
||||
|
||||
BetaRecord.fromJson(Map<String, dynamic> json){
|
||||
id = json['_id'];
|
||||
replayID = json['replayid'];
|
||||
gamemode = json['gamemode'];
|
||||
ts = DateTime.parse(json['ts']);
|
||||
enemyUsername = json['otherusers'][0]['username'];
|
||||
enemyID = json['otherusers'][0]['id'];
|
||||
results = BetaLeagueResults.fromJson(json['results']);
|
||||
prisecter = Prisecter.fromJson(json['p']);
|
||||
extras = LeagueExtras.fromJson(json['extras']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class Clears {
|
||||
late int singles;
|
||||
late int doubles;
|
||||
late int triples;
|
||||
late int quads;
|
||||
late int pentas;
|
||||
late int allClears;
|
||||
late int tSpinZeros;
|
||||
late int tSpinSingles;
|
||||
late int tSpinDoubles;
|
||||
late int tSpinTriples;
|
||||
late int tSpinQuads;
|
||||
late int tSpinPentas;
|
||||
late int tSpinMiniZeros;
|
||||
late int tSpinMiniSingles;
|
||||
late int tSpinMiniDoubles;
|
||||
late int tSpinMiniTriples;
|
||||
late int tSpinMiniQuads;
|
||||
|
||||
Clears(
|
||||
{required this.singles,
|
||||
required this.doubles,
|
||||
required this.triples,
|
||||
required this.quads,
|
||||
required this.pentas,
|
||||
required this.allClears,
|
||||
required this.tSpinZeros,
|
||||
required this.tSpinSingles,
|
||||
required this.tSpinDoubles,
|
||||
required this.tSpinTriples,
|
||||
required this.tSpinPentas,
|
||||
required this.tSpinQuads,
|
||||
required this.tSpinMiniZeros,
|
||||
required this.tSpinMiniSingles,
|
||||
required this.tSpinMiniDoubles,
|
||||
required this.tSpinMiniTriples,
|
||||
required this.tSpinMiniQuads});
|
||||
|
||||
Clears.fromJson(Map<String, dynamic> json) {
|
||||
singles = json['singles'];
|
||||
doubles = json['doubles'];
|
||||
triples = json['triples'];
|
||||
quads = json['quads'];
|
||||
pentas = json['pentas']??0;
|
||||
tSpinZeros = json['realtspins'];
|
||||
tSpinMiniZeros = json['minitspins'];
|
||||
tSpinMiniSingles = json['minitspinsingles'];
|
||||
tSpinSingles = json['tspinsingles'];
|
||||
tSpinMiniDoubles = json['minitspindoubles'];
|
||||
tSpinDoubles = json['tspindoubles'];
|
||||
tSpinMiniTriples = json['minitspintriples']??0;
|
||||
tSpinTriples = json['tspintriples'];
|
||||
tSpinMiniQuads = json['minitspinquads']??0;
|
||||
tSpinQuads = json['tspinquads'];
|
||||
tSpinPentas = json['tspinpentas']??0;
|
||||
allClears = json['allclear'];
|
||||
}
|
||||
|
||||
Clears operator + (Clears other){
|
||||
return Clears(
|
||||
singles: singles + other.singles,
|
||||
doubles: doubles + other.doubles,
|
||||
triples: triples + other.triples,
|
||||
quads: quads + other.quads,
|
||||
pentas: pentas + other.pentas,
|
||||
allClears: allClears + other.allClears,
|
||||
tSpinZeros: tSpinZeros + other.tSpinZeros,
|
||||
tSpinSingles: tSpinSingles + other.tSpinSingles,
|
||||
tSpinDoubles: tSpinDoubles + other.tSpinDoubles,
|
||||
tSpinTriples: tSpinTriples + other.tSpinTriples,
|
||||
tSpinPentas: tSpinPentas + other.tSpinPentas,
|
||||
tSpinQuads: tSpinQuads + other.tSpinQuads,
|
||||
tSpinMiniZeros: tSpinMiniZeros + other.tSpinMiniZeros,
|
||||
tSpinMiniSingles: tSpinMiniSingles + other.tSpinMiniSingles,
|
||||
tSpinMiniDoubles: tSpinMiniDoubles + other.tSpinMiniDoubles,
|
||||
tSpinMiniTriples: tSpinMiniTriples + other.tSpinMiniTriples,
|
||||
tSpinMiniQuads: tSpinMiniQuads + other.tSpinMiniQuads
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['singles'] = singles;
|
||||
data['doubles'] = doubles;
|
||||
data['triples'] = triples;
|
||||
data['quads'] = quads;
|
||||
data['pentas'] = pentas;
|
||||
data['realtspins'] = tSpinZeros;
|
||||
data['minitspins'] = tSpinMiniZeros;
|
||||
data['minitspinsingles'] = tSpinMiniSingles;
|
||||
data['tspinsingles'] = tSpinSingles;
|
||||
data['minitspindoubles'] = tSpinMiniDoubles;
|
||||
data['tspindoubles'] = tSpinDoubles;
|
||||
data['tspintriples'] = tSpinTriples;
|
||||
data['tspinquads'] = tSpinQuads;
|
||||
data['tspinpentas'] = tSpinPentas;
|
||||
data['allclear'] = allClears;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class Connections {
|
||||
Discord? discord;
|
||||
|
||||
Connections({this.discord});
|
||||
|
||||
Connections.fromJson(Map<String, dynamic> json) {
|
||||
discord = json['discord'] != null ? Discord.fromJson(json['discord']) : null;
|
||||
}
|
||||
@override
|
||||
bool operator ==(covariant Connections other) => discord == other.discord;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (discord != null) {
|
||||
data['discord'] = discord!.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Discord {
|
||||
late String id;
|
||||
late String username;
|
||||
|
||||
Discord({required this.id, required this.username});
|
||||
|
||||
Discord.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
username = json['username'];
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Discord other) => id == other.id;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['username'] = username;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:tetra_stats/data_objects/nerd_stats.dart';
|
||||
|
||||
class CutoffTetrio {
|
||||
late int pos;
|
||||
late double percentile;
|
||||
late double tr;
|
||||
late double targetTr;
|
||||
late double? apm;
|
||||
late double? pps;
|
||||
late double? vs;
|
||||
NerdStats? nerdStats;
|
||||
late int count;
|
||||
late double countPercentile;
|
||||
|
||||
CutoffTetrio({
|
||||
required this.pos,
|
||||
required this.percentile,
|
||||
required this.tr,
|
||||
required this.targetTr,
|
||||
required this.apm,
|
||||
required this.pps,
|
||||
required this.vs,
|
||||
required this.count,
|
||||
required this.countPercentile
|
||||
}){
|
||||
if (apm != null && pps != null && vs != null) nerdStats = NerdStats(apm!, pps!, vs!);
|
||||
}
|
||||
|
||||
CutoffTetrio.fromJson(Map<String, dynamic> json, int total){
|
||||
pos = json['pos'];
|
||||
percentile = json['percentile'].toDouble();
|
||||
tr = json['tr'].toDouble();
|
||||
targetTr = json['targettr'].toDouble();
|
||||
apm = json['apm']?.toDouble();
|
||||
pps = json['pps']?.toDouble();
|
||||
vs = json['vs']?.toDouble();
|
||||
count = json['count'];
|
||||
countPercentile = count / total;
|
||||
if (apm != null && pps != null && vs != null) nerdStats = NerdStats(apm!, pps!, vs!);
|
||||
}
|
||||
}
|
||||
|
||||
class CutoffsTetrio {
|
||||
late String id;
|
||||
late DateTime timestamp;
|
||||
late int total;
|
||||
Map<String, CutoffTetrio> data = {};
|
||||
|
||||
CutoffsTetrio.fromJson(Map<String, dynamic> json){
|
||||
id = json['s'];
|
||||
timestamp = DateTime.parse(json['t']);
|
||||
total = json['data']['total'];
|
||||
json['data'].remove("total");
|
||||
for (String rank in json['data'].keys){
|
||||
data[rank] = CutoffTetrio.fromJson(json['data'][rank], total);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class Distinguishment {
|
||||
late String type;
|
||||
String? detail;
|
||||
String? header;
|
||||
String? footer;
|
||||
|
||||
Distinguishment({required this.type, this.detail, this.header, this.footer});
|
||||
|
||||
Distinguishment.fromJson(Map<String, dynamic> json) {
|
||||
type = json['type'];
|
||||
detail = json['detail'];
|
||||
header = json['header'];
|
||||
footer = json['footer'];
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Distinguishment other) => type == other.type && detail == other.detail && header == other.header && footer == other.footer;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['type'] = type;
|
||||
data['detail'] = detail;
|
||||
data['header'] = header;
|
||||
data['footer'] = footer;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/est_tr.dart';
|
||||
import 'package:tetra_stats/data_objects/handling.dart';
|
||||
import 'package:tetra_stats/data_objects/nerd_stats.dart';
|
||||
import 'package:tetra_stats/data_objects/playstyle.dart';
|
||||
|
||||
class EndContextMulti {
|
||||
late String userId;
|
||||
late String username;
|
||||
late int naturalOrder;
|
||||
late int inputs;
|
||||
late int piecesPlaced;
|
||||
late Handling handling;
|
||||
late int points;
|
||||
late int wins;
|
||||
late double secondary;
|
||||
late List secondaryTracking;
|
||||
late double tertiary;
|
||||
late List tertiaryTracking;
|
||||
late double extra;
|
||||
late List extraTracking;
|
||||
late bool success;
|
||||
late NerdStats nerdStats;
|
||||
late List<NerdStats> nerdStatsTracking;
|
||||
late EstTr estTr;
|
||||
late List<EstTr> estTrTracking;
|
||||
late Playstyle playstyle;
|
||||
late List<Playstyle> playstyleTracking;
|
||||
|
||||
EndContextMulti(
|
||||
{required this.userId,
|
||||
required this.username,
|
||||
required this.naturalOrder,
|
||||
required this.inputs,
|
||||
required this.piecesPlaced,
|
||||
required this.handling,
|
||||
required this.points,
|
||||
required this.wins,
|
||||
required this.secondary,
|
||||
required this.secondaryTracking,
|
||||
required this.tertiary,
|
||||
required this.tertiaryTracking,
|
||||
required this.extra,
|
||||
required this.extraTracking,
|
||||
required this.success}){
|
||||
nerdStats = NerdStats(secondary, tertiary, extra);
|
||||
nerdStatsTracking = [for (int i = 0; i < secondaryTracking.length; i++) NerdStats(secondaryTracking[i], tertiaryTracking[i], extraTracking[i])];
|
||||
estTr = EstTr(secondary, tertiary, extra, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe);
|
||||
estTrTracking = [for (int i = 0; i < secondaryTracking.length; i++) EstTr(secondaryTracking[i], tertiaryTracking[i], extraTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].dss, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe)];
|
||||
playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank);
|
||||
playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)];
|
||||
}
|
||||
|
||||
EndContextMulti.fromJson(Map<String, dynamic> json) {
|
||||
userId = json['id'] ?? json['user']['_id'];
|
||||
username = json['username'] ?? json['user']['username'];
|
||||
handling = json['handling'] != null ? Handling.fromJson(json['handling']) : Handling(arr: -1, das: -1, sdf: -1, dcd: 0, cancel: true, safeLock: true);
|
||||
success = json['success'];
|
||||
inputs = json['inputs'] ?? -1;
|
||||
piecesPlaced = json['piecesplaced'] ?? -1;
|
||||
naturalOrder = json['naturalorder'];
|
||||
wins = json['wins'];
|
||||
points = json['points']['primary'];
|
||||
secondary = json['points']['secondary'].toDouble();
|
||||
tertiary = json['points']['tertiary'].toDouble();
|
||||
secondaryTracking = json['points']['secondaryAvgTracking'] != null ? json['points']['secondaryAvgTracking'].map((e) => e.toDouble()).toList() : [];
|
||||
tertiaryTracking = json['points']['tertiaryAvgTracking'] != null ? json['points']['tertiaryAvgTracking'].map((e) => e.toDouble()).toList() : [];
|
||||
extra = json['points']['extra']['vs'].toDouble();
|
||||
extraTracking = json['points']['extraAvgTracking'] != null ? json['points']['extraAvgTracking']['aggregatestats___vsscore'].map((e) => e.toDouble()).toList() : [];
|
||||
nerdStats = NerdStats(secondary, tertiary, extra);
|
||||
nerdStatsTracking = [for (int i = 0; i < secondaryTracking.length; i++) NerdStats(secondaryTracking[i], tertiaryTracking[i], extraTracking[i])];
|
||||
estTr = EstTr(secondary, tertiary, extra, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe);
|
||||
estTrTracking = [for (int i = 0; i < secondaryTracking.length; i++) EstTr(secondaryTracking[i], tertiaryTracking[i], extraTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].dss, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe)];
|
||||
playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank);
|
||||
playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)];
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator == (covariant EndContextMulti other){
|
||||
if (userId != other.userId) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['user'] = {'_id': userId, 'username': username};
|
||||
data['handling'] = handling.toJson();
|
||||
data['success'] = success;
|
||||
data['inputs'] = inputs;
|
||||
data['piecesplaced'] = piecesPlaced;
|
||||
data['naturalorder'] = naturalOrder;
|
||||
data['wins'] = wins;
|
||||
data['points'] = {'primary': points, 'secondary': secondary, 'tertiary':tertiary, 'extra': {'vs': extra}, 'secondaryAvgTracking': secondaryTracking, 'tertiaryAvgTracking': tertiaryTracking, 'extraAvgTracking': {'aggregatestats___vsscore': extraTracking}};
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
class EstTr {
|
||||
late double esttr;
|
||||
late double srarea;
|
||||
late double statrank;
|
||||
late double estglicko;
|
||||
|
||||
EstTr(double apm, double pps, double vs, double app, double dss, double dsp, double gbe) {
|
||||
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);
|
||||
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-estglicko
|
||||
)*pi
|
||||
)/sqrt(
|
||||
(
|
||||
(
|
||||
3*pow(ln10, 2)
|
||||
)*pow(60, 2)
|
||||
)+(
|
||||
2500*(
|
||||
(64*pow(pi,2))+(147*pow(ln10, 2))
|
||||
)
|
||||
)
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
class Finesse {
|
||||
late int combo;
|
||||
late int faults;
|
||||
late int perfectPieces;
|
||||
|
||||
Finesse({required this.combo, required this.faults, required this.perfectPieces});
|
||||
|
||||
Finesse.fromJson(Map<String, dynamic> json) {
|
||||
combo = json['combo'];
|
||||
faults = json['faults'];
|
||||
perfectPieces = json['perfectpieces'];
|
||||
}
|
||||
|
||||
Finesse operator + (Finesse other){
|
||||
return Finesse(combo: max(combo, other.combo), faults: faults + other.faults, perfectPieces: perfectPieces + other.perfectPieces);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['combo'] = combo;
|
||||
data['faults'] = faults;
|
||||
data['perfectpieces'] = perfectPieces;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class Handling {
|
||||
late num arr;
|
||||
late num das;
|
||||
late num sdf;
|
||||
late num dcd;
|
||||
late bool cancel;
|
||||
late bool safeLock;
|
||||
|
||||
Handling({required this.arr, required this.das, required this.sdf, required this.dcd, required this.cancel, required this.safeLock});
|
||||
|
||||
Handling.fromJson(Map<String, dynamic> json) {
|
||||
arr = json['arr'];
|
||||
das = json['das'];
|
||||
dcd = json['dcd'];
|
||||
sdf = json['sdf'];
|
||||
safeLock = json['safelock'];
|
||||
cancel = json['cancel'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['arr'] = arr.toDouble();
|
||||
data['das'] = das.toDouble();
|
||||
data['dcd'] = dcd.toDouble();
|
||||
data['sdf'] = sdf.toDouble();
|
||||
data['safelock'] = safeLock;
|
||||
data['cancel'] = cancel;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class LeaderboardPosition{
|
||||
int position;
|
||||
double percentage;
|
||||
|
||||
LeaderboardPosition(this.position, this.percentage);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:vector_math/vector_math.dart';
|
||||
|
||||
class NerdStats {
|
||||
late double app;
|
||||
late double vsapm;
|
||||
late double dss;
|
||||
late double dsp;
|
||||
late double appdsp;
|
||||
late double cheese;
|
||||
late double gbe;
|
||||
late double nyaapp;
|
||||
late double area;
|
||||
|
||||
NerdStats(double apm, double pps, double vs) {
|
||||
app = apm / (pps * 60);
|
||||
vsapm = vs / apm;
|
||||
dss = (vs / 100) - (apm / 60);
|
||||
dsp = ((vs / 100) - (apm / 60)) / pps;
|
||||
appdsp = app + dsp;
|
||||
cheese = (dsp * 150) + ((vsapm - 2) * 50) + (0.6 - app) * 125;
|
||||
gbe = app * dsp * 2;
|
||||
nyaapp = app - 5 * tan(radians((cheese / -30) + 1));
|
||||
area = apm * 1 + pps * 45 + vs * 0.444 + app * 185 + dss * 175 + dsp * 450 + gbe * 315;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/news_entry.dart';
|
||||
|
||||
class News{
|
||||
late String id;
|
||||
late List<NewsEntry> news;
|
||||
|
||||
News(this.id, this.news);
|
||||
|
||||
News.fromJson(Map<String, dynamic> json, String? userID){
|
||||
id = userID != null ? "user_$userID" : json['news'].first['stream'];
|
||||
news = [for (var entry in json['news']) NewsEntry.fromJson(entry)];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class NewsEntry {
|
||||
late String type;
|
||||
late Map<String, dynamic> data;
|
||||
late DateTime timestamp;
|
||||
|
||||
NewsEntry({required this.type, required this.data, required this.timestamp});
|
||||
|
||||
NewsEntry.fromJson(Map<String, dynamic> json){
|
||||
type = json["type"];
|
||||
data = json["data"];
|
||||
timestamp = DateTime.parse(json['ts']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/leaderboard_position.dart';
|
||||
|
||||
class PlayerLeaderboardPosition{
|
||||
late LeaderboardPosition? apm;
|
||||
late LeaderboardPosition? pps;
|
||||
late LeaderboardPosition? vs;
|
||||
late LeaderboardPosition? gamesPlayed;
|
||||
late LeaderboardPosition? gamesWon;
|
||||
late LeaderboardPosition? winrate;
|
||||
late LeaderboardPosition? glixare;
|
||||
late LeaderboardPosition? app;
|
||||
late LeaderboardPosition? vsapm;
|
||||
late LeaderboardPosition? dss;
|
||||
late LeaderboardPosition? dsp;
|
||||
late LeaderboardPosition? appdsp;
|
||||
late LeaderboardPosition? cheese;
|
||||
late LeaderboardPosition? gbe;
|
||||
late LeaderboardPosition? nyaapp;
|
||||
late LeaderboardPosition? area;
|
||||
late LeaderboardPosition? estTr;
|
||||
late LeaderboardPosition? accOfEst;
|
||||
|
||||
PlayerLeaderboardPosition({
|
||||
required this.apm,
|
||||
required this.pps,
|
||||
required this.vs,
|
||||
required this.gamesPlayed,
|
||||
required this.gamesWon,
|
||||
required this.winrate,
|
||||
required this.glixare,
|
||||
required this.app,
|
||||
required this.vsapm,
|
||||
required this.dss,
|
||||
required this.dsp,
|
||||
required this.appdsp,
|
||||
required this.cheese,
|
||||
required this.gbe,
|
||||
required this.nyaapp,
|
||||
required this.area,
|
||||
required this.estTr,
|
||||
required this.accOfEst
|
||||
});
|
||||
|
||||
PlayerLeaderboardPosition.fromSearchResults(List<LeaderboardPosition?> results){
|
||||
apm = results[0];
|
||||
pps = results[1];
|
||||
vs = results[2];
|
||||
gamesPlayed = results[3];
|
||||
gamesWon = results[4];
|
||||
winrate = results[5];
|
||||
glixare = results[6];
|
||||
app = results[7];
|
||||
vsapm = results[8];
|
||||
dss = results[9];
|
||||
dsp = results[10];
|
||||
appdsp = results[11];
|
||||
cheese = results[12];
|
||||
gbe = results[13];
|
||||
nyaapp = results[14];
|
||||
area = results[15];
|
||||
estTr = results[16];
|
||||
accOfEst = results[17];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
class Playstyle {
|
||||
late double opener;
|
||||
late double plonk;
|
||||
late double stride;
|
||||
late double infds;
|
||||
|
||||
Playstyle(double apm, double pps, double app, double vsapm, double dsp, double gbe, double srarea, double statrank) {
|
||||
double nmapm = ((apm / srarea) / ((0.069 * pow(1.0017, (pow(statrank, 5) / 4700))) + statrank / 360)) - 1;
|
||||
double nmpps = ((pps / srarea) / (0.0084264 * pow(2.14, (-2 * (statrank / 2.7 + 1.03))) - statrank / 5750 + 0.0067)) - 1;
|
||||
//double nmvs = ((vs / srarea) / (0.1333 * pow(1.0021, ((pow(statrank, 7) * (statrank / 16.5)) / 1400000)) + statrank / 133)) - 1;
|
||||
double nmapp = (app / (0.1368803292 * pow(1.0024, (pow(statrank, 5) / 2800)) + statrank / 54)) - 1;
|
||||
//double nmdss = (dss / (0.01436466667 * pow(4.1, ((statrank - 9.6) / 2.9)) + statrank / 140 + 0.01)) - 1;
|
||||
double nmdsp = (dsp / (0.02136327583 * pow(14, ((statrank - 14.75) / 3.9)) + statrank / 152 + 0.022)) - 1;
|
||||
double nmgbe = (gbe / (statrank / 350 + 0.005948424455 * pow(3.8, ((statrank - 6.1) / 4)) + 0.006)) - 1;
|
||||
double nmvsapm = (vsapm / (-pow(((statrank - 16) / 36), 2) + 2.133)) - 1;
|
||||
opener = ((nmapm + nmpps * 0.75 + nmvsapm * -10 + nmapp * 0.75 + nmdsp * -0.25) / 3.5) + 0.5;
|
||||
plonk = ((nmgbe + nmapp + nmdsp * 0.75 + nmpps * -1) / 2.73) + 0.5;
|
||||
stride = ((nmapm * -0.25 + nmpps + nmapp * -2 + nmdsp * -0.5) * 0.79) + 0.5;
|
||||
infds = ((nmdsp + nmapp * -0.75 + nmapm * 0.5 + nmvsapm * 1.5 + nmpps * 0.5) * 0.9) + 0.5;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class RecordExtras{
|
||||
|
||||
}
|
||||
|
||||
class ZenithExtras extends RecordExtras{
|
||||
List<String> mods = [];
|
||||
|
||||
ZenithExtras.fromJson(Map<String, dynamic> json){
|
||||
for (var mod in json["mods"]) {
|
||||
mods.add(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SmallLeague{
|
||||
late double glicko;
|
||||
late double rd;
|
||||
late double tr;
|
||||
late String rank;
|
||||
late int placement;
|
||||
|
||||
SmallLeague(this.glicko, this.rd, this.tr, this.rank, this.placement);
|
||||
|
||||
SmallLeague.fromJson(Map<String, dynamic> json){
|
||||
glicko = json['glicko'];
|
||||
rd = json['rd'];
|
||||
tr = json['tr'];
|
||||
rank = json['rank'];
|
||||
placement = json['placement']??-1;
|
||||
}
|
||||
}
|
||||
|
||||
class LeagueExtras extends RecordExtras{
|
||||
late String result;
|
||||
Map<String, List<SmallLeague?>> league = {};
|
||||
|
||||
LeagueExtras.fromJson(Map<String, dynamic> json){
|
||||
result = json['result'];
|
||||
for (String userID in json['league'].keys){
|
||||
league[userID] = [json['league'][userID][0] != null ? SmallLeague.fromJson(json['league'][userID][0]) : null, json['league'][userID][1] != null ? SmallLeague.fromJson(json['league'][userID][1]) : null];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
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;
|
||||
String? revolution;
|
||||
late DateTime timestamp;
|
||||
late ResultsStats stats;
|
||||
late int rank;
|
||||
late int countryRank;
|
||||
late AggregateStats aggregateStats;
|
||||
late RecordExtras extras;
|
||||
late Prisecter prisecter;
|
||||
|
||||
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<String, dynamic> json, int ran, int cran) {
|
||||
ownId = json['_id'];
|
||||
gamemode = json['gamemode'];
|
||||
stats = ResultsStats.fromJson(json['results']['stats']);
|
||||
replayId = json['replayid'];
|
||||
timestamp = DateTime.parse(json['ts']);
|
||||
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']);
|
||||
revolution = json["revolution"];
|
||||
var ex = json['extras'] as Map<String, dynamic>;
|
||||
switch (ex.keys.firstOrNull){
|
||||
case "zenith":
|
||||
extras = ZenithExtras.fromJson(json['extras']['zenith']);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['_id'] = ownId;
|
||||
data['results']['stats'] = stats.toJson();
|
||||
data['ismulti'] = false;
|
||||
data['replayid'] = replayId;
|
||||
data['ts'] = timestamp;
|
||||
data['user_id'] = userId;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/clears.dart';
|
||||
import 'package:tetra_stats/data_objects/finesse.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
|
||||
import 'package:tetra_stats/data_objects/zenith_results.dart';
|
||||
|
||||
class ResultsStats {
|
||||
late int topBtB;
|
||||
late int topCombo;
|
||||
late int holds;
|
||||
late int inputs;
|
||||
late int level;
|
||||
late int piecesPlaced;
|
||||
late int lines;
|
||||
late int score;
|
||||
double? seed;
|
||||
late Duration finalTime;
|
||||
late int tSpins;
|
||||
late Clears clears;
|
||||
late Garbage garbage;
|
||||
late int kills;
|
||||
Finesse? finesse;
|
||||
ZenithResults? zenith;
|
||||
|
||||
double get pps => piecesPlaced / (finalTime.inMicroseconds / 1000000);
|
||||
double get kpp => inputs / piecesPlaced;
|
||||
double get spp => score / piecesPlaced;
|
||||
double get kps => inputs / (finalTime.inMicroseconds / 1000000);
|
||||
double get finessePercentage => finesse != null ? finesse!.perfectPieces / piecesPlaced : 0;
|
||||
double get cps => zenith != null ? zenith!.avgrankpts / (finalTime.inMilliseconds / 1000 * 60) : 0;
|
||||
|
||||
ResultsStats(
|
||||
{
|
||||
required this.topBtB,
|
||||
required this.topCombo,
|
||||
required this.holds,
|
||||
required this.inputs,
|
||||
required this.level,
|
||||
required this.piecesPlaced,
|
||||
required this.lines,
|
||||
required this.score,
|
||||
required this.seed,
|
||||
required this.finalTime,
|
||||
required this.tSpins,
|
||||
required this.clears,
|
||||
required this.finesse});
|
||||
|
||||
ResultsStats.fromJson(Map<String, dynamic> json) {
|
||||
seed = json['seed']?.toDouble();
|
||||
lines = json['lines'];
|
||||
inputs = json['inputs'];
|
||||
holds = json['holds'] ?? 0;
|
||||
finalTime = Duration(microseconds: (json['finaltime'].toDouble() * 1000).floor());
|
||||
score = json['score'];
|
||||
level = json['level'];
|
||||
topCombo = json['topcombo'];
|
||||
topBtB = json['topbtb'];
|
||||
tSpins = json['tspins'];
|
||||
piecesPlaced = json['piecesplaced'];
|
||||
clears = Clears.fromJson(json['clears']);
|
||||
garbage = Garbage.fromJson(json['garbage']);
|
||||
kills = json['kills'];
|
||||
if (json.containsKey("finesse")) finesse = Finesse.fromJson(json['finesse']);
|
||||
if (json.containsKey("zenith")) zenith = ZenithResults.fromJson(json['zenith']);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['seed'] = seed;
|
||||
data['lines'] = lines;
|
||||
data['inputs'] = inputs;
|
||||
data['holds'] = holds;
|
||||
data['score'] = score;
|
||||
data['level'] = level;
|
||||
data['topcombo'] = topCombo;
|
||||
data['topbtb'] = topBtB;
|
||||
data['tspins'] = tSpins;
|
||||
data['piecesplaced'] = piecesPlaced;
|
||||
data['clears'] = clears.toJson();
|
||||
if (finesse != null) data['finesse'] = finesse!.toJson();
|
||||
data['finalTime'] = finalTime;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/record_single.dart';
|
||||
|
||||
class SingleplayerStream{
|
||||
late String userId;
|
||||
late String type;
|
||||
late List<RecordSingle> records;
|
||||
|
||||
SingleplayerStream({required this.userId, required this.records, required this.type});
|
||||
|
||||
SingleplayerStream.fromJson(List<dynamic> json, String userID, String tp) {
|
||||
userId = userID;
|
||||
type = tp;
|
||||
records = [];
|
||||
for (var value in json) {records.add(RecordSingle.fromJson(value, -1, -1));}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/achievement.dart';
|
||||
import 'package:tetra_stats/data_objects/record_single.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_zen.dart';
|
||||
|
||||
class Summaries {
|
||||
late String id;
|
||||
RecordSingle? sprint;
|
||||
RecordSingle? blitz;
|
||||
RecordSingle? zenith;
|
||||
RecordSingle? zenithCareerBest; // leaderboard best, not overall
|
||||
RecordSingle? zenithEx;
|
||||
RecordSingle? zenithExCareerBest; // leaderboard best, not overall
|
||||
late List<Achievement> achievements;
|
||||
late TetraLeague league;
|
||||
Map<int, TetraLeague> pastLeague = {};
|
||||
late TetrioZen zen;
|
||||
|
||||
Summaries(this.id, this.league, this.zen);
|
||||
|
||||
Summaries.fromJson(Map<String, dynamic> json, String i) {
|
||||
id = i;
|
||||
if (json['40l']['record'] != null)
|
||||
sprint = RecordSingle.fromJson(json['40l']['record'], json['40l']['rank'],
|
||||
json['40l']['rank_local']);
|
||||
if (json['blitz']['record'] != null)
|
||||
blitz = RecordSingle.fromJson(json['blitz']['record'],
|
||||
json['blitz']['rank'], json['blitz']['rank_local']);
|
||||
if (json['zenith']['record'] != null)
|
||||
zenith = RecordSingle.fromJson(json['zenith']['record'],
|
||||
json['zenith']['rank'], json['zenith']['rank_local']);
|
||||
if (json['zenith']['best']['record'] != null)
|
||||
zenithCareerBest = RecordSingle.fromJson(
|
||||
json['zenith']['best']['record'], json['zenith']['best']['rank'], -1);
|
||||
if (json['zenithex']['record'] != null)
|
||||
zenithEx = RecordSingle.fromJson(json['zenithex']['record'],
|
||||
json['zenithex']['rank'], json['zenithex']['rank_local']);
|
||||
if (json['zenithex']['best']['record'] != null)
|
||||
zenithExCareerBest = RecordSingle.fromJson(
|
||||
json['zenithex']['best']['record'],
|
||||
json['zenithex']['best']['rank'],
|
||||
-1);
|
||||
achievements = [
|
||||
for (var achievement in json['achievements'])
|
||||
Achievement.fromJson(achievement)
|
||||
];
|
||||
league =
|
||||
TetraLeague.fromJson(json['league'], DateTime.now(), currentSeason, i);
|
||||
if (json['league']['past'] != null && json['league']['past'].isNotEmpty)
|
||||
for (var key in json['league']['past'].keys) {
|
||||
pastLeague[int.parse(key)] = TetraLeague.fromJson(
|
||||
json['league']['past'][key],
|
||||
null,
|
||||
int.parse(json['league']['past'][key]['season']),
|
||||
i);
|
||||
}
|
||||
zen = TetrioZen.fromJson(json['zen']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
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_player_from_leaderboard.dart';
|
||||
|
||||
class TetraLeague {
|
||||
late String id;
|
||||
late DateTime timestamp;
|
||||
late int gamesPlayed;
|
||||
late int gamesWon;
|
||||
late String bestRank;
|
||||
late bool decaying;
|
||||
late double tr;
|
||||
late double gxe;
|
||||
late String rank;
|
||||
double? glicko;
|
||||
double? rd;
|
||||
late String percentileRank;
|
||||
late double percentile;
|
||||
late int standing;
|
||||
late int standingLocal;
|
||||
String? nextRank;
|
||||
late int nextAt;
|
||||
String? prevRank;
|
||||
late int prevAt;
|
||||
double? apm;
|
||||
double? pps;
|
||||
double? vs;
|
||||
NerdStats? nerdStats;
|
||||
EstTr? estTr;
|
||||
Playstyle? playstyle;
|
||||
late int season;
|
||||
|
||||
TetraLeague(
|
||||
{required this.id,
|
||||
required this.timestamp,
|
||||
required this.gamesPlayed,
|
||||
required this.gamesWon,
|
||||
required this.bestRank,
|
||||
required this.decaying,
|
||||
required this.tr,
|
||||
required this.gxe,
|
||||
required this.rank,
|
||||
this.glicko,
|
||||
this.rd,
|
||||
required this.percentileRank,
|
||||
required this.percentile,
|
||||
required this.standing,
|
||||
required this.standingLocal,
|
||||
this.nextRank,
|
||||
required this.nextAt,
|
||||
this.prevRank,
|
||||
required this.prevAt,
|
||||
this.apm,
|
||||
this.pps,
|
||||
this.vs,
|
||||
required this.season}) {
|
||||
nerdStats = (apm != null && pps != null && vs != null)
|
||||
? NerdStats(apm!, pps!, vs!)
|
||||
: null;
|
||||
estTr = (nerdStats != null)
|
||||
? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp,
|
||||
nerdStats!.gbe)
|
||||
: null;
|
||||
playstyle = (nerdStats != null)
|
||||
? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm,
|
||||
nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank)
|
||||
: null;
|
||||
}
|
||||
|
||||
double get winrate => gamesWon / gamesPlayed;
|
||||
double get s1tr => gxe * 250;
|
||||
|
||||
TetraLeague.fromJson(
|
||||
Map<String, dynamic> json, DateTime? ts, int s, String i) {
|
||||
timestamp = ts != null ? ts : seasonEnds[s - 1];
|
||||
season = s;
|
||||
id = i;
|
||||
gamesPlayed = json['gamesplayed'] ?? 0;
|
||||
gamesWon = json['gameswon'] ?? 0;
|
||||
tr = json['tr'] != null
|
||||
? json['tr'].toDouble()
|
||||
: json['rating'] != null
|
||||
? json['rating'].toDouble()
|
||||
: -1;
|
||||
glicko = json['glicko']?.toDouble();
|
||||
rd = json['rd'] != null ? json['rd']!.toDouble() : noTrRd;
|
||||
gxe = json['gxe'] != null ? json['gxe'].toDouble() : -1;
|
||||
rank = json['rank'] != null ? json['rank']!.toString() : 'z';
|
||||
bestRank = json['bestrank'] != null ? json['bestrank']!.toString() : 'z';
|
||||
apm = json['apm']?.toDouble();
|
||||
pps = json['pps']?.toDouble();
|
||||
vs = json['vs']?.toDouble();
|
||||
decaying = switch (json['decaying'].runtimeType) {
|
||||
int => json['decaying'] == 1,
|
||||
bool => json['decaying'],
|
||||
_ => false
|
||||
};
|
||||
standing = json['standing'] ?? json['placement'] ?? -1;
|
||||
percentile = json['percentile'] != null
|
||||
? json['percentile'].toDouble()
|
||||
: rankCutoffs[rank];
|
||||
standingLocal = json['standing_local'] ?? -1;
|
||||
prevRank = json['prev_rank'];
|
||||
prevAt = json['prev_at'] ?? -1;
|
||||
nextRank = json['next_rank'];
|
||||
nextAt = json['next_at'] ?? -1;
|
||||
percentileRank = json['percentile_rank'] ?? rank;
|
||||
nerdStats = (apm != null && pps != null && vs != null)
|
||||
? NerdStats(apm!, pps!, vs!)
|
||||
: null;
|
||||
estTr = (nerdStats != null)
|
||||
? EstTr(apm!, pps!, vs!, nerdStats!.app, nerdStats!.dss, nerdStats!.dsp,
|
||||
nerdStats!.gbe)
|
||||
: null;
|
||||
playstyle = (nerdStats != null)
|
||||
? Playstyle(apm!, pps!, nerdStats!.app, nerdStats!.vsapm,
|
||||
nerdStats!.dsp, nerdStats!.gbe, estTr!.srarea, estTr!.statrank)
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant TetraLeague other) =>
|
||||
gamesPlayed == other.gamesPlayed && rd == other.rd;
|
||||
|
||||
bool lessStrictCheck(covariant TetraLeague other) =>
|
||||
gamesPlayed == other.gamesPlayed && glicko == other.glicko;
|
||||
|
||||
double? get esttracc => (estTr != null) ? estTr!.esttr - tr : null;
|
||||
|
||||
TetrioPlayerFromLeaderboard convertToPlayerFromLeaderboard(String id) =>
|
||||
TetrioPlayerFromLeaderboard(
|
||||
id,
|
||||
"",
|
||||
"user",
|
||||
-1,
|
||||
null,
|
||||
timestamp,
|
||||
gamesPlayed,
|
||||
gamesWon,
|
||||
tr,
|
||||
gxe,
|
||||
glicko ?? 0,
|
||||
rd ?? noTrRd,
|
||||
rank,
|
||||
bestRank,
|
||||
apm ?? 0,
|
||||
pps ?? 0,
|
||||
vs ?? 0,
|
||||
decaying,
|
||||
-1,
|
||||
-1,
|
||||
Duration(seconds: -1),
|
||||
-1);
|
||||
|
||||
num? getStatByEnum(Stats stat){
|
||||
switch (stat) {
|
||||
case Stats.tr:
|
||||
return tr;
|
||||
case Stats.glicko:
|
||||
return glicko;
|
||||
case Stats.gxe:
|
||||
return gxe;
|
||||
case Stats.s1tr:
|
||||
return s1tr;
|
||||
case Stats.rd:
|
||||
return rd;
|
||||
case Stats.gp:
|
||||
return gamesPlayed;
|
||||
case Stats.gw:
|
||||
return gamesWon;
|
||||
case Stats.wr:
|
||||
return winrate*100;
|
||||
case Stats.apm:
|
||||
return apm;
|
||||
case Stats.pps:
|
||||
return pps;
|
||||
case Stats.vs:
|
||||
return vs;
|
||||
case Stats.app:
|
||||
return nerdStats?.app;
|
||||
case Stats.dss:
|
||||
return nerdStats?.dss;
|
||||
case Stats.dsp:
|
||||
return nerdStats?.dsp;
|
||||
case Stats.appdsp:
|
||||
return nerdStats?.appdsp;
|
||||
case Stats.vsapm:
|
||||
return nerdStats?.vsapm;
|
||||
case Stats.cheese:
|
||||
return nerdStats?.cheese;
|
||||
case Stats.gbe:
|
||||
return nerdStats?.gbe;
|
||||
case Stats.nyaapp:
|
||||
return nerdStats?.nyaapp;
|
||||
case Stats.area:
|
||||
return nerdStats?.area;
|
||||
case Stats.eTR:
|
||||
return estTr?.esttr;
|
||||
case Stats.acceTR:
|
||||
return esttracc;
|
||||
case Stats.acceTRabs:
|
||||
return esttracc?.abs();
|
||||
case Stats.opener:
|
||||
return playstyle?.opener;
|
||||
case Stats.plonk:
|
||||
return playstyle?.plonk;
|
||||
case Stats.infDS:
|
||||
return playstyle?.infds;
|
||||
case Stats.stride:
|
||||
return playstyle?.stride;
|
||||
case Stats.stridemMinusPlonk:
|
||||
return (playstyle?.stride??0.00) - (playstyle?.plonk??0.00);
|
||||
case Stats.openerMinusInfDS:
|
||||
return (playstyle?.opener??0.00) - (playstyle?.infds??0.00);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id + timestamp.millisecondsSinceEpoch.toRadixString(16);
|
||||
if (gamesPlayed > 0) data['gamesplayed'] = gamesPlayed;
|
||||
if (gamesWon > 0) data['gameswon'] = gamesWon;
|
||||
if (tr >= 0) data['tr'] = tr;
|
||||
if (glicko != null) data['glicko'] = glicko;
|
||||
if (gxe != -1) data['gxe'] = gxe;
|
||||
if (rd != null && rd != noTrRd) data['rd'] = rd;
|
||||
if (rank != 'z') data['rank'] = rank;
|
||||
if (bestRank != 'z') data['bestrank'] = bestRank;
|
||||
if (apm != null) data['apm'] = apm;
|
||||
if (pps != null) data['pps'] = pps;
|
||||
if (vs != null) data['vs'] = vs;
|
||||
if (decaying) data['decaying'] = decaying ? 1 : 0;
|
||||
if (standing >= 0) data['standing'] = standing;
|
||||
data['percentile'] = percentile;
|
||||
if (standingLocal >= 0) data['standing_local'] = standingLocal;
|
||||
if (prevRank != null) data['prev_rank'] = prevRank;
|
||||
if (prevAt >= 0) data['prev_at'] = prevAt;
|
||||
if (nextRank != null) data['next_rank'] = nextRank;
|
||||
if (nextAt >= 0) data['next_at'] = nextAt;
|
||||
data['percentile_rank'] = percentileRank;
|
||||
data['season'] = season;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/end_context_multi.dart';
|
||||
|
||||
class TetraLeagueAlphaRecord{
|
||||
late String replayId;
|
||||
late String ownId;
|
||||
late DateTime timestamp;
|
||||
late bool replayAvalable;
|
||||
late List<EndContextMulti> endContext;
|
||||
|
||||
TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext, required this.replayAvalable});
|
||||
|
||||
TetraLeagueAlphaRecord.fromJson(Map<String, dynamic> json) {
|
||||
endContext = [EndContextMulti.fromJson(json['endcontext'][0]), EndContextMulti.fromJson(json['endcontext'][1])];
|
||||
replayId = json['replayid'];
|
||||
ownId = json['_id']??replayId;
|
||||
timestamp = DateTime.parse(json['ts']);
|
||||
replayAvalable = ownId != replayId;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['_id'] = ownId;
|
||||
data['endcontext'][0] = endContext[0].toJson();
|
||||
data['endcontext'][1] = endContext[1].toJson();
|
||||
data['replayid'] = replayId;
|
||||
data['ts'] = timestamp;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant TetraLeagueAlphaRecord other) => (ownId == other.ownId) || (replayId == other.replayId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "TetraLeagueAlphaRecord: ${endContext.first.userId} vs ${endContext.last.userId}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart';
|
||||
|
||||
class TetraLeagueAlphaStream{
|
||||
late String userId;
|
||||
late List<TetraLeagueAlphaRecord> records;
|
||||
|
||||
TetraLeagueAlphaStream({required this.userId, required this.records});
|
||||
|
||||
TetraLeagueAlphaStream.fromJson(List<dynamic> json, String userID) {
|
||||
userId = userID;
|
||||
records = [];
|
||||
for (var value in json) {records.add(TetraLeagueAlphaRecord.fromJson(value));}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/beta_league_leaderboard_entry.dart';
|
||||
import 'package:tetra_stats/data_objects/beta_league_results.dart';
|
||||
import 'package:tetra_stats/data_objects/beta_league_round.dart';
|
||||
import 'package:tetra_stats/data_objects/beta_league_stats.dart';
|
||||
import 'package:tetra_stats/data_objects/beta_record.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart';
|
||||
|
||||
class TetraLeagueBetaStream{
|
||||
late String id;
|
||||
List<BetaRecord> records = [];
|
||||
|
||||
TetraLeagueBetaStream({required this.id, required this.records});
|
||||
|
||||
TetraLeagueBetaStream.fromJson(List<dynamic> json, String userID) {
|
||||
id = userID;
|
||||
for (var entry in json) {
|
||||
records.add(BetaRecord.fromJson(entry));
|
||||
}
|
||||
}
|
||||
|
||||
addFromAlphaStream(List<TetraLeagueAlphaRecord> r){
|
||||
for (var entry in r) {
|
||||
records.add(
|
||||
BetaRecord(
|
||||
id: entry.ownId,
|
||||
replayID: entry.replayId,
|
||||
ts: entry.timestamp,
|
||||
enemyID: entry.endContext[1].userId,
|
||||
enemyUsername: entry.endContext[1].username,
|
||||
gamemode: "oldleague",
|
||||
results: BetaLeagueResults(
|
||||
leaderboard: [
|
||||
BetaLeagueLeaderboardEntry(
|
||||
id: entry.endContext[0].userId,
|
||||
username: entry.endContext[0].username,
|
||||
naturalorder: entry.endContext[0].naturalOrder,
|
||||
wins: entry.endContext[0].points,
|
||||
stats: BetaLeagueStats(
|
||||
apm: entry.endContext[0].secondary,
|
||||
pps: entry.endContext[0].tertiary,
|
||||
vs: entry.endContext[0].extra,
|
||||
garbageSent: -1,
|
||||
garbageReceived: -1,
|
||||
kills: entry.endContext[0].points,
|
||||
altitude: 0.0,
|
||||
rank: -1
|
||||
)
|
||||
),
|
||||
BetaLeagueLeaderboardEntry(
|
||||
id: entry.endContext[1].userId,
|
||||
username: entry.endContext[1].username,
|
||||
naturalorder: entry.endContext[1].naturalOrder,
|
||||
wins: entry.endContext[1].points,
|
||||
stats: BetaLeagueStats(
|
||||
apm: entry.endContext[1].secondary,
|
||||
pps: entry.endContext[1].tertiary,
|
||||
vs: entry.endContext[1].extra,
|
||||
garbageSent: -1,
|
||||
garbageReceived: -1,
|
||||
kills: entry.endContext[1].points,
|
||||
altitude: 0.0,
|
||||
rank: -1
|
||||
)
|
||||
)
|
||||
],
|
||||
rounds: [
|
||||
for (int i=0; i<entry.endContext[0].secondaryTracking.length; i++)
|
||||
[BetaLeagueRound(
|
||||
id: entry.endContext[0].userId,
|
||||
username: entry.endContext[0].username,
|
||||
naturalorder: entry.endContext[0].naturalOrder,
|
||||
active: false,
|
||||
alive: false,
|
||||
lifetime: const Duration(milliseconds: -1),
|
||||
stats: BetaLeagueStats(
|
||||
apm: entry.endContext[0].secondaryTracking[i],
|
||||
pps: entry.endContext[0].tertiaryTracking[i],
|
||||
vs: entry.endContext[0].extraTracking[i],
|
||||
garbageSent: -1,
|
||||
garbageReceived: -1,
|
||||
kills: 0,
|
||||
altitude: 0.0,
|
||||
rank: -1
|
||||
)
|
||||
),BetaLeagueRound(
|
||||
id: entry.endContext[1].userId,
|
||||
username: entry.endContext[1].username,
|
||||
naturalorder: entry.endContext[1].naturalOrder,
|
||||
active: false,
|
||||
alive: false,
|
||||
lifetime: const Duration(milliseconds: -1),
|
||||
stats: BetaLeagueStats(
|
||||
apm: entry.endContext[1].secondaryTracking[i],
|
||||
pps: entry.endContext[1].tertiaryTracking[i],
|
||||
vs: entry.endContext[1].extraTracking[i],
|
||||
garbageSent: -1,
|
||||
garbageReceived: -1,
|
||||
kills: 0,
|
||||
altitude: 0.0,
|
||||
rank: -1
|
||||
)
|
||||
)]
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,332 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
const int currentSeason = 2;
|
||||
final DateTime sprintAndBlitzRelevance = DateTime(2024, 8, 25);
|
||||
const double noTrRd = 60.9;
|
||||
const double apmWeight = 1;
|
||||
const double ppsWeight = 45;
|
||||
const double vsWeight = 0.444;
|
||||
const double appWeight = 185;
|
||||
const double dssWeight = 175;
|
||||
const double dspWeight = 450;
|
||||
const double appdspWeight = 140;
|
||||
const double vsapmWeight = 60;
|
||||
const double cheeseWeight = 1.25;
|
||||
const double gbeWeight = 315;
|
||||
|
||||
const Map<int, double> xpTableScuffed = { // level: xp required
|
||||
05000: 67009018.4885772,
|
||||
10000: 763653437.386,
|
||||
15000: 2337651144.54149,
|
||||
20000: 4572735210.50902,
|
||||
25000: 7376166347.04745,
|
||||
30000: 10693620096.2168,
|
||||
40000: 18728882739.482,
|
||||
50000: 28468683855.2853
|
||||
};
|
||||
|
||||
const List<String> ranks = [
|
||||
"d",
|
||||
"d+",
|
||||
"c-",
|
||||
"c",
|
||||
"c+",
|
||||
"b-",
|
||||
"b",
|
||||
"b+",
|
||||
"a-",
|
||||
"a",
|
||||
"a+",
|
||||
"s-",
|
||||
"s",
|
||||
"s+",
|
||||
"ss",
|
||||
"u",
|
||||
"x",
|
||||
"x+"
|
||||
];
|
||||
const List<String> ranks2 = [
|
||||
"top1",
|
||||
"x+",
|
||||
"x",
|
||||
"u",
|
||||
"ss",
|
||||
"s+",
|
||||
"s",
|
||||
"s-",
|
||||
"a+",
|
||||
"a",
|
||||
"a-",
|
||||
"b+",
|
||||
"b",
|
||||
"b-",
|
||||
"c+",
|
||||
"c",
|
||||
"c-",
|
||||
"d+",
|
||||
"d"
|
||||
];
|
||||
const Map<String, double> rankCutoffs = {
|
||||
"x+": 0.002,
|
||||
"x": 0.01,
|
||||
"u": 0.05,
|
||||
"ss": 0.11,
|
||||
"s+": 0.17,
|
||||
"s": 0.23,
|
||||
"s-": 0.3,
|
||||
"a+": 0.38,
|
||||
"a": 0.46,
|
||||
"a-": 0.54,
|
||||
"b+": 0.62,
|
||||
"b": 0.7,
|
||||
"b-": 0.78,
|
||||
"c+": 0.84,
|
||||
"c": 0.9,
|
||||
"c-": 0.95,
|
||||
"d+": 0.975,
|
||||
"d": 1,
|
||||
"z": -1,
|
||||
"": 0.5
|
||||
};
|
||||
const Map<String, double> rankTargets = {
|
||||
"x+": 24000.00,
|
||||
"x": 22500.00,
|
||||
"u": 20000.00,
|
||||
"ss": 18000.00,
|
||||
"s+": 16500.00,
|
||||
"s": 15200.00,
|
||||
"s-": 13800.00,
|
||||
"a+": 12000.00,
|
||||
"a": 10500.00,
|
||||
"a-": 9000.00,
|
||||
"b+": 7400.00,
|
||||
"b": 5700.00,
|
||||
"b-": 4200.00,
|
||||
"c+": 3000.00,
|
||||
"c": 2000.00,
|
||||
"c-": 1300.00,
|
||||
"d+": 800.00,
|
||||
"d": 0.00,
|
||||
};
|
||||
|
||||
// DateTime seasonStart = DateTime.utc(2024, 08, 16, 18);
|
||||
//DateTime seasonEnd = DateTime.utc(2024, 07, 26, 15);
|
||||
enum Stats {
|
||||
tr,
|
||||
glicko,
|
||||
gxe,
|
||||
s1tr,
|
||||
rd,
|
||||
gp,
|
||||
gw,
|
||||
wr,
|
||||
apm,
|
||||
pps,
|
||||
vs,
|
||||
app,
|
||||
dss,
|
||||
dsp,
|
||||
appdsp,
|
||||
vsapm,
|
||||
cheese,
|
||||
gbe,
|
||||
nyaapp,
|
||||
area,
|
||||
eTR,
|
||||
acceTR,
|
||||
acceTRabs,
|
||||
opener,
|
||||
plonk,
|
||||
infDS,
|
||||
stride,
|
||||
stridemMinusPlonk,
|
||||
openerMinusInfDS
|
||||
}
|
||||
|
||||
const Map<Stats, String> chartsShortTitles = {
|
||||
Stats.tr: "TR",
|
||||
Stats.gxe: "Glixare",
|
||||
Stats.s1tr: "S1 TR",
|
||||
Stats.glicko: "Glicko",
|
||||
Stats.rd: "RD",
|
||||
Stats.gp: "GP",
|
||||
Stats.gw: "GW",
|
||||
Stats.wr: "WR%",
|
||||
Stats.apm: "APM",
|
||||
Stats.pps: "PPS",
|
||||
Stats.vs: "VS",
|
||||
Stats.app: "APP",
|
||||
Stats.dss: "DS/S",
|
||||
Stats.dsp: "DS/P",
|
||||
Stats.appdsp: "APP + DS/P",
|
||||
Stats.vsapm: "VS/APM",
|
||||
Stats.cheese: "Cheese",
|
||||
Stats.gbe: "GbE",
|
||||
Stats.nyaapp: "wAPP",
|
||||
Stats.area: "Area",
|
||||
Stats.eTR: "eTR",
|
||||
Stats.acceTR: "±eTR",
|
||||
Stats.acceTRabs: "±eTR absolute",
|
||||
Stats.opener: "Opener",
|
||||
Stats.plonk: "Plonk",
|
||||
Stats.infDS: "Inf. DS",
|
||||
Stats.stride: "Stride",
|
||||
Stats.stridemMinusPlonk: "Stride - Plonk",
|
||||
Stats.openerMinusInfDS: "Opener - Inf. DS"
|
||||
};
|
||||
|
||||
const Map<String, Color> rankColors = {
|
||||
// thanks osk for const rankColors at https://ch.tetr.io/res/js/base.js:458
|
||||
'x+': Color(0xFF643C8D),
|
||||
'x': Color(0xFFFF45FF),
|
||||
'u': Color(0xFFFF3813),
|
||||
'ss': Color(0xFFDB8B1F),
|
||||
's+': Color(0xFFD8AF0E),
|
||||
's': Color(0xFFE0A71B),
|
||||
's-': Color(0xFFB2972B),
|
||||
'a+': Color(0xFF1FA834),
|
||||
'a': Color(0xFF46AD51),
|
||||
'a-': Color(0xFF3BB687),
|
||||
'b+': Color(0xFF4F99C0),
|
||||
'b': Color(0xFF4F64C9),
|
||||
'b-': Color(0xFF5650C7),
|
||||
'c+': Color(0xFF552883),
|
||||
'c': Color(0xFF733E8F),
|
||||
'c-': Color(0xFF79558C),
|
||||
'd+': Color(0xFF8E6091),
|
||||
'd': Color(0xFF907591),
|
||||
'z': Color(0xFF375433),
|
||||
'top1': Colors.yellowAccent
|
||||
};
|
||||
|
||||
const List<Color> achievementColors = [
|
||||
Colors.grey,
|
||||
Color(0xFFB38070), // bronze
|
||||
Color(0xFF7E9EA7), // silver
|
||||
Color(0xFFE2A042), // gold
|
||||
Color(0xFF70D0A3), // platinum
|
||||
Color(0xFFD590FF), // diamond
|
||||
Colors.white,
|
||||
];
|
||||
|
||||
const Map<String, Duration> sprintAverages = {
|
||||
// based on https://discord.com/channels/673303546107658242/674421736162197515/1277367281264889908
|
||||
'x+': Duration(seconds: 18, milliseconds: 867),
|
||||
'x': Duration(seconds: 23, milliseconds: 277),
|
||||
'u': Duration(seconds: 28, milliseconds: 853),
|
||||
'ss': Duration(seconds: 35, milliseconds: 173),
|
||||
's+': Duration(seconds: 39, milliseconds: 028),
|
||||
's': Duration(seconds: 45, milliseconds: 807),
|
||||
's-': Duration(seconds: 48, milliseconds: 840),
|
||||
'a+': Duration(seconds: 54, milliseconds: 975),
|
||||
'a': Duration(seconds: 60, milliseconds: 287),
|
||||
'a-': Duration(seconds: 64, milliseconds: 019),
|
||||
'b+': Duration(seconds: 76, milliseconds: 531),
|
||||
'b': Duration(seconds: 77, milliseconds: 635),
|
||||
'b-': Duration(seconds: 92, milliseconds: 279),
|
||||
'c+': Duration(seconds: 97, milliseconds: 911),
|
||||
'c': Duration(seconds: 104, milliseconds: 700),
|
||||
'c-': Duration(seconds: 115, milliseconds: 173),
|
||||
'd+': Duration(seconds: 131, milliseconds: 486),
|
||||
'd': Duration(seconds: 158, milliseconds: 397),
|
||||
};
|
||||
|
||||
const Map<String, int> blitzAverages = {
|
||||
'x+': 879378,
|
||||
'x': 677479,
|
||||
'u': 485962,
|
||||
'ss': 369043,
|
||||
's+': 279242,
|
||||
's': 245619,
|
||||
's-': 199368,
|
||||
'a+': 162035,
|
||||
'a': 130949,
|
||||
'a-': 111505,
|
||||
'b+': 97251,
|
||||
'b': 83580,
|
||||
'b-': 70511,
|
||||
'c+': 56747,
|
||||
'c': 43002,
|
||||
'c-': 38925,
|
||||
'd+': 30483,
|
||||
'd': 22513,
|
||||
};
|
||||
|
||||
List<DateTime> seasonStarts = [
|
||||
DateTime.utc(2020, DateTime.april, 18, 4), // Source = twitter or something
|
||||
DateTime.utc(2024, DateTime.august, 16, 18, 41, 10) // Source = osk status page
|
||||
];
|
||||
|
||||
List<DateTime> seasonEnds = [
|
||||
DateTime.utc(2024, DateTime.july, 26, 15) // Source - TETR.IO discord guild
|
||||
];
|
||||
|
||||
/// Stolen directly from TETR.IO, redone for the sake of me
|
||||
|
||||
const List<String> clearNames = ["Zero", "Single", "Double", "Triple", "Quad", "Penta", "Hexa", "Hepta", "Octa", "Ennea", "Deca", "Hendeca", "Dodeca", "Triadeca", "Tessaradeca", "Pentedeca", "Hexadeca", "Heptadeca", "Octadeca", "Enneadeca", "Eicosa", "Kagaris"];
|
||||
|
||||
enum Lineclears{
|
||||
ZERO,
|
||||
SINGLE,
|
||||
DOUBLE,
|
||||
TRIPLE,
|
||||
QUAD,
|
||||
PENTA,
|
||||
TSPIN_MINI,
|
||||
TSPIN,
|
||||
TSPIN_MINI_SINGLE,
|
||||
TSPIN_SINGLE,
|
||||
TSPIN_MINI_DOUBLE,
|
||||
TSPIN_DOUBLE,
|
||||
TSPIN_MINI_TRIPLE,
|
||||
TSPIN_TRIPLE,
|
||||
TSPIN_MINI_QUAD,
|
||||
TSPIN_QUAD,
|
||||
TSPIN_PENTA,
|
||||
}
|
||||
|
||||
enum ComboTables{
|
||||
none,
|
||||
classic,
|
||||
modern,
|
||||
multiplier
|
||||
}
|
||||
|
||||
Map<ComboTables, String> comboTablesNames = {
|
||||
ComboTables.none: "None",
|
||||
ComboTables.classic: "Classic",
|
||||
ComboTables.modern: "Modern",
|
||||
ComboTables.multiplier: "Multiplier"
|
||||
};
|
||||
|
||||
const int BACKTOBACK_BONUS = 1;
|
||||
const double BACKTOBACK_BONUS_LOG = .8;
|
||||
const int COMBO_MINIFIER = 1;
|
||||
const double COMBO_MINIFIER_LOG = 1.25;
|
||||
const double COMBO_BONUS = .25;
|
||||
// const int ALL_CLEAR = 10; lol
|
||||
|
||||
const Map<Lineclears, int> garbage = {
|
||||
Lineclears.SINGLE: 0,
|
||||
Lineclears.DOUBLE: 1,
|
||||
Lineclears.TRIPLE: 2,
|
||||
Lineclears.QUAD: 4,
|
||||
Lineclears.PENTA: 5,
|
||||
Lineclears.TSPIN_MINI: 0,
|
||||
Lineclears.TSPIN: 0,
|
||||
Lineclears.TSPIN_MINI_SINGLE: 0,
|
||||
Lineclears.TSPIN_SINGLE: 2,
|
||||
Lineclears.TSPIN_MINI_DOUBLE: 1,
|
||||
Lineclears.TSPIN_DOUBLE: 4,
|
||||
Lineclears.TSPIN_MINI_TRIPLE: 2,
|
||||
Lineclears.TSPIN_TRIPLE: 6,
|
||||
Lineclears.TSPIN_MINI_QUAD: 4,
|
||||
Lineclears.TSPIN_QUAD: 10,
|
||||
Lineclears.TSPIN_PENTA: 12
|
||||
};
|
||||
|
||||
const Map<ComboTables, List<int>> combotable = {
|
||||
ComboTables.none: [0],
|
||||
ComboTables.classic: [0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5],
|
||||
ComboTables.modern: [0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4]
|
||||
};
|
|
@ -1,7 +1,12 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'tetrio.dart';
|
||||
import 'package:tetra_stats/data_objects/clears.dart';
|
||||
import 'package:tetra_stats/data_objects/end_context_multi.dart';
|
||||
import 'package:tetra_stats/data_objects/est_tr.dart';
|
||||
import 'package:tetra_stats/data_objects/finesse.dart';
|
||||
import 'package:tetra_stats/data_objects/nerd_stats.dart';
|
||||
import 'package:tetra_stats/data_objects/playstyle.dart';
|
||||
|
||||
// I want to implement those fancy TWC stats
|
||||
// So, i'm going to read replay for things
|
||||
|
@ -32,8 +37,11 @@ int biggestSpikeFromReplay(events){
|
|||
class Garbage{ // charsys where???
|
||||
late int sent;
|
||||
late int recived;
|
||||
late int attack;
|
||||
late int cleared;
|
||||
int? attack;
|
||||
int? cleared;
|
||||
int? sent_normal;
|
||||
int? maxspike;
|
||||
int? maxspike_nomult;
|
||||
|
||||
Garbage({
|
||||
required this.sent,
|
||||
|
@ -47,6 +55,9 @@ class Garbage{ // charsys where???
|
|||
recived = json['received'];
|
||||
attack = json['attack'];
|
||||
cleared = json['cleared'];
|
||||
sent_normal = json['sent_normal'];
|
||||
maxspike = json['maxspike'];
|
||||
maxspike_nomult = json['maxspike_nomult'];
|
||||
}
|
||||
|
||||
Garbage.toJson(){
|
||||
|
@ -54,7 +65,7 @@ class Garbage{ // charsys where???
|
|||
}
|
||||
|
||||
Garbage operator + (Garbage other){
|
||||
return Garbage(sent: sent + other.sent, recived: recived + other.recived, attack: attack + other.attack, cleared: cleared + other.cleared);
|
||||
return Garbage(sent: sent + other.sent, recived: recived + other.recived, attack: attack??0 + (other.attack??0), cleared: (cleared??0) + (other.cleared??0));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:tetra_stats/data_objects/badge.dart';
|
||||
import 'package:tetra_stats/data_objects/connections.dart';
|
||||
import 'package:tetra_stats/data_objects/distinguishment.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_zen.dart';
|
||||
|
||||
class TetrioPlayer {
|
||||
late String userId;
|
||||
late String username;
|
||||
late DateTime state;
|
||||
late String role;
|
||||
int? avatarRevision;
|
||||
int? bannerRevision;
|
||||
late DateTime registrationTime;
|
||||
List<Badge> badges = [];
|
||||
String? bio;
|
||||
String? country;
|
||||
late int friendCount;
|
||||
late int gamesPlayed;
|
||||
late int gamesWon;
|
||||
late Duration gameTime;
|
||||
late double xp;
|
||||
late int supporterTier;
|
||||
late bool verified;
|
||||
bool? badstanding;
|
||||
String? botmaster;
|
||||
Connections? connections;
|
||||
TetrioZen? zen;
|
||||
Distinguishment? distinguishment;
|
||||
DateTime? cachedUntil;
|
||||
|
||||
TetrioPlayer({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.role,
|
||||
required this.state,
|
||||
this.avatarRevision,
|
||||
this.bannerRevision,
|
||||
required this.registrationTime,
|
||||
required this.badges,
|
||||
this.bio,
|
||||
this.country,
|
||||
required this.friendCount,
|
||||
required this.gamesPlayed,
|
||||
required this.gamesWon,
|
||||
required this.gameTime,
|
||||
required this.xp,
|
||||
required this.supporterTier,
|
||||
required this.verified,
|
||||
this.badstanding,
|
||||
this.botmaster,
|
||||
required this.connections,
|
||||
this.zen,
|
||||
this.distinguishment,
|
||||
this.cachedUntil
|
||||
});
|
||||
|
||||
double get level => pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1;
|
||||
|
||||
TetrioPlayer.fromJson(Map<String, dynamic> json, DateTime stateTime, String id, String nick, [DateTime? cUntil]) {
|
||||
//developer.log("TetrioPlayer.fromJson $stateTime: $json", name: "data_objects/tetrio");
|
||||
userId = id;
|
||||
username = nick;
|
||||
state = stateTime;
|
||||
role = json['role'];
|
||||
registrationTime = json['ts'] != null ? DateTime.parse(json['ts']) : DateTime.fromMillisecondsSinceEpoch(int.parse(id.substring(0, 8), radix: 16) * 1000);
|
||||
if (json['badges'] != null) {
|
||||
json['badges'].forEach((v) {
|
||||
badges.add(Badge.fromJson(v));
|
||||
});
|
||||
}
|
||||
xp = json['xp'] != null ? json['xp'].toDouble() : -1;
|
||||
gamesPlayed = json['gamesplayed'] ?? -1;
|
||||
gamesWon = json['gameswon'] ?? -1;
|
||||
gameTime = json['gametime'] != null && json['gametime'] != -1 ? Duration(microseconds: (json['gametime'].toDouble() * 1000000).floor()) : const Duration(seconds: -1);
|
||||
country = json['country'];
|
||||
supporterTier = json['supporter_tier'] ?? 0;
|
||||
verified = json['verified'] ?? false;
|
||||
avatarRevision = json['avatar_revision'];
|
||||
bannerRevision = json['banner_revision'];
|
||||
bio = json['bio'];
|
||||
if (json['connections'] != null && json['connections'].isNotEmpty) connections = Connections.fromJson(json['connections']);
|
||||
distinguishment = json['distinguishment'] != null ? Distinguishment.fromJson(json['distinguishment']) : null;
|
||||
friendCount = json['friend_count'] ?? 0;
|
||||
badstanding = json['badstanding'];
|
||||
botmaster = json['botmaster'];
|
||||
cachedUntil = cUntil;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
// data['_id'] = userId;
|
||||
// data['username'] = username;
|
||||
data['role'] = role;
|
||||
data['ts'] = registrationTime.toString();
|
||||
if (badges.isNotEmpty) data['badges'] = badges.map((v) => v.toJson()).toList();
|
||||
if (xp >= 0) data['xp'] = xp;
|
||||
if (gamesPlayed >= 0) data['gamesplayed'] = gamesPlayed;
|
||||
if (gamesWon >= 0) data['gameswon'] = gamesWon;
|
||||
if (!gameTime.isNegative) data['gametime'] = gameTime.inMicroseconds / 1000000;
|
||||
if (country != null) data['country'] = country;
|
||||
if (supporterTier > 0) data['supporter_tier'] = supporterTier;
|
||||
if (verified) data['verified'] = verified;
|
||||
if (distinguishment != null) data['distinguishment'] = distinguishment?.toJson();
|
||||
if (avatarRevision != null) data['avatar_revision'] = avatarRevision;
|
||||
if (bannerRevision != null) data['banner_revision'] = bannerRevision;
|
||||
if (bio != null) data['bio'] = bio;
|
||||
if (connections != null) data['connections'] = connections!.toJson();
|
||||
if (friendCount > 0) data['friend_count'] = friendCount;
|
||||
if (badstanding != null) data['badstanding'] = badstanding;
|
||||
if (botmaster != null) data['botmaster'] = botmaster;
|
||||
//developer.log("TetrioPlayer.toJson: $data", name: "data_objects/tetrio");
|
||||
return data;
|
||||
}
|
||||
|
||||
bool isSameState(covariant TetrioPlayer other) {
|
||||
if (userId != other.userId) return false;
|
||||
if (username != other.username) return false;
|
||||
if (role != other.role) return false;
|
||||
if (listEquals(badges, other.badges) == false) return false;
|
||||
//if (bio != other.bio) return false;
|
||||
if (country != other.country) return false;
|
||||
if (friendCount != other.friendCount) return false;
|
||||
if (gamesPlayed != other.gamesPlayed) return false;
|
||||
if (gamesWon != other.gamesWon) return false;
|
||||
if (gameTime != other.gameTime) return false;
|
||||
if (xp != other.xp) return false;
|
||||
if (supporterTier != other.supporterTier) return false;
|
||||
if (verified != other.verified) return false;
|
||||
if (badstanding != other.badstanding) return false;
|
||||
if (botmaster != other.botmaster) return false;
|
||||
if (connections != other.connections) return false;
|
||||
if (distinguishment != other.distinguishment) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$username ($state)";
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => state.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant TetrioPlayer other) => isSameState(other) && state.isAtSameMomentAs(other.state);
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
// 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';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_prisecter.dart';
|
||||
|
||||
class TetrioPlayerFromLeaderboard {
|
||||
late String userId;
|
||||
late String username;
|
||||
late String role;
|
||||
late double xp;
|
||||
String? country;
|
||||
late DateTime timestamp;
|
||||
late int gamesPlayed;
|
||||
late int gamesWon;
|
||||
late double tr;
|
||||
late double gxe;
|
||||
late double? glicko;
|
||||
late double? rd;
|
||||
late String rank;
|
||||
late String? bestRank;
|
||||
late double apm;
|
||||
late double pps;
|
||||
late double vs;
|
||||
late bool decaying;
|
||||
late NerdStats nerdStats;
|
||||
late EstTr estTr;
|
||||
late Playstyle playstyle;
|
||||
late int gamesPlayedTotal;
|
||||
late int gamesWonTotal;
|
||||
late Duration playtime;
|
||||
late int ar;
|
||||
late Map<String, int> ar_counts;
|
||||
late Prisecter prisecter;
|
||||
|
||||
TetrioPlayerFromLeaderboard(
|
||||
this.userId,
|
||||
this.username,
|
||||
this.role,
|
||||
this.xp,
|
||||
this.country,
|
||||
this.timestamp,
|
||||
this.gamesPlayed,
|
||||
this.gamesWon,
|
||||
this.tr,
|
||||
this.gxe,
|
||||
this.glicko,
|
||||
this.rd,
|
||||
this.rank,
|
||||
this.bestRank,
|
||||
this.apm,
|
||||
this.pps,
|
||||
this.vs,
|
||||
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;
|
||||
|
||||
TetrioPlayerFromLeaderboard.fromJson(Map<String, dynamic> json, DateTime ts) {
|
||||
userId = json['_id'];
|
||||
username = json['username'];
|
||||
role = json['role'];
|
||||
xp = json['xp'].toDouble();
|
||||
country = json['country'];
|
||||
timestamp = ts;
|
||||
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']?.toDouble();
|
||||
glicko = json['league']['glicko']?.toDouble();
|
||||
rd = json['league']['rd']?.toDouble();
|
||||
rank = json['league']['rank'];
|
||||
bestRank = json['league']['bestrank'];
|
||||
apm = json['league']['apm'] != null ? json['league']['apm'].toDouble() : 0.00;
|
||||
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'];
|
||||
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);
|
||||
}
|
||||
|
||||
num getStatByEnum(Stats stat){
|
||||
switch (stat) {
|
||||
case Stats.tr:
|
||||
return tr;
|
||||
case Stats.glicko:
|
||||
return glicko??-1;
|
||||
case Stats.gxe:
|
||||
return gxe;
|
||||
case Stats.s1tr:
|
||||
return s1tr;
|
||||
case Stats.rd:
|
||||
return rd??-1;
|
||||
case Stats.gp:
|
||||
return gamesPlayed;
|
||||
case Stats.gw:
|
||||
return gamesWon;
|
||||
case Stats.wr:
|
||||
return winrate*100;
|
||||
case Stats.apm:
|
||||
return apm;
|
||||
case Stats.pps:
|
||||
return pps;
|
||||
case Stats.vs:
|
||||
return vs;
|
||||
case Stats.app:
|
||||
return nerdStats.app;
|
||||
case Stats.dss:
|
||||
return nerdStats.dss;
|
||||
case Stats.dsp:
|
||||
return nerdStats.dsp;
|
||||
case Stats.appdsp:
|
||||
return nerdStats.appdsp;
|
||||
case Stats.vsapm:
|
||||
return nerdStats.vsapm;
|
||||
case Stats.cheese:
|
||||
return nerdStats.cheese;
|
||||
case Stats.gbe:
|
||||
return nerdStats.gbe;
|
||||
case Stats.nyaapp:
|
||||
return nerdStats.nyaapp;
|
||||
case Stats.area:
|
||||
return nerdStats.area;
|
||||
case Stats.eTR:
|
||||
return estTr.esttr;
|
||||
case Stats.acceTR:
|
||||
return esttracc;
|
||||
case Stats.acceTRabs:
|
||||
return esttracc.abs();
|
||||
case Stats.opener:
|
||||
return playstyle.opener;
|
||||
case Stats.plonk:
|
||||
return playstyle.plonk;
|
||||
case Stats.infDS:
|
||||
return playstyle.infds;
|
||||
case Stats.stride:
|
||||
return playstyle.stride;
|
||||
case Stats.stridemMinusPlonk:
|
||||
return playstyle.stride - playstyle.plonk;
|
||||
case Stats.openerMinusInfDS:
|
||||
return playstyle.opener - playstyle.infds;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,756 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:tetra_stats/data_objects/leaderboard_position.dart';
|
||||
import 'package:tetra_stats/data_objects/player_leaderboard_position.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart';
|
||||
|
||||
class TetrioPlayersLeaderboard {
|
||||
late String type;
|
||||
late DateTime timestamp;
|
||||
late List<TetrioPlayerFromLeaderboard> leaderboard;
|
||||
|
||||
TetrioPlayersLeaderboard(this.type, this.leaderboard);
|
||||
|
||||
@override
|
||||
String toString(){
|
||||
return "$type leaderboard: ${leaderboard.length} players";
|
||||
}
|
||||
|
||||
List<TetrioPlayerFromLeaderboard> getStatRanking(List<TetrioPlayerFromLeaderboard> leaderboard, Stats stat, {bool reversed = false, String country = ""}){
|
||||
List<TetrioPlayerFromLeaderboard> lb = List.from(leaderboard);
|
||||
if (country.isNotEmpty){
|
||||
lb.removeWhere((element) => element.country != country);
|
||||
}
|
||||
lb.sort(((a, b) {
|
||||
if (a.getStatByEnum(stat).isNaN) return 1;
|
||||
if (b.getStatByEnum(stat).isNaN) return -1;
|
||||
if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){
|
||||
return reversed ? 1 : -1;
|
||||
}else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){
|
||||
return 0;
|
||||
}else{
|
||||
return reversed ? -1 : 1;
|
||||
}
|
||||
}));
|
||||
return lb;
|
||||
}
|
||||
|
||||
List<TetrioPlayerFromLeaderboard> getStatRankingFromLB(Stats stat, {bool reversed = false, String country = ""}){
|
||||
List<TetrioPlayerFromLeaderboard> lb = List.from(leaderboard);
|
||||
if (country.isNotEmpty){
|
||||
lb.removeWhere((element) => element.country != country);
|
||||
}
|
||||
lb.sort(((a, b) {
|
||||
if (a.getStatByEnum(stat).isNaN) return 1;
|
||||
if (b.getStatByEnum(stat).isNaN) return -1;
|
||||
if (a.getStatByEnum(stat) > b.getStatByEnum(stat)){
|
||||
return reversed ? 1 : -1;
|
||||
}else if (a.getStatByEnum(stat) == b.getStatByEnum(stat)){
|
||||
return 0;
|
||||
}else{
|
||||
return reversed ? -1 : 1;
|
||||
}
|
||||
}));
|
||||
return lb;
|
||||
}
|
||||
|
||||
List<dynamic> getRankData(String rank){
|
||||
if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank");
|
||||
List<TetrioPlayerFromLeaderboard> filtredLeaderboard = List.from(leaderboard);
|
||||
if (rank.isNotEmpty) {
|
||||
filtredLeaderboard.removeWhere((element) => element.rank != rank);
|
||||
}
|
||||
if (filtredLeaderboard.isNotEmpty){
|
||||
double
|
||||
avgAPM = 0,
|
||||
avgPPS = 0,
|
||||
avgVS = 0,
|
||||
avgTR = 0,
|
||||
avgGlixare = 0,
|
||||
avgGlicko = 0,
|
||||
avgRD = 0,
|
||||
avgAPP = 0,
|
||||
avgVSAPM = 0,
|
||||
avgDSS = 0,
|
||||
avgDSP = 0,
|
||||
avgAPPDSP = 0,
|
||||
avgCheese = 0,
|
||||
avgGBE = 0,
|
||||
avgNyaAPP = 0,
|
||||
avgArea = 0,
|
||||
avgEstTR = 0,
|
||||
avgEstAcc = 0,
|
||||
avgOpener = 0,
|
||||
avgPlonk = 0,
|
||||
avgStride = 0,
|
||||
avgInfDS = 0,
|
||||
lowestTR = 25000,
|
||||
lowestGlixare = double.infinity,
|
||||
lowestGlicko = double.infinity,
|
||||
lowestRD = double.infinity,
|
||||
lowestWinrate = double.infinity,
|
||||
lowestAPM = double.infinity,
|
||||
lowestPPS = double.infinity,
|
||||
lowestVS = double.infinity,
|
||||
lowestAPP = double.infinity,
|
||||
lowestVSAPM = double.infinity,
|
||||
lowestDSS = double.infinity,
|
||||
lowestDSP = double.infinity,
|
||||
lowestAPPDSP = double.infinity,
|
||||
lowestCheese = double.infinity,
|
||||
lowestGBE = double.infinity,
|
||||
lowestNyaAPP = double.infinity,
|
||||
lowestArea = double.infinity,
|
||||
lowestEstTR = double.infinity,
|
||||
lowestEstAcc = double.infinity,
|
||||
lowestOpener = double.infinity,
|
||||
lowestPlonk = double.infinity,
|
||||
lowestStride = double.infinity,
|
||||
lowestInfDS = double.infinity,
|
||||
highestTR = double.negativeInfinity,
|
||||
highestGlixare = double.negativeInfinity,
|
||||
highestGlicko = double.negativeInfinity,
|
||||
highestRD = double.negativeInfinity,
|
||||
highestWinrate = double.negativeInfinity,
|
||||
highestAPM = double.negativeInfinity,
|
||||
highestPPS = double.negativeInfinity,
|
||||
highestVS = double.negativeInfinity,
|
||||
highestAPP = double.negativeInfinity,
|
||||
highestVSAPM = double.negativeInfinity,
|
||||
highestDSS = double.negativeInfinity,
|
||||
highestDSP = double.negativeInfinity,
|
||||
highestAPPDSP = double.negativeInfinity,
|
||||
highestCheese = double.negativeInfinity,
|
||||
highestGBE = double.negativeInfinity,
|
||||
highestNyaAPP = double.negativeInfinity,
|
||||
highestArea = double.negativeInfinity,
|
||||
highestEstTR = double.negativeInfinity,
|
||||
highestEstAcc = double.negativeInfinity,
|
||||
highestOpener = double.negativeInfinity,
|
||||
highestPlonk = double.negativeInfinity,
|
||||
highestStride = double.negativeInfinity,
|
||||
highestInfDS = double.negativeInfinity;
|
||||
int avgGamesPlayed = 0,
|
||||
avgGamesWon = 0,
|
||||
totalGamesPlayed = 0,
|
||||
totalGamesWon = 0,
|
||||
lowestGamesPlayed = pow(2, 53) as int,
|
||||
lowestGamesWon = pow(2, 53) as int,
|
||||
highestGamesPlayed = 0,
|
||||
highestGamesWon = 0;
|
||||
String lowestTRid = "", lowestTRnick = "",
|
||||
lowestGlixareID = "", lowestGlixareNick = "",
|
||||
lowestGlickoID = "", lowestGlickoNick = "",
|
||||
lowestRdID = "", lowestRdNick = "",
|
||||
lowestGamesPlayedID = "", lowestGamesPlayedNick = "",
|
||||
lowestGamesWonID = "", lowestGamesWonNick = "",
|
||||
lowestWinrateID = "", lowestWinrateNick = "",
|
||||
lowestAPMid = "", lowestAPMnick = "",
|
||||
lowestPPSid = "", lowestPPSnick = "",
|
||||
lowestVSid = "", lowestVSnick = "",
|
||||
lowestAPPid = "", lowestAPPnick = "",
|
||||
lowestVSAPMid = "", lowestVSAPMnick = "",
|
||||
lowestDSSid = "", lowestDSSnick = "",
|
||||
lowestDSPid = "", lowestDSPnick = "",
|
||||
lowestAPPDSPid = "", lowestAPPDSPnick = "",
|
||||
lowestCheeseID = "", lowestCheeseNick = "",
|
||||
lowestGBEid = "", lowestGBEnick = "",
|
||||
lowestNyaAPPid = "", lowestNyaAPPnick = "",
|
||||
lowestAreaID = "", lowestAreaNick = "",
|
||||
lowestEstTRid = "", lowestEstTRnick = "",
|
||||
lowestEstAccID = "", lowestEstAccNick = "",
|
||||
lowestOpenerID = "", lowestOpenerNick = "",
|
||||
lowestPlonkID = "", lowestPlonkNick = "",
|
||||
lowestStrideID = "", lowestStrideNick = "",
|
||||
lowestInfDSid = "", lowestInfDSnick = "",
|
||||
highestTRid = "", highestTRnick = "",
|
||||
highestGlixareID = "", highestGlixareNick = "",
|
||||
highestGlickoID = "", highestGlickoNick = "",
|
||||
highestRdID = "", highestRdNick = "",
|
||||
highestGamesPlayedID = "", highestGamesPlayedNick = "",
|
||||
highestGamesWonID = "", highestGamesWonNick = "",
|
||||
highestWinrateID = "", highestWinrateNick = "",
|
||||
highestAPMid = "", highestAPMnick = "",
|
||||
highestPPSid = "", highestPPSnick = "",
|
||||
highestVSid = "", highestVSnick = "",
|
||||
highestAPPid = "", highestAPPnick = "",
|
||||
highestVSAPMid = "", highestVSAPMnick = "",
|
||||
highestDSSid = "", highestDSSnick = "",
|
||||
highestDSPid = "", highestDSPnick = "",
|
||||
highestAPPDSPid = "", highestAPPDSPnick = "",
|
||||
highestCheeseID = "", highestCheeseNick = "",
|
||||
highestGBEid = "", highestGBEnick = "",
|
||||
highestNyaAPPid = "", highestNyaAPPnick = "",
|
||||
highestAreaID = "", highestAreaNick = "",
|
||||
highestEstTRid = "", highestEstTRnick = "",
|
||||
highestEstAccID = "", highestEstAccNick = "",
|
||||
highestOpenerID = "", highestOpenerNick = "",
|
||||
highestPlonkID = "", highestPlonkNick = "",
|
||||
highestStrideID = "", highestStrideNick = "",
|
||||
highestInfDSid = "", highestInfDSnick = "";
|
||||
for (var entry in filtredLeaderboard){
|
||||
avgAPM += entry.apm;
|
||||
avgPPS += entry.pps;
|
||||
avgVS += entry.vs;
|
||||
avgTR += entry.tr;
|
||||
avgGlixare += entry.gxe;
|
||||
if (entry.glicko != null) avgGlicko += entry.glicko!;
|
||||
if (entry.rd != null) avgRD += entry.rd!;
|
||||
avgAPP += entry.nerdStats.app;
|
||||
avgVSAPM += entry.nerdStats.vsapm;
|
||||
avgDSS += entry.nerdStats.dss;
|
||||
avgDSP += entry.nerdStats.dsp;
|
||||
avgAPPDSP += entry.nerdStats.appdsp;
|
||||
avgCheese += entry.nerdStats.cheese;
|
||||
avgGBE += entry.nerdStats.gbe;
|
||||
avgNyaAPP += entry.nerdStats.nyaapp;
|
||||
avgArea += entry.nerdStats.area;
|
||||
avgEstTR += entry.estTr.esttr;
|
||||
avgEstAcc += entry.esttracc;
|
||||
avgOpener += entry.playstyle.opener;
|
||||
avgPlonk += entry.playstyle.plonk;
|
||||
avgStride += entry.playstyle.stride;
|
||||
avgInfDS += entry.playstyle.infds;
|
||||
totalGamesPlayed += entry.gamesPlayed;
|
||||
totalGamesWon += entry.gamesWon;
|
||||
if (entry.tr < lowestTR){
|
||||
lowestTR = entry.tr;
|
||||
lowestTRid = entry.userId;
|
||||
lowestTRnick = entry.username;
|
||||
}
|
||||
if (entry.gxe < lowestGlixare){
|
||||
lowestGlixare = entry.gxe;
|
||||
lowestGlixareID = entry.userId;
|
||||
lowestGlixareNick = entry.username;
|
||||
}
|
||||
if (entry.glicko != null && entry.glicko! < lowestGlicko){
|
||||
lowestGlicko = entry.glicko!;
|
||||
lowestGlickoID = entry.userId;
|
||||
lowestGlickoNick = entry.username;
|
||||
}
|
||||
if (entry.rd != null && entry.rd! < lowestRD){
|
||||
lowestRD = entry.rd!;
|
||||
lowestRdID = entry.userId;
|
||||
lowestRdNick = entry.username;
|
||||
}
|
||||
if (entry.gamesPlayed < lowestGamesPlayed){
|
||||
lowestGamesPlayed = entry.gamesPlayed;
|
||||
lowestGamesPlayedID = entry.userId;
|
||||
lowestGamesPlayedNick = entry.username;
|
||||
}
|
||||
if (entry.gamesWon < lowestGamesWon){
|
||||
lowestGamesWon = entry.gamesWon;
|
||||
lowestGamesWonID = entry.userId;
|
||||
lowestGamesWonNick = entry.username;
|
||||
}
|
||||
if (entry.winrate < lowestWinrate){
|
||||
lowestWinrate = entry.winrate;
|
||||
lowestWinrateID = entry.userId;
|
||||
lowestWinrateNick = entry.username;
|
||||
}
|
||||
if (entry.apm < lowestAPM){
|
||||
lowestAPM = entry.apm;
|
||||
lowestAPMid = entry.userId;
|
||||
lowestAPMnick = entry.username;
|
||||
}
|
||||
if (entry.pps < lowestPPS){
|
||||
lowestPPS = entry.pps;
|
||||
lowestPPSid = entry.userId;
|
||||
lowestPPSnick = entry.username;
|
||||
}
|
||||
if (entry.vs < lowestVS){
|
||||
lowestVS = entry.vs;
|
||||
lowestVSid = entry.userId;
|
||||
lowestVSnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.app < lowestAPP){
|
||||
lowestAPP = entry.nerdStats.app;
|
||||
lowestAPPid = entry.userId;
|
||||
lowestAPPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.vsapm < lowestVSAPM){
|
||||
lowestVSAPM = entry.nerdStats.vsapm;
|
||||
lowestVSAPMid = entry.userId;
|
||||
lowestVSAPMnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.dss < lowestDSS){
|
||||
lowestDSS = entry.nerdStats.dss;
|
||||
lowestDSSid = entry.userId;
|
||||
lowestDSSnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.dsp < lowestDSP){
|
||||
lowestDSP = entry.nerdStats.dsp;
|
||||
lowestDSPid = entry.userId;
|
||||
lowestDSPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.appdsp < lowestAPPDSP){
|
||||
lowestAPPDSP = entry.nerdStats.appdsp;
|
||||
lowestAPPDSPid = entry.userId;
|
||||
lowestAPPDSPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.cheese < lowestCheese){
|
||||
lowestCheese = entry.nerdStats.cheese;
|
||||
lowestCheeseID = entry.userId;
|
||||
lowestCheeseNick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.gbe < lowestGBE){
|
||||
lowestGBE = entry.nerdStats.gbe;
|
||||
lowestGBEid = entry.userId;
|
||||
lowestGBEnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.nyaapp < lowestNyaAPP){
|
||||
lowestNyaAPP = entry.nerdStats.nyaapp;
|
||||
lowestNyaAPPid = entry.userId;
|
||||
lowestNyaAPPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.area < lowestArea){
|
||||
lowestArea = entry.nerdStats.area;
|
||||
lowestAreaID = entry.userId;
|
||||
lowestAreaNick = entry.username;
|
||||
}
|
||||
if (entry.estTr.esttr < lowestEstTR){
|
||||
lowestEstTR = entry.estTr.esttr;
|
||||
lowestEstTRid = entry.userId;
|
||||
lowestEstTRnick = entry.username;
|
||||
}
|
||||
if (entry.esttracc < lowestEstAcc){
|
||||
lowestEstAcc = entry.esttracc;
|
||||
lowestEstAccID = entry.userId;
|
||||
lowestEstAccNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.opener < lowestOpener){
|
||||
lowestOpener = entry.playstyle.opener;
|
||||
lowestOpenerID = entry.userId;
|
||||
lowestOpenerNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.plonk < lowestPlonk){
|
||||
lowestPlonk = entry.playstyle.plonk;
|
||||
lowestPlonkID = entry.userId;
|
||||
lowestPlonkNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.stride < lowestStride){
|
||||
lowestStride = entry.playstyle.stride;
|
||||
lowestStrideID = entry.userId;
|
||||
lowestStrideNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.infds < lowestInfDS){
|
||||
lowestInfDS = entry.playstyle.infds;
|
||||
lowestInfDSid = entry.userId;
|
||||
lowestInfDSnick = entry.username;
|
||||
}
|
||||
if (entry.tr > highestTR){
|
||||
highestTR = entry.tr;
|
||||
highestTRid = entry.userId;
|
||||
highestTRnick = entry.username;
|
||||
}
|
||||
if (entry.gxe > highestGlixare){
|
||||
highestGlixare = entry.gxe;
|
||||
highestGlixareID = entry.userId;
|
||||
highestGlixareNick = entry.username;
|
||||
}
|
||||
if (entry.glicko != null && entry.glicko! > highestGlicko){
|
||||
highestGlicko = entry.glicko!;
|
||||
highestGlickoID = entry.userId;
|
||||
highestGlickoNick = entry.username;
|
||||
}
|
||||
if (entry.rd != null && entry.rd! > highestRD){
|
||||
highestRD = entry.rd!;
|
||||
highestRdID = entry.userId;
|
||||
highestRdNick = entry.username;
|
||||
}
|
||||
if (entry.gamesPlayed > highestGamesPlayed){
|
||||
highestGamesPlayed = entry.gamesPlayed;
|
||||
highestGamesPlayedID = entry.userId;
|
||||
highestGamesPlayedNick = entry.username;
|
||||
}
|
||||
if (entry.gamesWon > highestGamesWon){
|
||||
highestGamesWon = entry.gamesWon;
|
||||
highestGamesWonID = entry.userId;
|
||||
highestGamesWonNick = entry.username;
|
||||
}
|
||||
if (entry.winrate > highestWinrate){
|
||||
highestWinrate = entry.winrate;
|
||||
highestWinrateID = entry.userId;
|
||||
highestWinrateNick = entry.username;
|
||||
}
|
||||
if (entry.apm > highestAPM){
|
||||
highestAPM = entry.apm;
|
||||
highestAPMid = entry.userId;
|
||||
highestAPMnick = entry.username;
|
||||
}
|
||||
if (entry.pps > highestPPS){
|
||||
highestPPS = entry.pps;
|
||||
highestPPSid = entry.userId;
|
||||
highestPPSnick = entry.username;
|
||||
}
|
||||
if (entry.vs > highestVS){
|
||||
highestVS = entry.vs;
|
||||
highestVSid = entry.userId;
|
||||
highestVSnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.app > highestAPP){
|
||||
highestAPP = entry.nerdStats.app;
|
||||
highestAPPid = entry.userId;
|
||||
highestAPPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.vsapm > highestVSAPM){
|
||||
highestVSAPM = entry.nerdStats.vsapm;
|
||||
highestVSAPMid = entry.userId;
|
||||
highestVSAPMnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.dss > highestDSS){
|
||||
highestDSS = entry.nerdStats.dss;
|
||||
highestDSSid = entry.userId;
|
||||
highestDSSnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.dsp > highestDSP){
|
||||
highestDSP = entry.nerdStats.dsp;
|
||||
highestDSPid = entry.userId;
|
||||
highestDSPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.appdsp > highestAPPDSP){
|
||||
highestAPPDSP = entry.nerdStats.appdsp;
|
||||
highestAPPDSPid = entry.userId;
|
||||
highestAPPDSPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.cheese > highestCheese){
|
||||
highestCheese = entry.nerdStats.cheese;
|
||||
highestCheeseID = entry.userId;
|
||||
highestCheeseNick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.gbe > highestGBE){
|
||||
highestGBE = entry.nerdStats.gbe;
|
||||
highestGBEid = entry.userId;
|
||||
highestGBEnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.nyaapp > highestNyaAPP){
|
||||
highestNyaAPP = entry.nerdStats.nyaapp;
|
||||
highestNyaAPPid = entry.userId;
|
||||
highestNyaAPPnick = entry.username;
|
||||
}
|
||||
if (entry.nerdStats.area > highestArea){
|
||||
highestArea = entry.nerdStats.area;
|
||||
highestAreaID = entry.userId;
|
||||
highestAreaNick = entry.username;
|
||||
}
|
||||
if (entry.estTr.esttr > highestEstTR){
|
||||
highestEstTR = entry.estTr.esttr;
|
||||
highestEstTRid = entry.userId;
|
||||
highestEstTRnick = entry.username;
|
||||
}
|
||||
if (entry.esttracc > highestEstAcc){
|
||||
highestEstAcc = entry.esttracc;
|
||||
highestEstAccID = entry.userId;
|
||||
highestEstAccNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.opener > highestOpener){
|
||||
highestOpener = entry.playstyle.opener;
|
||||
highestOpenerID = entry.userId;
|
||||
highestOpenerNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.plonk > highestPlonk){
|
||||
highestPlonk = entry.playstyle.plonk;
|
||||
highestPlonkID = entry.userId;
|
||||
highestPlonkNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.stride > highestStride){
|
||||
highestStride = entry.playstyle.stride;
|
||||
highestStrideID = entry.userId;
|
||||
highestStrideNick = entry.username;
|
||||
}
|
||||
if (entry.playstyle.infds > highestInfDS){
|
||||
highestInfDS = entry.playstyle.infds;
|
||||
highestInfDSid = entry.userId;
|
||||
highestInfDSnick = entry.username;
|
||||
}
|
||||
}
|
||||
avgAPM /= filtredLeaderboard.length;
|
||||
avgPPS /= filtredLeaderboard.length;
|
||||
avgVS /= filtredLeaderboard.length;
|
||||
avgTR /= filtredLeaderboard.length;
|
||||
avgGlixare /= filtredLeaderboard.length;
|
||||
avgGlicko /= filtredLeaderboard.length;
|
||||
avgRD /= filtredLeaderboard.length;
|
||||
avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor();
|
||||
avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor();
|
||||
avgAPP /= filtredLeaderboard.length;
|
||||
avgVSAPM /= filtredLeaderboard.length;
|
||||
avgDSS /= filtredLeaderboard.length;
|
||||
avgDSP /= filtredLeaderboard.length;
|
||||
avgAPPDSP /= leaderboard.length;
|
||||
avgCheese /= filtredLeaderboard.length;
|
||||
avgGBE /= filtredLeaderboard.length;
|
||||
avgNyaAPP /= filtredLeaderboard.length;
|
||||
avgArea /= filtredLeaderboard.length;
|
||||
avgEstTR /= filtredLeaderboard.length;
|
||||
avgEstAcc /= filtredLeaderboard.length;
|
||||
avgOpener /= filtredLeaderboard.length;
|
||||
avgPlonk /= filtredLeaderboard.length;
|
||||
avgStride /= filtredLeaderboard.length;
|
||||
avgInfDS /= filtredLeaderboard.length;
|
||||
return [TetraLeague(id: "", timestamp: DateTime.now(), apm: avgAPM, pps: avgPPS, vs: avgVS, gxe: avgGlixare, glicko: avgGlicko, rd: avgRD, gamesPlayed: avgGamesPlayed, gamesWon: avgGamesWon, bestRank: rank, decaying: false, tr: avgTR, rank: rank == "" ? "z" : rank, percentileRank: rank, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason),
|
||||
{
|
||||
"totalGamesPlayed": totalGamesPlayed,
|
||||
"totalGamesWon": totalGamesWon,
|
||||
"players": filtredLeaderboard.length,
|
||||
"lowestTR": lowestTR,
|
||||
"lowestTRid": lowestTRid,
|
||||
"lowestTRnick": lowestTRnick,
|
||||
"lowestGlixare": lowestGlixare,
|
||||
"lowestGlixareID": lowestGlixareID,
|
||||
"lowestGlixareNick": lowestGlixareNick,
|
||||
"lowestS1tr": lowestGlixare * 250,
|
||||
"lowestS1trID": lowestGlixareID,
|
||||
"lowestS1trNick": lowestGlixareNick,
|
||||
"lowestGlicko": lowestGlicko,
|
||||
"lowestGlickoID": lowestGlickoID,
|
||||
"lowestGlickoNick": lowestGlickoNick,
|
||||
"lowestRD": lowestRD,
|
||||
"lowestRdID": lowestRdID,
|
||||
"lowestRdNick": lowestRdNick,
|
||||
"lowestGamesPlayed": lowestGamesPlayed,
|
||||
"lowestGamesPlayedID": lowestGamesPlayedID,
|
||||
"lowestGamesPlayedNick": lowestGamesPlayedNick,
|
||||
"lowestGamesWon": lowestGamesWon,
|
||||
"lowestGamesWonID": lowestGamesWonID,
|
||||
"lowestGamesWonNick": lowestGamesWonNick,
|
||||
"lowestWinrate": lowestWinrate,
|
||||
"lowestWinrateID": lowestWinrateID,
|
||||
"lowestWinrateNick": lowestWinrateNick,
|
||||
"lowestAPM": lowestAPM,
|
||||
"lowestAPMid": lowestAPMid,
|
||||
"lowestAPMnick": lowestAPMnick,
|
||||
"lowestPPS": lowestPPS,
|
||||
"lowestPPSid": lowestPPSid,
|
||||
"lowestPPSnick": lowestPPSnick,
|
||||
"lowestVS": lowestVS,
|
||||
"lowestVSid": lowestVSid,
|
||||
"lowestVSnick": lowestVSnick,
|
||||
"lowestAPP": lowestAPP,
|
||||
"lowestAPPid": lowestAPPid,
|
||||
"lowestAPPnick": lowestAPPnick,
|
||||
"lowestVSAPM": lowestVSAPM,
|
||||
"lowestVSAPMid": lowestVSAPMid,
|
||||
"lowestVSAPMnick": lowestVSAPMnick,
|
||||
"lowestDSS": lowestDSS,
|
||||
"lowestDSSid": lowestDSSid,
|
||||
"lowestDSSnick": lowestDSSnick,
|
||||
"lowestDSP": lowestDSP,
|
||||
"lowestDSPid": lowestDSPid,
|
||||
"lowestDSPnick": lowestDSPnick,
|
||||
"lowestAPPDSP": lowestAPPDSP,
|
||||
"lowestAPPDSPid": lowestAPPDSPid,
|
||||
"lowestAPPDSPnick": lowestAPPDSPnick,
|
||||
"lowestCheese": lowestCheese,
|
||||
"lowestCheeseID": lowestCheeseID,
|
||||
"lowestCheeseNick": lowestCheeseNick,
|
||||
"lowestGBE": lowestGBE,
|
||||
"lowestGBEid": lowestGBEid,
|
||||
"lowestGBEnick": lowestGBEnick,
|
||||
"lowestNyaAPP": lowestNyaAPP,
|
||||
"lowestNyaAPPid": lowestNyaAPPid,
|
||||
"lowestNyaAPPnick": lowestNyaAPPnick,
|
||||
"lowestArea": lowestArea,
|
||||
"lowestAreaID": lowestAreaID,
|
||||
"lowestAreaNick": lowestAreaNick,
|
||||
"lowestEstTR": lowestEstTR,
|
||||
"lowestEstTRid": lowestEstTRid,
|
||||
"lowestEstTRnick": lowestEstTRnick,
|
||||
"lowestEstAcc": lowestEstAcc,
|
||||
"lowestEstAccID": lowestEstAccID,
|
||||
"lowestEstAccNick": lowestEstAccNick,
|
||||
"lowestOpener": lowestOpener,
|
||||
"lowestOpenerID": lowestOpenerID,
|
||||
"lowestOpenerNick": lowestOpenerNick,
|
||||
"lowestPlonk": lowestPlonk,
|
||||
"lowestPlonkID": lowestPlonkID,
|
||||
"lowestPlonkNick": lowestPlonkNick,
|
||||
"lowestStride": lowestStride,
|
||||
"lowestStrideID": lowestStrideID,
|
||||
"lowestStrideNick": lowestStrideNick,
|
||||
"lowestInfDS": lowestInfDS,
|
||||
"lowestInfDSid": lowestInfDSid,
|
||||
"lowestInfDSnick": lowestInfDSnick,
|
||||
"highestTR": highestTR,
|
||||
"highestTRid": highestTRid,
|
||||
"highestTRnick": highestTRnick,
|
||||
"highestGlixare": highestGlixare,
|
||||
"highestGlixareID": highestGlixareID,
|
||||
"highestGlixareNick": highestGlixareNick,
|
||||
"highestS1tr": highestGlixare * 250,
|
||||
"highestS1trID": highestGlixareID,
|
||||
"highestS1trNick": highestGlixareNick,
|
||||
"highestGlicko": highestGlicko,
|
||||
"highestGlickoID": highestGlickoID,
|
||||
"highestGlickoNick": highestGlickoNick,
|
||||
"highestRD": highestRD,
|
||||
"highestRdID": highestRdID,
|
||||
"highestRdNick": highestRdNick,
|
||||
"highestGamesPlayed": highestGamesPlayed,
|
||||
"highestGamesPlayedID": highestGamesPlayedID,
|
||||
"highestGamesPlayedNick": highestGamesPlayedNick,
|
||||
"highestGamesWon": highestGamesWon,
|
||||
"highestGamesWonID": highestGamesWonID,
|
||||
"highestGamesWonNick": highestGamesWonNick,
|
||||
"highestWinrate": highestWinrate,
|
||||
"highestWinrateID": highestWinrateID,
|
||||
"highestWinrateNick": highestWinrateNick,
|
||||
"highestAPM": highestAPM,
|
||||
"highestAPMid": highestAPMid,
|
||||
"highestAPMnick": highestAPMnick,
|
||||
"highestPPS": highestPPS,
|
||||
"highestPPSid": highestPPSid,
|
||||
"highestPPSnick": highestPPSnick,
|
||||
"highestVS": highestVS,
|
||||
"highestVSid": highestVSid,
|
||||
"highestVSnick": highestVSnick,
|
||||
"highestAPP": highestAPP,
|
||||
"highestAPPid": highestAPPid,
|
||||
"highestAPPnick": highestAPPnick,
|
||||
"highestVSAPM": highestVSAPM,
|
||||
"highestVSAPMid": highestVSAPMid,
|
||||
"highestVSAPMnick": highestVSAPMnick,
|
||||
"highestDSS": highestDSS,
|
||||
"highestDSSid": highestDSSid,
|
||||
"highestDSSnick": highestDSSnick,
|
||||
"highestDSP": highestDSP,
|
||||
"highestDSPid": highestDSPid,
|
||||
"highestDSPnick": highestDSPnick,
|
||||
"highestAPPDSP": highestAPPDSP,
|
||||
"highestAPPDSPid": highestAPPDSPid,
|
||||
"highestAPPDSPnick": highestAPPDSPnick,
|
||||
"highestCheese": highestCheese,
|
||||
"highestCheeseID": highestCheeseID,
|
||||
"highestCheeseNick": highestCheeseNick,
|
||||
"highestGBE": highestGBE,
|
||||
"highestGBEid": highestGBEid,
|
||||
"highestGBEnick": highestGBEnick,
|
||||
"highestNyaAPP": highestNyaAPP,
|
||||
"highestNyaAPPid": highestNyaAPPid,
|
||||
"highestNyaAPPnick": highestNyaAPPnick,
|
||||
"highestArea": highestArea,
|
||||
"highestAreaID": highestAreaID,
|
||||
"highestAreaNick": highestAreaNick,
|
||||
"highestEstTR": highestEstTR,
|
||||
"highestEstTRid": highestEstTRid,
|
||||
"highestEstTRnick": highestEstTRnick,
|
||||
"highestEstAcc": highestEstAcc,
|
||||
"highestEstAccID": highestEstAccID,
|
||||
"highestEstAccNick": highestEstAccNick,
|
||||
"highestOpener": highestOpener,
|
||||
"highestOpenerID": highestOpenerID,
|
||||
"highestOpenerNick": highestOpenerNick,
|
||||
"highestPlonk": highestPlonk,
|
||||
"highestPlonkID": highestPlonkID,
|
||||
"highestPlonkNick": highestPlonkNick,
|
||||
"highestStride": highestStride,
|
||||
"highestStrideID": highestStrideID,
|
||||
"highestStrideNick": highestStrideNick,
|
||||
"highestInfDS": highestInfDS,
|
||||
"highestInfDSid": highestInfDSid,
|
||||
"highestInfDSnick": highestInfDSnick,
|
||||
"avgAPP": avgAPP,
|
||||
"avgVSAPM": avgVSAPM,
|
||||
"avgDSS": avgDSS,
|
||||
"avgDSP": avgDSP,
|
||||
"avgAPPDSP": avgAPPDSP,
|
||||
"avgCheese": avgCheese,
|
||||
"avgGBE": avgGBE,
|
||||
"avgNyaAPP": avgNyaAPP,
|
||||
"avgArea": avgArea,
|
||||
"avgEstTR": avgEstTR,
|
||||
"avgEstAcc": avgEstAcc,
|
||||
"avgOpener": avgOpener,
|
||||
"avgPlonk": avgPlonk,
|
||||
"avgStride": avgStride,
|
||||
"avgInfDS": avgInfDS,
|
||||
"toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0,
|
||||
}];
|
||||
}else{
|
||||
return [TetraLeague(id: "", timestamp: DateTime.now(), apm: 0, pps: 0, vs: 0, glicko: 0, rd: noTrRd, gamesPlayed: 0, gamesWon: 0, bestRank: rank, decaying: false, tr: 0, rank: rank, percentileRank: rank, gxe: -1, percentile: rankCutoffs[rank]!, standing: -1, standingLocal: -1, nextAt: -1, prevAt: -1, season: currentSeason),
|
||||
{"players": filtredLeaderboard.length, "lowestTR": 0, "toEnterTR": 0, "toEnterGlicko": 0}];
|
||||
}
|
||||
}
|
||||
|
||||
PlayerLeaderboardPosition? getLeaderboardPosition(Map<String, TetraLeague>league) {
|
||||
if (league.values.first.gamesPlayed == 0) return null;
|
||||
bool fakePositions = false;
|
||||
late List<TetrioPlayerFromLeaderboard> copyOfLeaderboard;
|
||||
if (leaderboard.indexWhere((element) => element.userId == league.keys.first) == -1){
|
||||
fakePositions =true;
|
||||
copyOfLeaderboard = List.of(leaderboard);
|
||||
copyOfLeaderboard.add(league.values.first.convertToPlayerFromLeaderboard(league.keys.first));
|
||||
}
|
||||
List<Stats> stats = [Stats.apm, Stats.pps, Stats.vs, Stats.gp, Stats.gw, Stats.wr, Stats.gxe,
|
||||
Stats.app, Stats.vsapm, Stats.dss, Stats.dsp, Stats.appdsp, Stats.cheese, Stats.gbe, Stats.nyaapp, Stats.area, Stats.eTR, Stats.acceTR];
|
||||
List<LeaderboardPosition?> results = [];
|
||||
for (Stats stat in stats) {
|
||||
List<TetrioPlayerFromLeaderboard> sortedLeaderboard = getStatRanking(fakePositions ? copyOfLeaderboard : leaderboard, stat, reversed: stat == Stats.cheese ? true : false);
|
||||
int position = sortedLeaderboard.indexWhere((element) => element.userId == league.keys.first) + 1;
|
||||
if (position == 0) {
|
||||
results.add(null);
|
||||
} else {
|
||||
results.add(LeaderboardPosition(fakePositions ? 1001 : position, position / sortedLeaderboard.length));
|
||||
}
|
||||
}
|
||||
return PlayerLeaderboardPosition.fromSearchResults(results);
|
||||
}
|
||||
|
||||
Map<String, List<dynamic>> get averages => {
|
||||
'x+': getRankData("x+"),
|
||||
'x': getRankData("x"),
|
||||
'u': getRankData("u"),
|
||||
'ss': getRankData("ss"),
|
||||
's+': getRankData("s+"),
|
||||
's': getRankData("s"),
|
||||
's-': getRankData("s-"),
|
||||
'a+': getRankData("a+"),
|
||||
'a': getRankData("a"),
|
||||
'a-': getRankData("a-"),
|
||||
'b+': getRankData("b+"),
|
||||
'b': getRankData("b"),
|
||||
'b-': getRankData("b-"),
|
||||
'c+': getRankData("c+"),
|
||||
'c': getRankData("c"),
|
||||
'c-': getRankData("c-"),
|
||||
'd+': getRankData("d+"),
|
||||
'd': getRankData("d"),
|
||||
'z': getRankData("z")
|
||||
};
|
||||
|
||||
Map<String, double> get cutoffsGlicko => {
|
||||
'x': getRankData("x")[1]["toEnterGlicko"],
|
||||
'u': getRankData("u")[1]["toEnterGlicko"],
|
||||
'ss': getRankData("ss")[1]["toEnterGlicko"],
|
||||
's+': getRankData("s+")[1]["toEnterGlicko"],
|
||||
's': getRankData("s")[1]["toEnterGlicko"],
|
||||
's-': getRankData("s-")[1]["toEnterGlicko"],
|
||||
'a+': getRankData("a+")[1]["toEnterGlicko"],
|
||||
'a': getRankData("a")[1]["toEnterGlicko"],
|
||||
'a-': getRankData("a-")[1]["toEnterGlicko"],
|
||||
'b+': getRankData("b+")[1]["toEnterGlicko"],
|
||||
'b': getRankData("b")[1]["toEnterGlicko"],
|
||||
'b-': getRankData("b-")[1]["toEnterGlicko"],
|
||||
'c+': getRankData("c+")[1]["toEnterGlicko"],
|
||||
'c': getRankData("c")[1]["toEnterGlicko"],
|
||||
'c-': getRankData("c-")[1]["toEnterGlicko"],
|
||||
'd+': getRankData("d+")[1]["toEnterGlicko"],
|
||||
'd': getRankData("d")[1]["toEnterGlicko"]
|
||||
};
|
||||
|
||||
TetrioPlayersLeaderboard.fromJson(List<dynamic> json, String t, DateTime ts) {
|
||||
type = t;
|
||||
timestamp = ts;
|
||||
leaderboard = [];
|
||||
for (Map<String, dynamic> entry in json) {
|
||||
leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts));
|
||||
}
|
||||
}
|
||||
|
||||
addPlayers(List<TetrioPlayerFromLeaderboard> list){
|
||||
leaderboard.addAll(list);
|
||||
}
|
||||
}
|
|
@ -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<String, dynamic> json){
|
||||
pri = json['pri'];
|
||||
sec = json['sec'];
|
||||
ter = json['ter'];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
class TetrioZen {
|
||||
late int level;
|
||||
late int score;
|
||||
|
||||
TetrioZen({required this.level, required this.score});
|
||||
|
||||
double get scoreRequirement => (10000 + 10000 * ((log(level + 1) / log(2)) - 1));
|
||||
|
||||
TetrioZen.fromJson(Map<String, dynamic> json) {
|
||||
level = json['level'];
|
||||
score = json['score'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['level'] = level;
|
||||
data['score'] = score;
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
import 'package:tetra_stats/data_objects/record_single.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_zen.dart';
|
||||
|
||||
class UserRecords{
|
||||
String id;
|
||||
RecordSingle? sprint;
|
||||
RecordSingle? blitz;
|
||||
TetrioZen zen;
|
||||
|
||||
UserRecords(this.id, this.sprint, this.blitz, this.zen);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// ignore_for_file: hash_and_equals
|
||||
|
||||
class ZenithResults{
|
||||
late double altitude;
|
||||
late double rank;
|
||||
late double peakrank;
|
||||
late double avgrankpts;
|
||||
late int floor;
|
||||
late double targetingfactor;
|
||||
late double targetinggrace;
|
||||
late double totalbonus;
|
||||
late int revives;
|
||||
late int revivesTotal;
|
||||
late bool speedrun;
|
||||
late bool speedrunSeen;
|
||||
late List<Duration> splits;
|
||||
|
||||
ZenithResults.fromJson(Map<String, dynamic> json){
|
||||
altitude = json['altitude'].toDouble();
|
||||
rank = json['rank'].toDouble();
|
||||
peakrank = json['peakrank'].toDouble();
|
||||
avgrankpts = json['avgrankpts'].toDouble();
|
||||
floor = json['floor'];
|
||||
targetingfactor = json['targetingfactor'].toDouble();
|
||||
targetinggrace = json['targetinggrace'].toDouble();
|
||||
totalbonus = json['totalbonus'].toDouble();
|
||||
revives = json['revives'];
|
||||
revivesTotal = json['revivesTotal'];
|
||||
speedrun = json['speedrun'];
|
||||
speedrunSeen = json['speedrun_seen'];
|
||||
splits = [];
|
||||
for (int ms in json['splits']) {
|
||||
splits.add(Duration(milliseconds: ms));
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -7,24 +7,20 @@ import 'dart:developer' as developer;
|
|||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tetra_stats/services/tetrio_crud.dart';
|
||||
import 'package:tetra_stats/views/customization_view.dart';
|
||||
import 'package:tetra_stats/views/ranks_averages_view.dart';
|
||||
import 'package:tetra_stats/views/sprint_and_blitz_averages.dart';
|
||||
import 'package:tetra_stats/views/tl_leaderboard_view.dart';
|
||||
import 'package:tetra_stats/views/first_time_view.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:tetra_stats/views/main_view.dart';
|
||||
import 'package:tetra_stats/views/settings_view.dart';
|
||||
import 'package:tetra_stats/views/tracked_players_view.dart';
|
||||
import 'package:tetra_stats/views/calc_view.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
late final PackageInfo packageInfo;
|
||||
late SharedPreferences prefs;
|
||||
late TetrioService teto;
|
||||
late GoRouter router;
|
||||
|
||||
ThemeData theme = ThemeData(
|
||||
fontFamily: 'Eurostile Round',
|
||||
colorScheme: const ColorScheme.dark(
|
||||
|
@ -32,6 +28,12 @@ ThemeData theme = ThemeData(
|
|||
surface: Color.fromARGB(255, 10, 10, 10),
|
||||
secondary: Color(0xFF00838F),
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
titleLarge: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42),
|
||||
titleSmall: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9, fontWeight: FontWeight.w200),
|
||||
headlineMedium: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36),
|
||||
displayLarge: TextStyle(fontSize: 18),
|
||||
),
|
||||
cardTheme: const CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)),
|
||||
drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)),
|
||||
searchBarTheme: const SearchBarThemeData(
|
||||
|
@ -44,65 +46,31 @@ ThemeData theme = ThemeData(
|
|||
),
|
||||
segmentedButtonTheme: SegmentedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
|
||||
side: const WidgetStatePropertyAll(BorderSide(color: Colors.transparent)),
|
||||
surfaceTintColor: const WidgetStatePropertyAll(Colors.cyanAccent),
|
||||
iconColor: const WidgetStatePropertyAll(Colors.cyanAccent),
|
||||
shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200),
|
||||
)
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.black
|
||||
);
|
||||
|
||||
final router = GoRouter(
|
||||
initialLocation: "/",
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
builder: (_, __) => const MainView(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'settings',
|
||||
builder: (_, __) => const SettingsView(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'customization',
|
||||
builder: (_, __) => const CustomizationView(),
|
||||
dividerColor: Color.fromARGB(50, 158, 158, 158),
|
||||
dividerTheme: DividerThemeData(color: Color.fromARGB(50, 158, 158, 158)),
|
||||
expansionTileTheme: ExpansionTileThemeData(
|
||||
expansionAnimationStyle: AnimationStyle(curve: Easing.standard, reverseCurve: Easing.standard),
|
||||
expandedAlignment: Alignment.bottomCenter,
|
||||
),
|
||||
]
|
||||
dropdownMenuTheme: DropdownMenuThemeData(textStyle: TextStyle(fontFamily: "Eurostile Round", fontSize: 18)),
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
tooltipTheme: TooltipThemeData(
|
||||
textStyle: TextStyle(color: Colors.white),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
border: Border.all(
|
||||
color: Colors.white
|
||||
),
|
||||
GoRoute(
|
||||
path: "leaderboard",
|
||||
builder: (_, __) => const TLLeaderboardView(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "LBvalues",
|
||||
builder: (_, __) => const RankAveragesView(),
|
||||
),
|
||||
]
|
||||
),
|
||||
GoRoute(
|
||||
path: "LBvalues",
|
||||
builder: (_, __) => const RankAveragesView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'states',
|
||||
builder: (_, __) => const TrackedPlayersView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'calc',
|
||||
builder: (_, __) => const CalcView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'sprintAndBlitzAverages',
|
||||
builder: (_, __) => const SprintAndBlitzView(),
|
||||
color: Colors.black,
|
||||
)
|
||||
]
|
||||
),
|
||||
GoRoute( // that one intended for Android users, that can open https://ch.tetr.io/u/ links
|
||||
path: "/u/:userId",
|
||||
builder: (_, __) => MainView(player: __.pathParameters['userId'])
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
void main() async {
|
||||
|
@ -128,6 +96,24 @@ void main() async {
|
|||
prefs = await SharedPreferences.getInstance();
|
||||
teto = TetrioService();
|
||||
|
||||
router = GoRouter(
|
||||
initialLocation: prefs.getBool("notFirstTime") == true ? "/" : "/hihello",
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
builder: (_, __) => const MainView(),
|
||||
),
|
||||
GoRoute( // that one intended for Android users, that can open https://ch.tetr.io/u/ links
|
||||
path: "/u/:userId",
|
||||
builder: (_, __) => MainView(player: __.pathParameters['userId'])
|
||||
),
|
||||
GoRoute(
|
||||
path: "/hihello",
|
||||
builder: (_, __) => const FirstTimeView(),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
// Choosing the locale
|
||||
String? locale = prefs.getString("locale");
|
||||
if (locale == null){
|
||||
|
|
|
@ -86,4 +86,24 @@ class DB {
|
|||
var newDBStats = await dbFile.stat();
|
||||
return dbStats.size - newDBStats.size;
|
||||
}
|
||||
|
||||
Future<bool> checkImportingDB(File db) async {
|
||||
final newDB = await openDatabase(db.path);
|
||||
var usersTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioUsersTable}`);");
|
||||
List<String> usersTableRows = [for (Map<String, Object?> row in usersTable) row["name"] as String];
|
||||
if (!listEquals(usersTableRows, tetrioUsersTableRows)) return false;
|
||||
var usersToTrackTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioUsersToTrackTable}`);");
|
||||
List<String> usersToTrackTableRows = [for (Map<String, Object?> row in usersToTrackTable) row["name"] as String];
|
||||
if (!listEquals(usersToTrackTableRows, tetrioUsersToTrackTableRows)) return false;
|
||||
var leagueMatchesTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetraLeagueMatchesTable}`);");
|
||||
List<String> leagueMatchesTableRows = [for (Map<String, Object?> row in leagueMatchesTable) row["name"] as String];
|
||||
if (!listEquals(leagueMatchesTableRows, tetraLeagueMatchesTableRows)) return false;
|
||||
var tlReplayStatsTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioTLReplayStatsTable}`);");
|
||||
List<String> TLReplayStatsTableRows = [for (Map<String, Object?> row in tlReplayStatsTable) row["name"] as String];
|
||||
if (!listEquals(TLReplayStatsTableRows, tetrioTLReplayStatsTableRows)) return false;
|
||||
var leagueTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioLeagueTable}`);");
|
||||
List<String> leagueTableRows = [for (Map<String, Object?> row in leagueTable) row["name"] as String];
|
||||
if (!listEquals(leagueTableRows, tetrioLeagueTableRows)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,21 +4,38 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sql.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_stats.dart';
|
||||
import 'package:tetra_stats/data_objects/cutoff_tetrio.dart';
|
||||
import 'package:tetra_stats/data_objects/end_context_multi.dart';
|
||||
import 'package:tetra_stats/data_objects/news.dart';
|
||||
import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart';
|
||||
import 'package:tetra_stats/data_objects/player_leaderboard_position.dart';
|
||||
import 'package:tetra_stats/data_objects/record_single.dart';
|
||||
import 'package:tetra_stats/data_objects/singleplayer_stream.dart';
|
||||
import 'package:tetra_stats/data_objects/summaries.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league_alpha_stream.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_zen.dart';
|
||||
import 'package:tetra_stats/data_objects/user_records.dart';
|
||||
import 'package:tetra_stats/main.dart' show packageInfo;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:tetra_stats/services/custom_http_client.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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';
|
||||
import 'package:csv/csv.dart';
|
||||
|
||||
const String dbName = "TetraStats.db";
|
||||
const String webVersionDomain = "ts.dan63.by";
|
||||
const String tetrioUsersTable = "tetrioUsers";
|
||||
const String tetrioUsersToTrackTable = "tetrioUsersToTrack";
|
||||
const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces";
|
||||
|
@ -33,6 +50,11 @@ const String endContext2 = "endContext2";
|
|||
const String statesCol = "jsonStates";
|
||||
const String player1id = "player1id";
|
||||
const String player2id = "player2id";
|
||||
const List<String> tetrioUsersTableRows = [idCol, nickCol, "jsonStates"];
|
||||
const List<String> tetrioUsersToTrackTableRows = [idCol];
|
||||
const List<String> tetraLeagueMatchesTableRows = [idCol, replayID, player1id, player2id, timestamp, endContext1, endContext2];
|
||||
const List<String> tetrioTLReplayStatsTableRows = [idCol, "data", "freyhoe"];
|
||||
const List<String> tetrioLeagueTableRows = [idCol, "gamesplayed", "gameswon", "tr", "glicko", "rd", "gxe", "rank", "bestrank", "apm", "pps", "vs", "decaying", "standing", "standing_local", "percentile", "prev_rank", "prev_at", "next_rank", "next_at", "percentile_rank", "season"];
|
||||
/// Table, that store players data, their stats at some moments of time
|
||||
const String createTetrioUsersTable = '''
|
||||
CREATE TABLE IF NOT EXISTS "tetrioUsers" (
|
||||
|
@ -268,7 +290,7 @@ class TetrioService extends DB {
|
|||
// If failed, actually trying to retrieve
|
||||
Uri url;
|
||||
if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID});
|
||||
} else { // Actually going to hit inoue
|
||||
url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
|
||||
}
|
||||
|
@ -337,6 +359,25 @@ class TetrioService extends DB {
|
|||
return data;
|
||||
}
|
||||
|
||||
|
||||
/// Returns three integers, representing size of the database in bytes, amount of TL records in it and amount of TL states in it
|
||||
Future<(int, int, int)> getDatabaseData() async {
|
||||
await ensureDbIsOpen();
|
||||
final db = getDatabaseOrThrow();
|
||||
String dbPath;
|
||||
if (kIsWeb) {
|
||||
dbPath = dbName;
|
||||
} else {
|
||||
final docsPath = await getApplicationDocumentsDirectory();
|
||||
dbPath = join(docsPath.path, dbName);
|
||||
}
|
||||
var dbFile = File(dbPath);
|
||||
var dbSize = (await dbFile.stat()).size;
|
||||
var dbTLRecordsQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetraLeagueMatchesTable}`')).first['COUNT(*)']! as int;
|
||||
var dbTLStatesQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetrioLeagueTable}`')).first['COUNT(*)']! as int;
|
||||
return (dbSize, dbTLRecordsQuery, dbTLStatesQuery);
|
||||
}
|
||||
|
||||
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
|
||||
/// Throws an exception if fails to retrieve.
|
||||
Future<SingleplayerStream> fetchStream(String userID, String stream) async {
|
||||
|
@ -345,7 +386,7 @@ class TetrioService extends DB {
|
|||
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/$stream');
|
||||
}
|
||||
|
@ -393,7 +434,7 @@ class TetrioService extends DB {
|
|||
|
||||
Uri url;
|
||||
if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "PeakTR", "user": id});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "PeakTR", "user": id});
|
||||
} else { // Actually going to hit p1nkl0bst3r api
|
||||
url = Uri.https('api.p1nkl0bst3r.xyz', 'toptr/$id');
|
||||
}
|
||||
|
@ -444,7 +485,7 @@ class TetrioService extends DB {
|
|||
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "cutoffs"});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "cutoffs"});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/labs/league_ranks');
|
||||
}
|
||||
|
@ -485,10 +526,10 @@ class TetrioService extends DB {
|
|||
}
|
||||
|
||||
Future<Cutoffs?> fetchCutoffsBeanserver() async {
|
||||
Cutoffs? cached = _cache.get("", Cutoffs);
|
||||
Cutoffs? cached = _cache.get("CutoffsTetrioleague_ranks", Cutoffs);
|
||||
if (cached != null) return cached;
|
||||
|
||||
Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/cutoffs.json');
|
||||
Uri url = Uri.https(webVersionDomain, 'beanserver_blaster/cutoffs.json');
|
||||
|
||||
try{
|
||||
final response = await client.get(url);
|
||||
|
@ -531,13 +572,68 @@ class TetrioService extends DB {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<Cutoffs>> fetchCutoffsHistory() async {
|
||||
Uri url = Uri.https(webVersionDomain, 'beanserver_blaster/history.csv');
|
||||
|
||||
try{
|
||||
final response = await client.get(url);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
List<List<dynamic>> csv = const CsvToListConverter().convert(response.body, eol: "\n")..removeAt(0);
|
||||
List<Cutoffs> history = [];
|
||||
for (List<dynamic> entry in csv){
|
||||
Map<String, double> tr = {};
|
||||
Map<String, double> glicko = {};
|
||||
Map<String, double> gxe = {};
|
||||
for(int i = 0; i < ranks.length; i++){
|
||||
tr[ranks[ranks.length - 1 - i]] = entry[1 + i*3];
|
||||
glicko[ranks[ranks.length - 1 - i]] = entry[2 + i*3];
|
||||
gxe[ranks[ranks.length - 1 - i]] = entry[3 + i*3];
|
||||
}
|
||||
history.add(
|
||||
Cutoffs(
|
||||
DateTime.fromMillisecondsSinceEpoch(entry[0]*1000),
|
||||
tr,
|
||||
glicko,
|
||||
gxe
|
||||
)
|
||||
);
|
||||
}
|
||||
return history;
|
||||
case 404:
|
||||
developer.log("fetchCutoffsHistory: 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:
|
||||
developer.log("fetchCutoffsHistory: Cutoffs are unavalable (${response.statusCode})", name: "services/tetrio_crud", error: response.statusCode);
|
||||
return [];
|
||||
default:
|
||||
developer.log("fetchCutoffsHistory: 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
|
||||
}
|
||||
}
|
||||
|
||||
Future<TetrioPlayerFromLeaderboard> fetchTopOneFromTheLeaderboard() async {
|
||||
TetrioPlayerFromLeaderboard? cached = _cache.get("topone", TetrioPlayerFromLeaderboard);
|
||||
if (cached != null) return cached;
|
||||
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLTopOne"});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLTopOne"});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/users/by/league', {"after": "25000:0:0", "limit": "1"});
|
||||
}
|
||||
|
@ -577,10 +673,11 @@ class TetrioService extends DB {
|
|||
|
||||
/// 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<List<TetraLeague>> fetchAndsaveTLHistory(String id) async {
|
||||
Future<List<TetraLeague>> fetchAndsaveTLHistory(String id, int season) async {
|
||||
// TODO: find le way to get season 2 history
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id});
|
||||
} else {
|
||||
url = Uri.https('api.p1nkl0bst3r.xyz', 'tlhist/$id');
|
||||
}
|
||||
|
@ -652,7 +749,7 @@ class TetrioService extends DB {
|
|||
Future<TetraLeagueAlphaStream> fetchAndSaveOldTLmatches(String userID) async {
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID});
|
||||
} else {
|
||||
url = Uri.https('api.p1nkl0bst3r.xyz', 'tlmatches/$userID', {"before": "0", "count": "9000"});
|
||||
}
|
||||
|
@ -694,7 +791,7 @@ class TetrioService extends DB {
|
|||
TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
|
||||
if (cached != null) return cached;
|
||||
|
||||
Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/leaderboard.json');
|
||||
Uri url = Uri.https(webVersionDomain, 'beanserver_blaster/leaderboard.json');
|
||||
|
||||
try{
|
||||
final response = await client.get(url);
|
||||
|
@ -728,34 +825,121 @@ 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({String? prisecter, String? lb, String? country}) async {
|
||||
// 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(webVersionDomain, 'oskware_bridge.php', {
|
||||
"endpoint": "leaderboard",
|
||||
"lb": lb??"league",
|
||||
if (prisecter != null) "after": prisecter,
|
||||
if (country != null) "country": country
|
||||
});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', {
|
||||
"limit": "100",
|
||||
if (prisecter != null) "after": prisecter,
|
||||
if (country != null) "country": country
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RecordSingle>> fetchTetrioRecordsLeaderboard({String? prisecter, String? lb, String? country}) async{
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {
|
||||
"endpoint": "RecordsLeaderboard",
|
||||
"lb": lb??"40l",
|
||||
if (prisecter != null) "after": prisecter,
|
||||
if (country != null) "country": country
|
||||
});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/records/${lb??"40l"}_${country != null ? "country_${country}":"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<RecordSingle> leaderboard = [];
|
||||
for (Map<String, dynamic> entry in rawJson['data']['entries']) {
|
||||
leaderboard.add(RecordSingle.fromJson(entry, -1, -1));
|
||||
}
|
||||
developer.log("fetchTetrioRecordsLeaderboard: 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("fetchTetrioRecordsLeaderboard: 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("fetchTetrioRecordsLeaderboard: 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);
|
||||
|
@ -768,7 +952,7 @@ class TetrioService extends DB {
|
|||
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioNews", "user": userID.toLowerCase().trim(), "limit": "100"});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioNews", "user": userID.toLowerCase().trim(), "limit": "100"});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/news/user_${userID.toLowerCase().trim()}', {"limit": "100"});
|
||||
}
|
||||
|
@ -810,15 +994,22 @@ class TetrioService extends DB {
|
|||
|
||||
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
|
||||
/// Throws an exception if fails to retrieve.
|
||||
Future<TetraLeagueBetaStream> fetchTLStream(String userID) async {
|
||||
TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream);
|
||||
if (cached != null) return cached;
|
||||
Future<TetraLeagueBetaStream> fetchTLStream(String userID, {String? prisecter}) async {
|
||||
// TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream);
|
||||
// if (cached != null) return cached;
|
||||
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {
|
||||
"endpoint": "tetrioUserTL",
|
||||
"user": userID.toLowerCase().trim(),
|
||||
if (prisecter != null) "after": prisecter
|
||||
});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/league/recent');
|
||||
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/league/recent', {
|
||||
"limit": "100",
|
||||
if (prisecter != null) "after": prisecter
|
||||
});
|
||||
}
|
||||
try {
|
||||
final response = await client.get(url);
|
||||
|
@ -946,7 +1137,7 @@ class TetrioService extends DB {
|
|||
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserRecords", "user": userID.toLowerCase().trim()});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUserRecords", "user": userID.toLowerCase().trim()});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records');
|
||||
}
|
||||
|
@ -999,7 +1190,7 @@ class TetrioService extends DB {
|
|||
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "Summaries", "id": id});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "Summaries", "id": id});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/users/$id/summaries');
|
||||
}
|
||||
|
@ -1137,9 +1328,9 @@ class TetrioService extends DB {
|
|||
// trying to find player with given discord id
|
||||
Uri dUrl;
|
||||
if (kIsWeb) {
|
||||
dUrl = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()});
|
||||
dUrl = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()});
|
||||
} else {
|
||||
dUrl = Uri.https('ch.tetr.io', 'api/users/search/${user.toLowerCase().trim()}');
|
||||
dUrl = Uri.https('ch.tetr.io', 'api/users/search/discord:${user.toLowerCase().trim()}');
|
||||
}
|
||||
try{
|
||||
final response = await client.get(dUrl);
|
||||
|
@ -1182,7 +1373,7 @@ class TetrioService extends DB {
|
|||
// finally going to obtain
|
||||
Uri url;
|
||||
if (kIsWeb) {
|
||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()});
|
||||
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()});
|
||||
} else {
|
||||
url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}');
|
||||
}
|
||||
|
@ -1191,7 +1382,7 @@ class TetrioService extends DB {
|
|||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
var json = jsonDecode(response.body);
|
||||
var json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
if (json['success']) {
|
||||
// parse and count stats
|
||||
TetrioPlayer player = TetrioPlayer.fromJson(json['data'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['_id'], json['data']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true));
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
Color getColorOfRank(int rank){
|
||||
if (rank < 1) return Colors.grey;
|
||||
if (rank == 1) return Colors.yellowAccent;
|
||||
if (rank == 2) return Colors.blueGrey;
|
||||
if (rank == 3) return Colors.brown[400]!;
|
||||
|
@ -8,3 +9,17 @@ Color getColorOfRank(int rank){
|
|||
if (rank <= 99) return Colors.greenAccent;
|
||||
return Colors.grey;
|
||||
}
|
||||
|
||||
Color? getStatColor(num value, num? avgValue, bool higherIsBetter){
|
||||
if (avgValue == null) return null;
|
||||
num percentile = (higherIsBetter ? value / avgValue : avgValue / value).abs();
|
||||
if (percentile > 1.50) return Colors.purpleAccent;
|
||||
if (percentile > 1.20) return Colors.blueAccent;
|
||||
if (percentile > 0.90) return Colors.greenAccent;
|
||||
if (percentile > 0.70) return Colors.yellowAccent;
|
||||
return Colors.redAccent;
|
||||
}
|
||||
|
||||
Color getDifferenceColor(num diff){
|
||||
return diff.isNegative ? Colors.redAccent : Colors.greenAccent;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> copyToClipboard(String text) async {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
|
||||
final NumberFormat compareIntf = NumberFormat("+#,###;-#,###")..maximumFractionDigits = 0;
|
||||
final NumberFormat fDiff = NumberFormat("+#,###.####;-#,###.####");
|
||||
final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3;
|
||||
final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2;
|
||||
final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0);
|
||||
|
@ -11,6 +13,7 @@ final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSetting
|
|||
final NumberFormat f1 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 1);
|
||||
final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode);
|
||||
final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2;
|
||||
final NumberFormat percentagef4 = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 4;
|
||||
|
||||
/// Readable [a] - [b], without sign
|
||||
String readableIntDifference(int a, int b){
|
||||
|
|
|
@ -73,6 +73,10 @@ String get40lTime(int microseconds){
|
|||
return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000);
|
||||
}
|
||||
|
||||
String getALittleBitMoreNormalTime(Duration time){
|
||||
return "${intf.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}";
|
||||
}
|
||||
|
||||
String getMoreNormalTime(Duration time){
|
||||
return "${nonsecs.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/utils/open_in_browser.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
late String oldWindowTitle;
|
||||
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode);
|
||||
|
||||
class AboutView extends StatefulWidget {
|
||||
const AboutView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AboutState();
|
||||
}
|
||||
|
||||
class AboutCard extends StatelessWidget{
|
||||
final String title;
|
||||
final String value;
|
||||
final String? undervalue; //what?
|
||||
final List<InlineSpan> endvalue; // ...
|
||||
|
||||
const AboutCard(this.title, this.value, this.undervalue, this.endvalue);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(child: Column(
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center),
|
||||
Divider(),
|
||||
Text(value, textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineMedium),
|
||||
if (undervalue != null) Text(undervalue!, textAlign: TextAlign.center),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.6),
|
||||
children: endvalue
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class AboutState extends State<AboutView> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle(t.aboutView.title);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(child: Center(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 6.0, 0.0, 18.0),
|
||||
child: Text(t.aboutView.title, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center),
|
||||
))),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Card(child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 568.00),
|
||||
child: Text(textAlign: TextAlign.center, t.aboutView.about),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
AboutCard(t.aboutView.appVersion, packageInfo.version, t.aboutView.build(build: packageInfo.buildNumber), [
|
||||
TextSpan(text: "${packageInfo.appName} (${packageInfo.packageName}) • "),
|
||||
TextSpan(text: t.aboutView.GHrepo, style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer()..onTap = (){launchInBrowser(Uri.https("github.com", "dan63047/TetraStats"));}),
|
||||
TextSpan(text: " • "),
|
||||
TextSpan(text: t.aboutView.submitAnIssue, style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer()..onTap = (){launchInBrowser(Uri.https("github.com", "dan63047/TetraStats/issues/new/choose"));}),
|
||||
]),
|
||||
Card(child: Center(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 6.0, 0.0, 18.0),
|
||||
child: Text(t.aboutView.credits, style: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center),
|
||||
))),
|
||||
Wrap(
|
||||
direction: Axis.horizontal,
|
||||
children: [
|
||||
FractionallySizedBox(widthFactor: 1/((MediaQuery.of(context).size.width/600).ceil()), child: AboutCard(t.aboutView.authorAndDeveloper, "dan63", null, [
|
||||
TextSpan(text: t.aboutView.supportHim, style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer()..onTap = (){launchInBrowser(Uri.https("dan63.by", "donate"));})
|
||||
])),
|
||||
FractionallySizedBox(widthFactor: 1/((MediaQuery.of(context).size.width/600).ceil()), child: AboutCard(t.aboutView.providedFormulas, "kerrmunism", null, [
|
||||
//TextSpan(text: "Support him!", style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer()..onTap = (){launchInBrowser(Uri.https("paypal.com", "paypalme/Kerrmunism"));})
|
||||
])),
|
||||
FractionallySizedBox(widthFactor: 1/((MediaQuery.of(context).size.width/600).ceil()), child: AboutCard(t.aboutView.providedS1history, "p1nkl0bst3r", null, [
|
||||
//TextSpan(text: "Support him!", style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer()..onTap = (){launchInBrowser(Uri.https("paypal.com", "paypalme/Kerrmunism"));})
|
||||
])),
|
||||
FractionallySizedBox(widthFactor: 1/((MediaQuery.of(context).size.width/600).ceil()), child: AboutCard(t.aboutView.inoue, "szy", null, [
|
||||
//TextSpan(text: "Support him!", style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer()..onTap = (){launchInBrowser(Uri.https("paypal.com", "paypalme/Kerrmunism"));})
|
||||
])),
|
||||
FractionallySizedBox(widthFactor: 1/((MediaQuery.of(context).size.width/600).ceil()), child: AboutCard(t.aboutView.zhCNlocale, "neko_ab4093", null, [
|
||||
//TextSpan(text: "Support him!", style: TextStyle(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: Theme.of(context).colorScheme.primary), recognizer: TapGestureRecognizer()..onTap = (){launchInBrowser(Uri.https("paypal.com", "paypalme/Kerrmunism"));})
|
||||
])),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/widgets/graphs.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
double? apm;
|
||||
double? pps;
|
||||
double? vs;
|
||||
NerdStats? nerdStats;
|
||||
EstTr? estTr;
|
||||
Playstyle? playstyle;
|
||||
late String oldWindowTitle;
|
||||
|
||||
class CalcView extends StatefulWidget {
|
||||
const CalcView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => CalcState();
|
||||
}
|
||||
|
||||
class CalcState extends State<CalcView> {
|
||||
TextEditingController ppsController = TextEditingController();
|
||||
TextEditingController apmController = TextEditingController();
|
||||
TextEditingController vsController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${t.statsCalc}");
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void calc() {
|
||||
apm = double.tryParse(apmController.text);
|
||||
pps = double.tryParse(ppsController.text);
|
||||
vs = double.tryParse(vsController.text);
|
||||
if (apm != null && pps != null && vs != null) {
|
||||
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);
|
||||
setState(() {});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Please, enter valid values")));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.statsCalc),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 768),
|
||||
child: Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 16, 16, 32),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: TextField(
|
||||
onSubmitted: (value) => calc(),
|
||||
controller: apmController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(label: Text("APM"), alignLabelWithHint: true),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onSubmitted: (value) => calc(),
|
||||
controller: ppsController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(label: Text("PPS"), alignLabelWithHint: true),
|
||||
)),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: TextField(
|
||||
onSubmitted: (value) => calc(),
|
||||
controller: vsController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(label: Text("VS"), alignLabelWithHint: true),
|
||||
),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => calc(),
|
||||
child: Text(t.calc),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
if (nerdStats == null) Text(t.calcViewNoValues)
|
||||
else Column(children: [
|
||||
_ListEntry(value: nerdStats!.app, label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.vsapm, label: "VS/APM", fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.dss, label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.dsp, label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.appdsp, label: "APP + DS/P", fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.cheese, label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.gbe, label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.nyaapp, label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),
|
||||
_ListEntry(value: nerdStats!.area, label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), fractionDigits: 3),
|
||||
_ListEntry(value: estTr!.esttr, label: t.statCellNum.estOfTR, fractionDigits: 3),
|
||||
Graphs(apm!, pps!, vs!, nerdStats!, playstyle!)
|
||||
],)
|
||||
],),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ListEntry extends StatelessWidget {
|
||||
final double value;
|
||||
final String label;
|
||||
final int? fractionDigits;
|
||||
const _ListEntry({required this.value, required this.label, this.fractionDigits});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits ?? 0);
|
||||
return ListTile(title: Text(label), trailing: Text(f.format(value), style: const TextStyle(fontSize: 22)));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,178 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:tetra_stats/views/settings_view.dart' show subtitleStyle;
|
||||
import 'package:tetra_stats/main.dart' show MyAppState, prefs;
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
late String oldWindowTitle;
|
||||
Color pickerColor = Colors.cyanAccent;
|
||||
Color currentColor = Colors.cyanAccent;
|
||||
|
||||
class CustomizationView extends StatefulWidget {
|
||||
const CustomizationView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => CustomizationState();
|
||||
}
|
||||
|
||||
class CustomizationState extends State<CustomizationView> {
|
||||
late bool oskKagariGimmick;
|
||||
late bool sheetbotRadarGraphs;
|
||||
late int ratingMode;
|
||||
late int timestampMode;
|
||||
|
||||
void changeColor(Color color) {
|
||||
setState(() => pickerColor = color);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) {
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${t.settings}");
|
||||
}
|
||||
_getPreferences();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _getPreferences() {
|
||||
if (prefs.getBool("oskKagariGimmick") != null) {
|
||||
oskKagariGimmick = prefs.getBool("oskKagariGimmick")!;
|
||||
} else {
|
||||
oskKagariGimmick = true;
|
||||
}
|
||||
if (prefs.getBool("sheetbotRadarGraphs") != null) {
|
||||
sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")!;
|
||||
} else {
|
||||
sheetbotRadarGraphs = false;
|
||||
}
|
||||
if (prefs.getInt("ratingMode") != null) {
|
||||
ratingMode = prefs.getInt("ratingMode")!;
|
||||
} else {
|
||||
ratingMode = 0;
|
||||
}
|
||||
if (prefs.getInt("timestampMode") != null) {
|
||||
timestampMode = prefs.getInt("timestampMode")!;
|
||||
} else {
|
||||
timestampMode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ThemeData getTheme(BuildContext context, Color color){
|
||||
return Theme.of(context).copyWith(colorScheme: ColorScheme.dark(primary: color, secondary: Colors.white));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
List<DropdownMenuItem<AppLocale>>? locales =
|
||||
<DropdownMenuItem<AppLocale>>[];
|
||||
for (var v in AppLocale.values) {
|
||||
locales.add(DropdownMenuItem<AppLocale>(
|
||||
value: v, child: Text(t.locales[v.languageTag]!)));
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.customization),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.AccentColor),
|
||||
subtitle: Text(t.AccentColorDescription, style: subtitleStyle),
|
||||
trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: const Text('Pick an accent color'),
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: changeColor,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
child: const Text('Set'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
context.findAncestorStateOfType<MyAppState>()?.setAccentColor(pickerColor);
|
||||
prefs.setInt("accentColor", pickerColor.value);
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
]));
|
||||
}
|
||||
),
|
||||
// const ListTile(
|
||||
// title: Text("Stats Table in TL mathes list"),
|
||||
// subtitle: Text("Not implemented"),
|
||||
// ),
|
||||
ListTile(title: Text(t.timestamps),
|
||||
subtitle: Text(t.timestampsDescription, style: subtitleStyle),
|
||||
trailing: DropdownButton(
|
||||
value: timestampMode,
|
||||
items: <DropdownMenuItem>[
|
||||
DropdownMenuItem(value: 0, child: Text(t.timestampsAbsoluteGMT)),
|
||||
DropdownMenuItem(value: 1, child: Text(t.timestampsAbsoluteLocalTime)),
|
||||
DropdownMenuItem(value: 2, child: Text(t.timestampsRelative))
|
||||
],
|
||||
onChanged: (dynamic value){
|
||||
prefs.setInt("timestampMode", value);
|
||||
setState(() {
|
||||
timestampMode = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(title: Text(t.rating),
|
||||
subtitle: Text(t.ratingDescription, style: subtitleStyle),
|
||||
trailing: DropdownButton(
|
||||
value: ratingMode,
|
||||
items: <DropdownMenuItem>[
|
||||
const DropdownMenuItem(value: 0, child: Text("TR")),
|
||||
const DropdownMenuItem(value: 1, child: Text("Glicko")),
|
||||
DropdownMenuItem(value: 2, child: Text(t.ratingLBposition))
|
||||
],
|
||||
onChanged: (dynamic value){
|
||||
prefs.setInt("ratingMode", value);
|
||||
setState(() {
|
||||
ratingMode = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(title: Text(t.sheetbotGraphs),
|
||||
subtitle: Text(t.sheetbotGraphsDescription, style: subtitleStyle),
|
||||
trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){
|
||||
prefs.setBool("sheetbotRadarGraphs", value);
|
||||
setState(() {
|
||||
sheetbotRadarGraphs = value;
|
||||
});
|
||||
}),),
|
||||
ListTile(title: Text(t.oskKagari),
|
||||
subtitle: Text(t.oskKagariDescription, style: subtitleStyle),
|
||||
trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){
|
||||
prefs.setBool("oskKagariGimmick", value);
|
||||
setState(() {
|
||||
oskKagariGimmick = value;
|
||||
});
|
||||
}),)
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,642 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||
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/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/widgets/graphs.dart';
|
||||
import 'package:tetra_stats/widgets/info_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/nerd_stats_thingy.dart';
|
||||
|
||||
class DestinationCalculator extends StatefulWidget{
|
||||
final BoxConstraints constraints;
|
||||
|
||||
const DestinationCalculator({super.key, required this.constraints});
|
||||
|
||||
@override
|
||||
State<DestinationCalculator> createState() => _DestinationCalculatorState();
|
||||
}
|
||||
|
||||
enum CalcCards{
|
||||
calc,
|
||||
damage
|
||||
}
|
||||
|
||||
CalcCards calcCard = CalcCards.calc;
|
||||
|
||||
class ClearData{
|
||||
final String title;
|
||||
final Lineclears lineclear;
|
||||
final int lines;
|
||||
final bool miniSpin;
|
||||
final bool spin;
|
||||
bool perfectClear = false;
|
||||
int id = -1;
|
||||
|
||||
ClearData(this.title, this.lineclear, this.lines, this.miniSpin, this.spin);
|
||||
|
||||
ClearData cloneWith(int i){
|
||||
ClearData newOne = ClearData(title, lineclear, lines, miniSpin, spin)..id = i;
|
||||
return newOne;
|
||||
}
|
||||
|
||||
bool get difficultClear {
|
||||
if (lines == 0) return false;
|
||||
if (lines >= 4 || miniSpin || spin) return true;
|
||||
else return false;
|
||||
}
|
||||
|
||||
void togglePC(){
|
||||
perfectClear = !perfectClear;
|
||||
}
|
||||
|
||||
int dealsDamage(int combo, int b2b, int previousB2B, Rules rules){
|
||||
if (lines == 0) return 0;
|
||||
double damage = 0;
|
||||
|
||||
if (spin){
|
||||
if (lines <= 5) damage += garbage[lineclear]!;
|
||||
else damage += garbage[Lineclears.TSPIN_PENTA]! + 2 * (lines - 5);
|
||||
} else if (miniSpin){
|
||||
damage += garbage[lineclear]!;
|
||||
} else {
|
||||
if (lines <= 5) damage += garbage[lineclear]!;
|
||||
else damage += garbage[Lineclears.PENTA]! + (lines - 5);
|
||||
}
|
||||
|
||||
if (difficultClear && b2b >= 1 && rules.b2b){
|
||||
if (rules.b2bChaining) damage += BACKTOBACK_BONUS * ((1 + log(1 + (b2b) * BACKTOBACK_BONUS_LOG)).floor() + (b2b == 1 ? 0 : (1 + log(1 +(b2b) * BACKTOBACK_BONUS_LOG) % 1) / 3)); // but it should be b2b-1 ???
|
||||
else damage += 1; // if b2b chaining off
|
||||
}
|
||||
|
||||
if (rules.combo && rules.comboTable != ComboTables.none) {
|
||||
if (combo >= 1){
|
||||
if (lines == 1 && rules.comboTable != ComboTables.multiplier) damage += combotable[rules.comboTable]![max(0, min(combo - 1, combotable[rules.comboTable]!.length - 1))];
|
||||
else damage *= (1 + COMBO_BONUS * (combo));
|
||||
}
|
||||
if (combo >= 2) {
|
||||
damage = max(log(1 + COMBO_MINIFIER * (combo) * COMBO_MINIFIER_LOG), damage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1){
|
||||
damage += rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b);
|
||||
}
|
||||
|
||||
if (perfectClear) damage += rules.pcDamage;
|
||||
|
||||
return (damage * rules.multiplier).floor();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<ClearData>> clearsExisting = {
|
||||
t.calcDestination.noSpinClears: [
|
||||
ClearData(t.calcDestination.noLineclear, Lineclears.ZERO, 0, false, false),
|
||||
ClearData(t.stats.lineClear.single, Lineclears.SINGLE, 1, false, false),
|
||||
ClearData(t.stats.lineClear.double, Lineclears.DOUBLE, 2, false, false),
|
||||
ClearData(t.stats.lineClear.triple, Lineclears.TRIPLE, 3, false, false),
|
||||
ClearData(t.stats.lineClear.quad, Lineclears.QUAD, 4, false, false)
|
||||
],
|
||||
t.stats.spins: [
|
||||
ClearData("${t.stats.spin} ${t.stats.lineClears.zero}", Lineclears.TSPIN, 0, false, true),
|
||||
ClearData("${t.stats.spin} ${t.stats.lineClear.single}", Lineclears.TSPIN_SINGLE, 1, false, true),
|
||||
ClearData("${t.stats.spin} ${t.stats.lineClear.double}", Lineclears.TSPIN_DOUBLE, 2, false, true),
|
||||
ClearData("${t.stats.spin} ${t.stats.lineClear.triple}", Lineclears.TSPIN_TRIPLE, 3, false, true),
|
||||
ClearData("${t.stats.spin} ${t.stats.lineClear.quad}", Lineclears.TSPIN_QUAD, 4, false, true),
|
||||
],
|
||||
"${t.stats.mini} ${t.stats.spins}": [
|
||||
ClearData("${t.stats.mini} ${t.stats.spin} ${t.stats.lineClears.zero}", Lineclears.TSPIN_MINI, 0, true, false),
|
||||
ClearData("${t.stats.mini} ${t.stats.spin} ${t.stats.lineClear.single}", Lineclears.TSPIN_MINI_SINGLE, 1, true, false),
|
||||
ClearData("${t.stats.mini} ${t.stats.spin} ${t.stats.lineClear.double}", Lineclears.TSPIN_MINI_DOUBLE, 2, true, false),
|
||||
ClearData("${t.stats.mini} ${t.stats.spin} ${t.stats.lineClear.triple}", Lineclears.TSPIN_MINI_TRIPLE, 3, true, false),
|
||||
]
|
||||
};
|
||||
|
||||
class Rules{
|
||||
bool combo = true;
|
||||
bool b2b = true;
|
||||
bool b2bChaining = false;
|
||||
bool surge = true;
|
||||
int surgeInitAmount = 4;
|
||||
int surgeInitAtB2b = 4;
|
||||
ComboTables comboTable = ComboTables.multiplier;
|
||||
int pcDamage = 5;
|
||||
int pcB2B = 1;
|
||||
double multiplier = 1.0;
|
||||
}
|
||||
|
||||
const TextStyle mainToggleInRules = TextStyle(fontSize: 18, fontWeight: ui.FontWeight.w800);
|
||||
|
||||
class _DestinationCalculatorState extends State<DestinationCalculator> {
|
||||
// Stats calculator variables
|
||||
double? apm;
|
||||
double? pps;
|
||||
double? vs;
|
||||
NerdStats? nerdStats;
|
||||
EstTr? estTr;
|
||||
Playstyle? playstyle;
|
||||
TextEditingController ppsController = TextEditingController();
|
||||
TextEditingController apmController = TextEditingController();
|
||||
TextEditingController vsController = TextEditingController();
|
||||
|
||||
// Damage Calculator variables
|
||||
List<Widget> rSideWidgets = [];
|
||||
List<Widget> lSideWidgets = [];
|
||||
int combo = -1;
|
||||
int b2b = -1;
|
||||
int previousB2B = -1;
|
||||
int totalDamage = 0;
|
||||
int normalDamage = 0;
|
||||
int comboDamage = 0;
|
||||
int b2bDamage = 0;
|
||||
int surgeDamage = 0;
|
||||
int pcDamage = 0;
|
||||
|
||||
// values for "the bar"
|
||||
late double sec2end;
|
||||
late double sec3end;
|
||||
late double sec4end;
|
||||
late double sec5end;
|
||||
|
||||
List<ClearData> clears = [];
|
||||
Map<String, int> customClearsChoice = {
|
||||
t.calcDestination.noSpinClears: 5,
|
||||
t.calcDestination.spins: 5
|
||||
};
|
||||
int idCounter = 0;
|
||||
Rules rules = Rules();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void calc() {
|
||||
apm = double.tryParse(apmController.text);
|
||||
pps = double.tryParse(ppsController.text);
|
||||
vs = double.tryParse(vsController.text);
|
||||
if (apm != null && pps != null && vs != null) {
|
||||
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);
|
||||
setState(() {});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Please, enter valid values")));
|
||||
}
|
||||
}
|
||||
|
||||
Widget getCalculator(){
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.constraints.maxWidth > 768.0) Card(
|
||||
child: Center(child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(t.calcNavigation.stats, style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
//TODO: animate those TextFields
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0),
|
||||
child: TextField(
|
||||
onSubmitted: (value) => calc(),
|
||||
onChanged: (value) {setState(() {});},
|
||||
controller: apmController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(suffix: apmController.value.text.isNotEmpty ? Text("APM") : null, alignLabelWithHint: true, hintText: widget.constraints.maxWidth > 768.0 ? t.calcDestination.placeholders(stat: t.stats.apm.short) : t.stats.apm.short),
|
||||
),
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0),
|
||||
child: TextField(
|
||||
onSubmitted: (value) => calc(),
|
||||
onChanged: (value) {setState(() {});},
|
||||
controller: ppsController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(suffix: ppsController.value.text.isNotEmpty ? Text("PPS") : null, alignLabelWithHint: true, hintText: widget.constraints.maxWidth > 768.0 ? t.calcDestination.placeholders(stat: t.stats.pps.short) : t.stats.pps.short),
|
||||
),
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0),
|
||||
child: TextField(
|
||||
onSubmitted: (value) => calc(),
|
||||
onChanged: (value) {setState(() {});},
|
||||
controller: vsController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(suffix: vsController.value.text.isNotEmpty ? Text("VS") : null, alignLabelWithHint: true, hintText: widget.constraints.maxWidth > 768.0 ? t.calcDestination.placeholders(stat: t.stats.vs.short) : t.stats.vs.short),
|
||||
),
|
||||
)
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => calc(),
|
||||
child: Text(t.calcDestination.statsCalcButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (nerdStats != null) Card(
|
||||
child: NerdStatsThingy(nerdStats: nerdStats!, width: widget.constraints.minWidth)
|
||||
),
|
||||
if (playstyle != null) Card(
|
||||
child: Graphs(apm!, pps!, vs!, nerdStats!, playstyle!)
|
||||
),
|
||||
if (nerdStats == null) InfoThingy(t.calcDestination.tip)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget rSideDamageCalculator(double width, bool hasSidebar){
|
||||
return SizedBox(
|
||||
width: width - (hasSidebar ? 80 : 0),
|
||||
height: widget.constraints.maxHeight - (hasSidebar ? 108 : 178),
|
||||
child: clears.isEmpty ? InfoThingy(t.calcDestination.damageCalcTip) :
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ReorderableListView(
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState((){
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final ClearData item = clears.removeAt(oldIndex);
|
||||
clears.insert(newIndex, item);
|
||||
});
|
||||
},
|
||||
children: lSideWidgets,
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 34.0, 0.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text("${t.calcDestination.totalDamage}:", style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)),
|
||||
Spacer(),
|
||||
Text(intf.format(totalDamage), style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: ui.FontWeight.w100))
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text("${t.calcDestination.lineclears}: ${intf.format(normalDamage)}"),
|
||||
Text("${t.calcDestination.combo}: ${intf.format(comboDamage)}"),
|
||||
Text("${t.stats.b2b.short}: ${intf.format(b2bDamage)}"),
|
||||
Text("${t.calcDestination.surge}: ${intf.format(surgeDamage)}"),
|
||||
Text("${t.calcDestination.pcs}: ${intf.format(pcDamage)}")
|
||||
],
|
||||
),
|
||||
if (totalDamage > 0) SfLinearGauge(
|
||||
minimum: 0,
|
||||
maximum: totalDamage.toDouble(),
|
||||
showLabels: false,
|
||||
showTicks: false,
|
||||
ranges: [
|
||||
LinearGaugeRange(
|
||||
color: Colors.green,
|
||||
startValue: 0,
|
||||
endValue: normalDamage.toDouble(),
|
||||
position: LinearElementPosition.cross,
|
||||
),
|
||||
LinearGaugeRange(
|
||||
color: Colors.yellow,
|
||||
startValue: normalDamage.toDouble(),
|
||||
endValue: sec2end,
|
||||
position: LinearElementPosition.cross,
|
||||
),
|
||||
LinearGaugeRange(
|
||||
color: Colors.blue,
|
||||
startValue: sec2end,
|
||||
endValue: sec3end,
|
||||
position: LinearElementPosition.cross,
|
||||
),
|
||||
LinearGaugeRange(
|
||||
color: Colors.red,
|
||||
startValue: sec3end,
|
||||
endValue: sec4end,
|
||||
position: LinearElementPosition.cross,
|
||||
),
|
||||
LinearGaugeRange(
|
||||
color: Colors.orange,
|
||||
startValue: sec4end,
|
||||
endValue: sec5end,
|
||||
position: LinearElementPosition.cross,
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton.icon(onPressed: (){setState((){clears.clear();});}, icon: const Icon(Icons.clear), label: Text("Clear all"), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0))))))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getDamageCalculator(){
|
||||
rSideWidgets = [];
|
||||
lSideWidgets = [];
|
||||
for (var key in clearsExisting.keys){
|
||||
rSideWidgets.add(Text(key));
|
||||
for (ClearData data in clearsExisting[key]!) rSideWidgets.add(Card(
|
||||
child: ListTile(
|
||||
title: Text(data.title),
|
||||
subtitle: Text("${data.dealsDamage(0, 0, 0, rules)} damage${data.difficultClear ? ", difficult" : ""}", style: TextStyle(color: Colors.grey)),
|
||||
trailing: Icon(Icons.arrow_forward_ios),
|
||||
onTap: (){
|
||||
setState((){
|
||||
clears.add(data.cloneWith(idCounter));
|
||||
});
|
||||
idCounter++;
|
||||
},
|
||||
),
|
||||
));
|
||||
if (key != "${t.stats.mini} ${t.stats.spins}") rSideWidgets.add(Card(
|
||||
child: ListTile(
|
||||
title: Text(t.calcDestination.custom),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: 30.0, child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(hintText: "5"),
|
||||
onChanged: (value) => customClearsChoice[key] = int.parse(value),
|
||||
)),
|
||||
Text(" ${t.stats.lines}", style: Theme.of(context).textTheme.displayLarge),
|
||||
Icon(Icons.arrow_forward_ios)
|
||||
],
|
||||
),
|
||||
onTap: (){
|
||||
setState((){
|
||||
clears.add(ClearData("${key == t.calcDestination.spins ? "${t.stats.spin} " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} ${t.stats.lines})", key == t.calcDestination.spins ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, customClearsChoice[key]!, false, key == t.calcDestination.spins).cloneWith(idCounter));
|
||||
});
|
||||
idCounter++;
|
||||
},
|
||||
),
|
||||
));
|
||||
rSideWidgets.add(const Divider());
|
||||
}
|
||||
|
||||
combo = -1;
|
||||
b2b = -1;
|
||||
previousB2B = -1;
|
||||
totalDamage = 0;
|
||||
normalDamage = 0;
|
||||
comboDamage = 0;
|
||||
b2bDamage = 0;
|
||||
surgeDamage = 0;
|
||||
pcDamage = 0;
|
||||
|
||||
for (ClearData lineclear in clears){
|
||||
previousB2B = b2b;
|
||||
if (lineclear.difficultClear) b2b++; else if (lineclear.lines > 0) b2b = -1;
|
||||
if (lineclear.lines > 0) combo++; else combo = -1;
|
||||
int pcDmg = lineclear.perfectClear ? (rules.pcDamage * rules.multiplier).floor() : 0;
|
||||
int normalDmg = lineclear.dealsDamage(0, 0, 0, rules) - pcDmg;
|
||||
int surgeDmg = (!lineclear.difficultClear && rules.surge && previousB2B >= rules.surgeInitAtB2b && b2b == -1) ? rules.surgeInitAmount + (previousB2B - rules.surgeInitAtB2b) : 0;
|
||||
int b2bDmg = lineclear.dealsDamage(0, b2b, b2b-1, rules) - normalDmg - pcDmg;
|
||||
int dmg = lineclear.dealsDamage(combo, b2b, previousB2B, rules);
|
||||
int comboDmg = dmg - normalDmg - b2bDmg - surgeDmg - pcDmg;
|
||||
lSideWidgets.add(
|
||||
ListTile(
|
||||
key: ValueKey(lineclear.id),
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(onPressed: (){ setState((){clears.removeWhere((element) => element.id == lineclear.id,);}); }, icon: Icon(Icons.clear)),
|
||||
if (lineclear.lines > 0) IconButton(onPressed: (){ setState((){lineclear.togglePC();}); }, icon: Icon(Icons.local_parking_outlined, color: lineclear.perfectClear ? Colors.white : Colors.grey.shade800)),
|
||||
],
|
||||
),
|
||||
title: Text("${lineclear.title}${lineclear.perfectClear ? " PC" : ""}${combo > 0 ? ", ${combo} combo" : ""}${b2b > 0 ? ", B2Bx${b2b}" : ""}"),
|
||||
subtitle: lineclear.lines > 0 ? Text("${dmg == normalDmg ? "No bonuses" : ""}${b2bDmg > 0 ? "+${intf.format(b2bDmg)} for B2B" : ""}${(b2bDmg > 0 && comboDmg > 0) ? ", " : ""}${comboDmg > 0 ? "+${intf.format(comboDmg)} for combo" : ""}${(comboDmg > 0 && lineclear.perfectClear) ? ", " : ""}${lineclear.perfectClear ? "+${intf.format(pcDmg)} for PC" : ""}${(surgeDmg > 0 && (lineclear.perfectClear || comboDmg > 0)) ? ", " : ""}${surgeDmg > 0 ? "Surge released: +${intf.format(surgeDmg)}" : ""}", style: TextStyle(color: Colors.grey)) : null,
|
||||
trailing: lineclear.lines > 0 ? Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(dmg.toString(), style: TextStyle(fontSize: 36, fontWeight: ui.FontWeight.w100)),
|
||||
) : null,
|
||||
)
|
||||
);
|
||||
totalDamage += dmg;
|
||||
normalDamage += normalDmg;
|
||||
comboDamage += comboDmg;
|
||||
b2bDamage += b2bDmg;
|
||||
surgeDamage += surgeDmg;
|
||||
pcDamage += pcDmg;
|
||||
}
|
||||
// values for "the bar"
|
||||
sec2end = normalDamage.toDouble()+comboDamage.toDouble();
|
||||
sec3end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble();
|
||||
sec4end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble();
|
||||
sec5end = normalDamage.toDouble()+comboDamage.toDouble()+b2bDamage.toDouble()+surgeDamage.toDouble()+pcDamage.toDouble();
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.constraints.maxWidth > 768.0) Card(
|
||||
child: Center(child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(t.calcNavigation.damage, style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.constraints.maxWidth > 768.0 ? 350.0 : widget.constraints.maxWidth,
|
||||
child: DefaultTabController(length: widget.constraints.maxWidth > 768.0 ? 2 : 3,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
child: TabBar(
|
||||
labelStyle: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 28),
|
||||
labelColor: Theme.of(context).colorScheme.primary,
|
||||
tabs: [
|
||||
Tab(text: t.calcDestination.actions),
|
||||
if (widget.constraints.maxWidth <= 768.0) Tab(text: t.calcDestination.results),
|
||||
Tab(text: t.calcDestination.rules),
|
||||
]
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: widget.constraints.maxHeight - 164,
|
||||
child: TabBarView(children: [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: rSideWidgets,
|
||||
),
|
||||
),
|
||||
if (widget.constraints.maxWidth <= 768.0) SingleChildScrollView(
|
||||
child: rSideDamageCalculator(widget.constraints.minWidth, false),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.calcDestination.multiplier, style: mainToggleInRules),
|
||||
trailing: SizedBox(width: 90.0, child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))],
|
||||
decoration: InputDecoration(hintText: rules.multiplier.toString()),
|
||||
onChanged: (value) => setState((){rules.multiplier = double.parse(value);}),
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.calcDestination.pcDamage),
|
||||
trailing: SizedBox(width: 90.0, child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9]'))],
|
||||
decoration: InputDecoration(hintText: rules.pcDamage.toString()),
|
||||
onChanged: (value) => setState((){rules.pcDamage = int.parse(value);}),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.calcDestination.combo, style: mainToggleInRules),
|
||||
trailing: Switch(value: rules.combo, onChanged: (v) => setState((){rules.combo = v;})),
|
||||
),
|
||||
if (rules.combo) ListTile(
|
||||
title: Text(t.calcDestination.comboTable),
|
||||
trailing: DropdownButton(
|
||||
items: [for (var v in ComboTables.values) if (v != ComboTables.none) DropdownMenuItem(value: v.index, child: Text(comboTablesNames[v]!))],
|
||||
value: rules.comboTable.index,
|
||||
onChanged: (v) => setState((){rules.comboTable = ComboTables.values[v!];}),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text("${t.stats.b2b.full} (${t.stats.b2b.short})", style: mainToggleInRules),
|
||||
trailing: Switch(value: rules.b2b, onChanged: (v) => setState((){rules.b2b = v;})),
|
||||
),
|
||||
if (rules.b2b) ListTile(
|
||||
title: Text(t.calcDestination.b2bChaining),
|
||||
trailing: Switch(value: rules.b2bChaining, onChanged: (v) => setState((){rules.b2bChaining = v;})),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.calcDestination.surge, style: mainToggleInRules),
|
||||
trailing: Switch(value: rules.surge, onChanged: (v) => setState((){rules.surge = v;})),
|
||||
),
|
||||
if (rules.surge) ListTile(
|
||||
title: Text(t.calcDestination.surgeStartAtB2B),
|
||||
trailing: SizedBox(width: 90.0, child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(hintText: rules.surgeInitAtB2b.toString()),
|
||||
onChanged: (value) => setState((){rules.surgeInitAtB2b = int.parse(value);}),
|
||||
)),
|
||||
),
|
||||
if (rules.surge) ListTile(
|
||||
title: Text(t.calcDestination.surgeStartAmount),
|
||||
trailing: SizedBox(width: 90.0, child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(hintText: rules.surgeInitAmount.toString()),
|
||||
onChanged: (value) => setState((){rules.surgeInitAmount = int.parse(value);}),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
]),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
if (widget.constraints.maxWidth > 768.0) rSideDamageCalculator(widget.constraints.maxWidth - 350, true)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: widget.constraints.maxHeight - (widget.constraints.maxWidth > 768.0 ? 32 : 133),
|
||||
child: switch (calcCard){
|
||||
CalcCards.calc => getCalculator(),
|
||||
CalcCards.damage => getDamageCalculator()
|
||||
}
|
||||
),
|
||||
if (widget.constraints.maxWidth > 768.0) SegmentedButton<CalcCards>(
|
||||
showSelectedIcon: false,
|
||||
segments: <ButtonSegment<CalcCards>>[
|
||||
ButtonSegment<CalcCards>(
|
||||
value: CalcCards.calc,
|
||||
label: Text(t.calcNavigation.stats),
|
||||
),
|
||||
ButtonSegment<CalcCards>(
|
||||
value: CalcCards.damage,
|
||||
label: Text(t.calcNavigation.damage),
|
||||
),
|
||||
],
|
||||
selected: <CalcCards>{calcCard},
|
||||
onSelectionChanged: (Set<CalcCards> newSelection) {
|
||||
setState(() {
|
||||
calcCard = newSelection.first;
|
||||
});})
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||
import 'package:tetra_stats/data_objects/cutoff_tetrio.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/utils/text_shadow.dart';
|
||||
import 'package:tetra_stats/views/rank_view.dart';
|
||||
import 'package:tetra_stats/widgets/future_error.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' hide Colors;
|
||||
|
||||
class FetchCutoffsResults{
|
||||
late bool success;
|
||||
CutoffsTetrio? cutoffs;
|
||||
Exception? exception;
|
||||
|
||||
FetchCutoffsResults(this.success, this.cutoffs, this.exception);
|
||||
}
|
||||
|
||||
class DestinationCutoffs extends StatefulWidget{
|
||||
final BoxConstraints constraints;
|
||||
|
||||
const DestinationCutoffs({super.key, required this.constraints});
|
||||
|
||||
@override
|
||||
State<DestinationCutoffs> createState() => _DestinationCutoffsState();
|
||||
}
|
||||
|
||||
class _DestinationCutoffsState extends State<DestinationCutoffs> {
|
||||
|
||||
Future<CutoffsTetrio> fetch() async {
|
||||
TetrioPlayerFromLeaderboard top1;
|
||||
CutoffsTetrio cutoffs;
|
||||
List<dynamic> requests = await Future.wait([
|
||||
teto.fetchCutoffsTetrio(),
|
||||
teto.fetchTopOneFromTheLeaderboard(),
|
||||
]);
|
||||
cutoffs = requests[0];
|
||||
top1 = requests[1];
|
||||
cutoffs.data["top1"] = CutoffTetrio(
|
||||
pos: 1,
|
||||
percentile: 0.00,
|
||||
tr: top1.tr,
|
||||
targetTr: 25000,
|
||||
apm: top1.apm,
|
||||
pps: top1.pps,
|
||||
vs: top1.vs,
|
||||
count: 1,
|
||||
countPercentile: 0.0
|
||||
);
|
||||
return cutoffs;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<CutoffsTetrio>(
|
||||
future: fetch(),
|
||||
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 SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Center(child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(t.cutoffsDestination.title, style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(t.cutoffsDestination.relevance(timestamp: timestamp(snapshot.data!.timestamp))),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom:4.0),
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Text(t.cutoffsDestination.actual),
|
||||
),
|
||||
Text(t.cutoffsDestination.target)
|
||||
]
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0),
|
||||
child: SfLinearGauge(
|
||||
minimum: 0.00000000,
|
||||
maximum: 25000.0000,
|
||||
showTicks: false,
|
||||
showLabels: false,
|
||||
ranges: [
|
||||
for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange(
|
||||
position: LinearElementPosition.outside,
|
||||
startValue: snapshot.data!.data[cutoff]!.tr,
|
||||
startWidth: 20.0,
|
||||
endWidth: 20.0,
|
||||
endValue: switch (cutoff){
|
||||
"top1" => 25000.00,
|
||||
"x+" => snapshot.data!.data["top1"]!.tr,
|
||||
_ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr
|
||||
},
|
||||
color: cutoff != "top1" ? rankColors[cutoff] : Colors.grey.shade800,
|
||||
),
|
||||
for (var cutoff in snapshot.data!.data.keys) LinearGaugeRange(
|
||||
position: LinearElementPosition.inside,
|
||||
startValue: snapshot.data!.data[cutoff]!.targetTr,
|
||||
endValue: switch (cutoff){
|
||||
"top1" => 25000.00,
|
||||
"x+" => snapshot.data!.data["top1"]!.targetTr,
|
||||
_ => snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr
|
||||
},
|
||||
color: cutoff != "top1" ? rankColors[cutoff] : null,
|
||||
),
|
||||
for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[cutoff]!.tr < snapshot.data!.data[cutoff]!.targetTr) LinearGaugeRange(
|
||||
position: LinearElementPosition.cross,
|
||||
startValue: snapshot.data!.data[cutoff]!.tr,
|
||||
endValue: snapshot.data!.data[cutoff]!.targetTr,
|
||||
color: Colors.green,
|
||||
),
|
||||
for (var cutoff in snapshot.data!.data.keys.skip(1)) if (snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr)LinearGaugeRange(
|
||||
position: LinearElementPosition.cross,
|
||||
startValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.targetTr,
|
||||
endValue: snapshot.data!.data[ranks[ranks.indexOf(cutoff)+1]]!.tr,
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
markerPointers: [
|
||||
for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.tr), style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(0, 35, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0), value: snapshot.data!.data[cutoff]!.tr, position: LinearElementPosition.outside, offset: 20),
|
||||
for (var cutoff in snapshot.data!.data.keys) LinearWidgetPointer(child: Container(child: Text(intf.format(snapshot.data!.data[cutoff]!.targetTr), textAlign: ui.TextAlign.right, style: TextStyle(fontSize: 12)), transform: Matrix4.compose(Vector3(-15, 0, 0), Quaternion.axisAngle(Vector3(0, 0, 1), -1), Vector3(1, 1, 1)), height: 45.0, transformAlignment: Alignment.topRight), value: snapshot.data!.data[cutoff]!.targetTr, position: LinearElementPosition.inside, offset: 6)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
border: TableBorder.all(color: Colors.grey.shade900),
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(48),
|
||||
1: FixedColumnWidth(155),
|
||||
2: FixedColumnWidth(140),
|
||||
3: FixedColumnWidth(160),
|
||||
4: FixedColumnWidth(150),
|
||||
5: FixedColumnWidth(90),
|
||||
6: FixedColumnWidth(130),
|
||||
7: FixedColumnWidth(120),
|
||||
8: FixedColumnWidth(125),
|
||||
9: FixedColumnWidth(70),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.cutoffsDestination.cutoffTR, textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.cutoffsDestination.targetTR, textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.cutoffsDestination.state, textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.stats.apm.short, textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.stats.pps.short, textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.stats.vs.short, textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.cutoffsDestination.advanced, textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.cutoffsDestination.players(n: intf.format(snapshot.data!.total)), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: TextButton(child: Text(t.cutoffsDestination.moreInfo, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500)), onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => RankView(rank: "", nextRankTR: snapshot.data!.data["top1"]!.tr, nextRankPercentile: 0.00, nextRankTargetTR: 25000.00, totalPlayers: snapshot.data!.total, cutoffTetrio: CutoffTetrio(apm: 0, pps: 0, vs: 0, pos: 0, percentile: 1, count: snapshot.data!.total, countPercentile: 1, tr: snapshot.data!.data["d"]!.tr, targetTr: snapshot.data!.data['d']!.targetTr)),
|
||||
),
|
||||
);
|
||||
},),
|
||||
),
|
||||
]
|
||||
),
|
||||
for (String rank in snapshot.data!.data.keys) if (rank != "top1") TableRow(
|
||||
decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])),
|
||||
children: [
|
||||
Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(f2.format(snapshot.data!.data[rank]!.targetTr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.right,
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow),
|
||||
children: [
|
||||
if (rank == "x+") TextSpan(text: t.cutoffsDestination.NumberOne(tr: f2.format(snapshot.data!.data["top1"]!.tr)), style: const TextStyle(color: Colors.white60, shadows: null))
|
||||
else TextSpan(text: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? t.cutoffsDestination.inflated(tr: f2.format(snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr - snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr)) : t.cutoffsDestination.notInflated, style: TextStyle(color: snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr > snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr ? Colors.white :Colors.white60, shadows: null)),
|
||||
TextSpan(text: "\n", style: const TextStyle(color: Colors.white60, shadows: null)),
|
||||
if (rank == "d") TextSpan(text: t.cutoffsDestination.wellDotDotDot, style: const TextStyle(color: Colors.white60, shadows: null))
|
||||
else TextSpan(text: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? t.cutoffsDestination.deflated(tr: f2.format(snapshot.data!.data[rank]!.targetTr - snapshot.data!.data[rank]!.tr)) : t.cutoffsDestination.notDeflated, style: TextStyle(color: snapshot.data!.data[rank]!.tr < snapshot.data!.data[rank]!.targetTr ? Colors.white : Colors.white60, shadows: null))
|
||||
]
|
||||
)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(snapshot.data?.data[rank]?.apm != null ? f2.format(snapshot.data!.data[rank]!.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null ? Colors.white : Colors.grey, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(snapshot.data?.data[rank]?.pps != null ? f2.format(snapshot.data!.data[rank]!.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.pps != null ? Colors.white : Colors.grey, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(snapshot.data?.data[rank]?.vs != null ? f2.format(snapshot.data!.data[rank]!.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text("${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null ? f3.format(snapshot.data!.data[rank]!.apm! / (snapshot.data!.data[rank]!.pps! * 60)) : "-.---"} ${t.stats.app.short}\n${snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.vs != null ? f3.format(snapshot.data!.data[rank]!.vs! / snapshot.data!.data[rank]!.apm!) : "-.---"} ${t.stats.vsapm.short}", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: snapshot.data?.data[rank]?.apm != null && snapshot.data?.data[rank]?.pps != null && snapshot.data?.data[rank]?.vs != null ? Colors.white : Colors.grey, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.right,
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow),
|
||||
children: [
|
||||
TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)),
|
||||
TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)),
|
||||
TextSpan(text: "\n(${t.cutoffsDestination.fromPlace(n: intf.format(snapshot.data!.data[rank]!.pos))})", style: const TextStyle(color: Colors.white60, shadows: null))
|
||||
]
|
||||
))
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: TextButton(child: Text(t.cutoffsDestination.viewButton, textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500)), onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(maintainState: true,
|
||||
builder: (context) => RankView(rank: rank, nextRankTR: rank == "x+" ? snapshot.data!.data["top1"]!.tr : snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.tr, nextRankPercentile: rank == "x+" ? 0.00 : snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.percentile, nextRankTargetTR: rank == "x+" ? 25000.00 : snapshot.data!.data[ranks[ranks.indexOf(rank)+1]]!.targetTr, totalPlayers: snapshot.data!.total, cutoffTetrio: snapshot.data!.data[rank]!),
|
||||
),
|
||||
);
|
||||
},),
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError){ return FutureError(snapshot); }
|
||||
}
|
||||
return Text("huh?");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,602 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/services/crud_exceptions.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/views/main_view.dart';
|
||||
import 'package:tetra_stats/widgets/error_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/future_error.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class DestinationGraphs extends StatefulWidget{
|
||||
final String searchFor;
|
||||
//final Function setState;
|
||||
final BoxConstraints constraints;
|
||||
final bool noSidebar;
|
||||
|
||||
const DestinationGraphs({super.key, required this.searchFor, required this.constraints, required this.noSidebar});
|
||||
|
||||
@override
|
||||
State<DestinationGraphs> createState() => _DestinationGraphsState();
|
||||
}
|
||||
|
||||
Graph graph = Graph.history;
|
||||
Stats Ychart = Stats.tr;
|
||||
|
||||
enum Graph{
|
||||
history,
|
||||
leagueState,
|
||||
leagueCutoffs
|
||||
}
|
||||
|
||||
class _DestinationGraphsState extends State<DestinationGraphs> {
|
||||
bool fetchData = false;
|
||||
bool _gamesPlayedInsteadOfDateAndTime = false;
|
||||
late ZoomPanBehavior _zoomPanBehavior;
|
||||
late TooltipBehavior _historyTooltipBehavior;
|
||||
late TooltipBehavior _tooltipBehavior;
|
||||
late TooltipBehavior _leagueTooltipBehavior;
|
||||
String yAxisTitle = "";
|
||||
bool _smooth = false;
|
||||
final List<DropdownMenuItem<Stats>> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
|
||||
Stats _Xchart = Stats.tr;
|
||||
int _season = currentSeason-1;
|
||||
ValueNotifier<String> historyPlayerUsername = ValueNotifier("");
|
||||
ValueNotifier<String> historyPlayerAvatarRevizion = ValueNotifier("");
|
||||
List<String> excludeRanks = [];
|
||||
late Future<List<_MyScatterSpot>> futureLeague = getTetraLeagueData(_Xchart, Ychart);
|
||||
String searchLeague = "";
|
||||
int? TLstatePlayers;
|
||||
DateTime? TLrelevance;
|
||||
|
||||
@override
|
||||
void initState(){
|
||||
_historyTooltipBehavior = TooltipBehavior(
|
||||
color: Colors.black,
|
||||
borderColor: Colors.white,
|
||||
enable: true,
|
||||
animationDuration: 0,
|
||||
builder: (dynamic data, dynamic point, dynamic series,
|
||||
int pointIndex, int seriesIndex) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"${f4.format(data.stat)} $yAxisTitle",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20),
|
||||
),
|
||||
),
|
||||
Text(_gamesPlayedInsteadOfDateAndTime ? t.graphsDestination.gamesPlayed(games: t.stats.games(n: data.gamesPlayed)) : timestamp(data.timestamp))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
_tooltipBehavior = TooltipBehavior(
|
||||
color: Colors.black,
|
||||
borderColor: Colors.white,
|
||||
enable: true,
|
||||
animationDuration: 0,
|
||||
builder: (dynamic data, dynamic point, dynamic series,
|
||||
int pointIndex, int seriesIndex) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"${data.nickname} (${data.rank.toUpperCase()})",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20),
|
||||
),
|
||||
),
|
||||
Text('${f4.format(data.x)} ${chartsShortTitles[_Xchart]}\n${f4.format(data.y)} ${chartsShortTitles[Ychart]}')
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
_leagueTooltipBehavior = TooltipBehavior(
|
||||
color: Colors.black,
|
||||
borderColor: Colors.white,
|
||||
enable: true,
|
||||
animationDuration: 0,
|
||||
builder: (dynamic data, dynamic point, dynamic series,
|
||||
int pointIndex, int seriesIndex) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"${f4.format(point.y)} $yAxisTitle",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20),
|
||||
),
|
||||
),
|
||||
Text(timestamp(data.ts))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
_zoomPanBehavior = ZoomPanBehavior(
|
||||
enablePinching: true,
|
||||
enableSelectionZooming: true,
|
||||
enableMouseWheelZooming : true,
|
||||
enablePanning: true,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<Map<int, Map<Stats, List<_HistoryChartSpot>>>> getHistoryData(bool fetchHistory) async {
|
||||
if(fetchHistory){
|
||||
try{
|
||||
var history = await teto.fetchAndsaveTLHistory(widget.searchFor, 1);
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.graphsDestination.fetchAndsaveTLHistoryResult(number: history.length))));
|
||||
}on TetrioHistoryNotExist{
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.noHistorySaved)));
|
||||
}on P1nkl0bst3rForbidden {
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rForbidden)));
|
||||
}on P1nkl0bst3rInternalProblem {
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rinternal)));
|
||||
}on P1nkl0bst3rTooManyRequests{
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTooManyRequests)));
|
||||
}
|
||||
}
|
||||
|
||||
List<List<TetraLeague>> states = await Future.wait<List<TetraLeague>>([
|
||||
teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2),
|
||||
]);
|
||||
Map<int, Map<Stats, List<_HistoryChartSpot>>> historyData = {}; // [season][metric][spot]
|
||||
for (int season = 0; season < currentSeason; season++){
|
||||
if (states[season].length >= 2){
|
||||
Map<Stats, List<_HistoryChartSpot>> statsMap = {};
|
||||
for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())];
|
||||
historyData[season] = statsMap;
|
||||
}
|
||||
}
|
||||
fetchData = false;
|
||||
|
||||
historyPlayerUsername.value = await teto.getNicknameByID(widget.searchFor);
|
||||
|
||||
return historyData;
|
||||
}
|
||||
|
||||
Future<List<_MyScatterSpot>> getTetraLeagueData(Stats x, Stats y) async {
|
||||
TetrioPlayersLeaderboard leaderboard = await teto.fetchTLLeaderboard();
|
||||
TLrelevance = leaderboard.timestamp;
|
||||
TLstatePlayers = leaderboard.leaderboard.length;
|
||||
List<_MyScatterSpot> _spots = [
|
||||
for (TetrioPlayerFromLeaderboard entry in leaderboard.leaderboard)
|
||||
if (excludeRanks.indexOf(entry.rank) == -1) _MyScatterSpot(
|
||||
entry.getStatByEnum(x).toDouble(),
|
||||
entry.getStatByEnum(y).toDouble(),
|
||||
entry.userId,
|
||||
entry.username,
|
||||
entry.rank,
|
||||
(rankColors[entry.rank]??Colors.white).withOpacity((searchLeague.isEmpty || entry.username.startsWith(searchLeague.toLowerCase())) ? 1.0 : 0.005)
|
||||
)
|
||||
];
|
||||
return _spots;
|
||||
}
|
||||
|
||||
bool? getTotalFilterValue(){
|
||||
if (excludeRanks.isEmpty) return true;
|
||||
if (excludeRanks.length == ranks.length) return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget getHistoryGraph(){
|
||||
return FutureBuilder<Map<int, Map<Stats, List<_HistoryChartSpot>>>>(
|
||||
future: getHistoryData(fetchData),
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState){
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasData){
|
||||
if (snapshot.data!.isEmpty || !snapshot.data!.containsKey(_season)) return ErrorThingy(eText: t.errors.notEnoughData);
|
||||
List<_HistoryChartSpot> selectedGraph = snapshot.data![_season]![Ychart]!;
|
||||
yAxisTitle = chartsShortTitles[Ychart]!;
|
||||
return SfCartesianChart(
|
||||
tooltipBehavior: _historyTooltipBehavior,
|
||||
zoomPanBehavior: _zoomPanBehavior,
|
||||
primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(),
|
||||
primaryYAxis: const NumericAxis(
|
||||
rangePadding: ChartRangePadding.additional,
|
||||
),
|
||||
margin: const EdgeInsets.all(0),
|
||||
series: <CartesianSeries>[
|
||||
if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>(
|
||||
enableTooltip: true,
|
||||
dataSource: selectedGraph,
|
||||
animationDuration: 0,
|
||||
opacity: _smooth ? 0 : 1,
|
||||
xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed,
|
||||
yValueMapper: (_HistoryChartSpot data, _) => data.stat,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
trendlines:<Trendline>[
|
||||
Trendline(
|
||||
isVisible: _smooth,
|
||||
period: (selectedGraph.length/175).floor(),
|
||||
type: TrendlineType.movingAverage,
|
||||
color: Theme.of(context).colorScheme.primary)
|
||||
],
|
||||
)
|
||||
else StepLineSeries<_HistoryChartSpot, DateTime>(
|
||||
enableTooltip: true,
|
||||
dataSource: selectedGraph,
|
||||
animationDuration: 0,
|
||||
opacity: _smooth ? 0 : 1,
|
||||
xValueMapper: (_HistoryChartSpot data, _) => data.timestamp,
|
||||
yValueMapper: (_HistoryChartSpot data, _) => data.stat,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
trendlines:<Trendline>[
|
||||
Trendline(
|
||||
isVisible: _smooth,
|
||||
period: (selectedGraph.length/175).floor(),
|
||||
type: TrendlineType.movingAverage,
|
||||
color: Theme.of(context).colorScheme.primary)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}else{ return FutureError(snapshot); }
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget getLeagueState (){
|
||||
return FutureBuilder<List<_MyScatterSpot>>(
|
||||
future: futureLeague,
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState){
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasData){
|
||||
return SfCartesianChart(
|
||||
tooltipBehavior: _tooltipBehavior,
|
||||
zoomPanBehavior: _zoomPanBehavior,
|
||||
//primaryXAxis: CategoryAxis(),
|
||||
series: [
|
||||
ScatterSeries(
|
||||
enableTooltip: true,
|
||||
dataSource: snapshot.data,
|
||||
animationDuration: 0,
|
||||
pointColorMapper: (data, _) => data.color,
|
||||
markerSettings: MarkerSettings(
|
||||
isVisible: false,
|
||||
borderColor: Colors.black,
|
||||
),
|
||||
xValueMapper: (data, _) => data.x,
|
||||
yValueMapper: (data, _) => data.y,
|
||||
onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: snapshot.data![point.pointIndex!].nickname))),
|
||||
)
|
||||
],
|
||||
);
|
||||
}else{ return FutureError(snapshot); }
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget getCutoffsHistory(){
|
||||
return FutureBuilder<List<Cutoffs>>(
|
||||
future: teto.fetchCutoffsHistory(),
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState){
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasData){
|
||||
yAxisTitle = chartsShortTitles[Ychart]!;
|
||||
return SfCartesianChart(
|
||||
tooltipBehavior: _leagueTooltipBehavior,
|
||||
zoomPanBehavior: _zoomPanBehavior,
|
||||
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>[
|
||||
for (String rank in ranks) StepLineSeries<Cutoffs, DateTime>(
|
||||
enableTooltip: true,
|
||||
dataSource: snapshot.data,
|
||||
animationDuration: 0,
|
||||
//opacity: 0.5,
|
||||
xValueMapper: (Cutoffs data, _) => data.ts,
|
||||
yValueMapper: (Cutoffs data, _) => switch (Ychart){
|
||||
Stats.glicko => data.glicko[rank],
|
||||
Stats.gxe => data.gxe[rank],
|
||||
_ => data.tr[rank]
|
||||
},
|
||||
color: rankColors[rank]!
|
||||
)
|
||||
],
|
||||
);
|
||||
}else{ return FutureError(snapshot); }
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
child: Wrap(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
if (graph == Graph.leagueState && TLstatePlayers != null && TLrelevance != null) Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.right,
|
||||
text: TextSpan(
|
||||
style: TextStyle(color: Colors.white, fontFamily: "Eurostile Round"),
|
||||
children: [
|
||||
TextSpan(text: t.stats.players(n: TLstatePlayers!)),
|
||||
TextSpan(text: "\n"),
|
||||
TextSpan(text: timestamp(TLrelevance!))
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (graph == Graph.history) Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.person),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: ValueListenableBuilder<String>(
|
||||
valueListenable: historyPlayerUsername,
|
||||
builder: (context, value, child) {
|
||||
return Text(value, style: TextStyle(fontSize: 22, fontFamily: "Eurostile Round Extended"));
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
if (graph == Graph.leagueState) SizedBox(
|
||||
width: 300,
|
||||
child: TextField(
|
||||
style: TextStyle(fontSize: 18.0000),
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(Icons.search),
|
||||
isDense: true
|
||||
),
|
||||
onChanged: (v){
|
||||
searchLeague = v;
|
||||
},
|
||||
onSubmitted: (v){
|
||||
searchLeague = v;
|
||||
setState((){futureLeague = getTetraLeagueData(_Xchart, Ychart);});
|
||||
},
|
||||
)
|
||||
),
|
||||
if (graph == Graph.history) Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(padding: EdgeInsets.all(8.0), child: Text("${t.season}:", style: TextStyle(fontSize: 22))),
|
||||
DropdownButton(
|
||||
items: [for (int i = 1; i <= currentSeason; i++) DropdownMenuItem(value: i-1, child: Text("$i"))],
|
||||
value: _season,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_season = value!;
|
||||
});
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
if (graph != Graph.leagueCutoffs) Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))),
|
||||
DropdownButton(
|
||||
items: switch (graph){
|
||||
Graph.history => [DropdownMenuItem(value: false, child: Text(t.graphsDestination.dateAndTime)), DropdownMenuItem(value: true, child: Text(t.stats.gp.full))],
|
||||
Graph.leagueState => _yAxis,
|
||||
Graph.leagueCutoffs => [],
|
||||
},
|
||||
value: graph == Graph.history ? _gamesPlayedInsteadOfDateAndTime : _Xchart,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (graph == Graph.history)
|
||||
_gamesPlayedInsteadOfDateAndTime = value! as bool;
|
||||
else{
|
||||
_Xchart = value! as Stats;
|
||||
setState((){futureLeague = getTetraLeagueData(_Xchart, Ychart);});
|
||||
}
|
||||
});
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))),
|
||||
DropdownButton<Stats>(
|
||||
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(() {
|
||||
Ychart = value!;
|
||||
futureLeague = getTetraLeagueData(_Xchart, Ychart);
|
||||
});
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
if (graph == Graph.history) Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Checkbox(value: _smooth,
|
||||
checkColor: Colors.black,
|
||||
onChanged: ((value) {
|
||||
setState(() {
|
||||
_smooth = value!;
|
||||
});
|
||||
})),
|
||||
Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22))
|
||||
],
|
||||
),
|
||||
if (graph == Graph.leagueState) IconButton(
|
||||
color: excludeRanks.isNotEmpty ? Theme.of(context).colorScheme.primary : null,
|
||||
onPressed: (){
|
||||
showDialog(context: context, builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, StateSetter setAlertState) {
|
||||
return AlertDialog(
|
||||
title: Text(t.graphsDestination.filterModaleTitle, textAlign: TextAlign.center),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(value: getTotalFilterValue(), tristate: true, title: Text(t.filterModale.all, style: TextStyle(fontFamily: "Eurostile Round Extended")), onChanged: (value){
|
||||
setAlertState(
|
||||
(){
|
||||
if (excludeRanks.length*2 > ranks.length){
|
||||
excludeRanks.clear();
|
||||
}else{
|
||||
excludeRanks = List.of(ranks);
|
||||
}
|
||||
}
|
||||
);
|
||||
}),
|
||||
for(String rank in ranks.reversed) CheckboxListTile(value: excludeRanks.indexOf(rank) == -1, onChanged: (value){
|
||||
setAlertState(
|
||||
(){
|
||||
if (excludeRanks.indexOf(rank) == -1){
|
||||
excludeRanks.add(rank);
|
||||
}else{
|
||||
excludeRanks.remove(rank);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, title: Text(rank.toUpperCase()),)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.actions.cancel),
|
||||
onPressed: () {Navigator.of(context).pop();}
|
||||
),
|
||||
TextButton(
|
||||
child: Text(t.actions.apply),
|
||||
onPressed: () {Navigator.of(context).pop(); setState((){futureLeague = getTetraLeagueData(_Xchart, Ychart);});}
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}, icon: Icon(Icons.filter_alt)),
|
||||
IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width - (widget.noSidebar ? 0 : 88),
|
||||
height: MediaQuery.of(context).size.height - 96,
|
||||
child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30),
|
||||
child: switch (graph){
|
||||
Graph.history => getHistoryGraph(),
|
||||
Graph.leagueState => getLeagueState(),
|
||||
Graph.leagueCutoffs => getCutoffsHistory()
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (!widget.noSidebar) SegmentedButton<Graph>(
|
||||
showSelectedIcon: false,
|
||||
segments: <ButtonSegment<Graph>>[
|
||||
ButtonSegment<Graph>(
|
||||
value: Graph.history,
|
||||
label: Text(t.graphsNavigation.history)),
|
||||
ButtonSegment<Graph>(
|
||||
value: Graph.leagueState,
|
||||
label: Text(t.graphsNavigation.league)),
|
||||
ButtonSegment<Graph>(
|
||||
value: Graph.leagueCutoffs,
|
||||
label: Text(t.graphsNavigation.cutoffs),
|
||||
),
|
||||
],
|
||||
selected: <Graph>{graph},
|
||||
onSelectionChanged: (Set<Graph> newSelection) {
|
||||
setState(() {
|
||||
graph = newSelection.first;
|
||||
switch (newSelection.first){
|
||||
case Graph.leagueCutoffs:
|
||||
case Graph.history:
|
||||
Ychart = Stats.tr;
|
||||
case Graph.leagueState:
|
||||
Ychart = Stats.apm;
|
||||
}
|
||||
});})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryChartSpot{
|
||||
final DateTime timestamp;
|
||||
final int gamesPlayed;
|
||||
final String rank;
|
||||
final double stat;
|
||||
const _HistoryChartSpot(this.timestamp, this.gamesPlayed, this.rank, this.stat);
|
||||
}
|
||||
|
||||
class _MyScatterSpot{
|
||||
num x;
|
||||
num y;
|
||||
String id;
|
||||
String nickname;
|
||||
String rank;
|
||||
Color color;
|
||||
_MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,110 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/open_in_browser.dart';
|
||||
import 'package:tetra_stats/views/about_view.dart';
|
||||
import 'package:tetra_stats/views/sprint_and_blitz_averages.dart';
|
||||
|
||||
class DestinationInfo extends StatefulWidget{
|
||||
final BoxConstraints constraints;
|
||||
|
||||
const DestinationInfo({super.key, required this.constraints});
|
||||
|
||||
@override
|
||||
State<DestinationInfo> createState() => _DestinationInfo();
|
||||
}
|
||||
|
||||
class InfoCard extends StatelessWidget {
|
||||
final double height;
|
||||
final double viewportWidth;
|
||||
final String assetLink;
|
||||
final String? assetLinkOnFocus;
|
||||
final String title;
|
||||
final String description;
|
||||
final void Function() onPressed;
|
||||
|
||||
const InfoCard({required this.height, this.viewportWidth = double.infinity, required this.assetLink, required this.title, required this.description, this.assetLinkOnFocus, required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: SizedBox(
|
||||
width: viewportWidth > 768.0 ? 450 : viewportWidth,
|
||||
height: viewportWidth > 768.0 ? height : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(assetLink, fit: BoxFit.cover, height: viewportWidth > 768.0 ? 300.0 : 150.0, width: viewportWidth > 768.0 ? null : viewportWidth),
|
||||
TextButton(child: Text(title, style: (viewportWidth > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall)!.copyWith(decoration: TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), textAlign: TextAlign.center), onPressed: onPressed),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(description),
|
||||
),
|
||||
if (viewportWidth > 768.0) Spacer()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _DestinationInfo extends State<DestinationInfo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> cards = [
|
||||
InfoCard(
|
||||
height: widget.constraints.maxHeight,
|
||||
viewportWidth: widget.constraints.maxWidth,
|
||||
assetLink: "res/images/info card 1.png",
|
||||
title: t.infoDestination.sprintAndBlitzAverages,
|
||||
description: "${t.infoDestination.sprintAndBlitzAveragesDescription}\n\n${t.sprintAndBlitsRelevance(date: DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(sprintAndBlitzRelevance))}",
|
||||
onPressed: (){
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => SprintAndBlitzView(),
|
||||
));
|
||||
}
|
||||
),
|
||||
InfoCard(
|
||||
height: widget.constraints.maxHeight,
|
||||
viewportWidth: widget.constraints.maxWidth,
|
||||
assetLink: "res/images/info card 2.png",
|
||||
title: t.infoDestination.tetraStatsWiki,
|
||||
description: t.infoDestination.tetraStatsWikiDescription,
|
||||
onPressed: (){
|
||||
launchInBrowser(Uri.https("github.com", "dan63047/TetraStats/wiki"));
|
||||
}
|
||||
),
|
||||
InfoCard(
|
||||
height: widget.constraints.maxHeight,
|
||||
viewportWidth: widget.constraints.maxWidth,
|
||||
assetLink: "res/images/info card 3.png",
|
||||
title: t.infoDestination.about,
|
||||
description: t.infoDestination.aboutDescription,
|
||||
onPressed: (){
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => AboutView(),
|
||||
));
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
child: Center(child: Text(t.infoDestination.title, style: widget.constraints.maxWidth > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall!.copyWith(height: 1.1))),
|
||||
),
|
||||
SizedBox(
|
||||
height: widget.constraints.maxWidth > 768.0 ? widget.constraints.maxHeight - 61 : widget.constraints.maxHeight - 170,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: widget.constraints.maxWidth > 768.0 ? Axis.horizontal : Axis.vertical,
|
||||
child: widget.constraints.maxWidth > 768.0 ? Row(children: cards) : Column(children: cards),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,418 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/utils/relative_timestamps.dart';
|
||||
import 'package:tetra_stats/views/user_view.dart';
|
||||
import 'package:tetra_stats/widgets/future_error.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class DestinationLeaderboards extends StatefulWidget{
|
||||
final BoxConstraints constraints;
|
||||
final bool noSidebar;
|
||||
|
||||
const DestinationLeaderboards({super.key, required this.constraints, required this.noSidebar});
|
||||
|
||||
@override
|
||||
State<DestinationLeaderboards> createState() => _DestinationLeaderboardsState();
|
||||
}
|
||||
|
||||
const double transformThreshold = 768.0;
|
||||
|
||||
enum Leaderboards{
|
||||
tl,
|
||||
fullTL,
|
||||
xp,
|
||||
ar,
|
||||
sprint,
|
||||
blitz,
|
||||
zenith,
|
||||
zenithex,
|
||||
}
|
||||
|
||||
class _DestinationLeaderboardsState extends State<DestinationLeaderboards> {
|
||||
//Duration postSeasonLeft = seasonStart.difference(DateTime.now());
|
||||
final Map<Leaderboards, String> leaderboards = {
|
||||
Leaderboards.tl: t.leaderboardsDestination.tl,
|
||||
Leaderboards.fullTL: t.leaderboardsDestination.fullTL,
|
||||
Leaderboards.xp: t.stats.xp.full,
|
||||
Leaderboards.ar: t.leaderboardsDestination.ar,
|
||||
Leaderboards.sprint: t.gamemodes["40l"]!,
|
||||
Leaderboards.blitz: t.gamemodes["blitz"]!,
|
||||
Leaderboards.zenith: t.gamemodes["zenith"]!,
|
||||
Leaderboards.zenithex: t.gamemodes["zenithex"]!,
|
||||
};
|
||||
Leaderboards _currentLb = Leaderboards.tl;
|
||||
final StreamController<List<dynamic>> _dataStreamController = StreamController<List<dynamic>>.broadcast();
|
||||
late final ScrollController _scrollController;
|
||||
Stream<List<dynamic>> get dataStream => _dataStreamController.stream;
|
||||
List<dynamic> list = [];
|
||||
bool _isFetchingData = false;
|
||||
bool _reachedTheEnd = false;
|
||||
List<String> _excludeRanks = [];
|
||||
bool _reverse = false;
|
||||
String? prisecter;
|
||||
List<DropdownMenuEntry> _countries = [for (MapEntry e in t.countries.entries) DropdownMenuEntry(value: e.key, label: e.value)];
|
||||
List<DropdownMenuEntry> _stats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuEntry(value: e.key, label: e.value)];
|
||||
String? _country;
|
||||
Stats stat = Stats.tr;
|
||||
int? fullTLlbPlayers;
|
||||
DateTime? fullTLlbTimestamp;
|
||||
|
||||
bool? getTotalFilterValue(){
|
||||
if (_excludeRanks.isEmpty) return true;
|
||||
if (_excludeRanks.length == ranks.length) return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _fetchData() async {
|
||||
if (_isFetchingData || _reachedTheEnd) {
|
||||
// Avoid fetching new data while already fetching
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_isFetchingData = true;
|
||||
setState(() {});
|
||||
TetrioPlayersLeaderboard? fullLB;
|
||||
|
||||
if (_currentLb == Leaderboards.fullTL){
|
||||
fullLB = await teto.fetchTLLeaderboard();
|
||||
fullTLlbPlayers = fullLB.leaderboard.length;
|
||||
fullTLlbTimestamp = fullLB.timestamp;
|
||||
_reachedTheEnd = true;
|
||||
}
|
||||
|
||||
final items = switch(_currentLb){
|
||||
Leaderboards.tl => await teto.fetchTetrioLeaderboard(prisecter: prisecter, country: _country),
|
||||
Leaderboards.fullTL => fullLB!.getStatRankingFromLB(stat, country: _country??""),
|
||||
Leaderboards.xp => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "xp", country: _country),
|
||||
Leaderboards.ar => await teto.fetchTetrioLeaderboard(prisecter: prisecter, lb: "ar", country: _country),
|
||||
Leaderboards.sprint => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, country: _country),
|
||||
Leaderboards.blitz => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "blitz", country: _country),
|
||||
Leaderboards.zenith => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenith", country: _country),
|
||||
Leaderboards.zenithex => await teto.fetchTetrioRecordsLeaderboard(prisecter: prisecter, lb: "zenithex", country: _country),
|
||||
};
|
||||
|
||||
if (_currentLb == Leaderboards.fullTL && _excludeRanks.isNotEmpty) items.removeWhere((e) => _excludeRanks.indexOf((e as TetrioPlayerFromLeaderboard).rank) != -1);
|
||||
if (items.isEmpty) _reachedTheEnd = true;
|
||||
list.addAll((_reverse && _currentLb == Leaderboards.fullTL) ? items.reversed : items);
|
||||
if (items.isNotEmpty){
|
||||
_dataStreamController.add(list);
|
||||
prisecter = list.last.prisecter.toString();
|
||||
} else{
|
||||
_dataStreamController.add([]);
|
||||
}
|
||||
|
||||
} 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 && _currentLb != Leaderboards.fullTL) {
|
||||
// When the last item is fully visible, load the next page.
|
||||
_fetchData();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget rightSide(double width){
|
||||
print(width);
|
||||
const double eukjsakjas = 450;
|
||||
TextStyle trailingStyle = TextStyle(fontSize: 28, fontFamily: width < eukjsakjas ? "Eurostile Round Condensed" : null);
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Card(
|
||||
child: StreamBuilder<List<dynamic>>(
|
||||
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: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
DropdownMenu(
|
||||
leadingIcon: Icon(Icons.public),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
isDense: true,
|
||||
),
|
||||
textStyle: TextStyle(fontSize: 14, height: 0.9),
|
||||
dropdownMenuEntries: _countries,
|
||||
initialSelection: "",
|
||||
onSelected: ((value) {
|
||||
_country = value as String?;
|
||||
list.clear();
|
||||
prisecter = null;
|
||||
_isFetchingData = false;
|
||||
_reachedTheEnd = false;
|
||||
setState((){_fetchData();});
|
||||
})
|
||||
),
|
||||
if (_currentLb == Leaderboards.fullTL) SizedBox(width: 5.0),
|
||||
if (_currentLb == Leaderboards.fullTL) DropdownMenu(
|
||||
leadingIcon: Icon(Icons.sort),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
isDense: true,
|
||||
),
|
||||
textStyle: TextStyle(fontSize: 14, height: 0.9),
|
||||
dropdownMenuEntries: _stats,
|
||||
initialSelection: stat,
|
||||
onSelected: ((value) {
|
||||
stat = value;
|
||||
list.clear();
|
||||
prisecter = null;
|
||||
_isFetchingData = false;
|
||||
_reachedTheEnd = false;
|
||||
setState((){_fetchData();});
|
||||
})
|
||||
),
|
||||
if (_currentLb == Leaderboards.fullTL) IconButton(
|
||||
color: _excludeRanks.isNotEmpty ? Theme.of(context).colorScheme.primary : null,
|
||||
onPressed: () async {
|
||||
await showDialog(context: context, builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, StateSetter setAlertState) {
|
||||
return AlertDialog(
|
||||
title: Text("Filter", textAlign: TextAlign.center),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(value: getTotalFilterValue(), tristate: true, title: Text("All", style: TextStyle(fontFamily: "Eurostile Round Extended")), onChanged: (value){
|
||||
setAlertState(
|
||||
(){
|
||||
if (_excludeRanks.length*2 > ranks.length){
|
||||
_excludeRanks.clear();
|
||||
}else{
|
||||
_excludeRanks = List.of(ranks);
|
||||
}
|
||||
}
|
||||
);
|
||||
}),
|
||||
for(String rank in ranks.reversed) CheckboxListTile(value: _excludeRanks.indexOf(rank) == -1, onChanged: (value){
|
||||
setAlertState(
|
||||
(){
|
||||
if (_excludeRanks.indexOf(rank) == -1){
|
||||
_excludeRanks.add(rank);
|
||||
}else{
|
||||
_excludeRanks.remove(rank);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, title: Text(rank.toUpperCase()),)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text("Apply"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
setState(() {
|
||||
_currentLb = Leaderboards.fullTL;
|
||||
list.clear();
|
||||
prisecter = null;
|
||||
_isFetchingData = false;
|
||||
_reachedTheEnd = false;
|
||||
_fetchData();
|
||||
});
|
||||
}, icon: Icon(Icons.filter_alt)),
|
||||
if (_currentLb == Leaderboards.fullTL) IconButton(
|
||||
color: _reverse ? Theme.of(context).colorScheme.primary : null,
|
||||
icon: Transform.rotate(angle: _reverse ? pi : 0.0, child: Icon(Icons.filter_list)),
|
||||
onPressed: (){
|
||||
_reverse = !_reverse;
|
||||
list.clear();
|
||||
prisecter = null;
|
||||
_isFetchingData = false;
|
||||
_reachedTheEnd = false;
|
||||
_fetchData();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
if (_currentLb == Leaderboards.fullTL && fullTLlbPlayers != null && fullTLlbTimestamp != null) Text("${t.stats.players(n: fullTLlbPlayers!)} • ${t.sprintAndBlitsRelevance(date: timestamp(fullTLlbTimestamp!))}"),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: list.length,
|
||||
prototypeItem: ListTile(
|
||||
leading: Text("0"),
|
||||
title: Text("ehhh...", style: TextStyle(fontSize: 22)),
|
||||
trailing: SizedBox(height: 36, width: 1),
|
||||
subtitle: const Text("eh...", style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index){
|
||||
return ListTile(
|
||||
leading: Text(intf.format(index+1)),
|
||||
title: Text(snapshot.data![index].username, style: TextStyle(fontSize: 22)),
|
||||
trailing: switch (_currentLb){
|
||||
Leaderboards.tl => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle),
|
||||
Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36)
|
||||
],
|
||||
),
|
||||
Leaderboards.fullTL => switch (stat) {
|
||||
Stats.tr => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${f2.format(snapshot.data![index].tr)} TR", style: trailingStyle),
|
||||
Image.asset("res/tetrio_tl_alpha_ranks/${snapshot.data![index].rank}.png", height: 36)
|
||||
],
|
||||
),
|
||||
Stats.gp => Text("${intf.format(snapshot.data![index].getStatByEnum(stat))} ${chartsShortTitles[stat]}", style: trailingStyle),
|
||||
Stats.gw => Text("${intf.format(snapshot.data![index].getStatByEnum(stat))} ${chartsShortTitles[stat]}", style: trailingStyle),
|
||||
Stats.apm => Text("${f2.format(snapshot.data![index].getStatByEnum(stat))} ${chartsShortTitles[stat]}", style: trailingStyle),
|
||||
Stats.pps => Text("${f2.format(snapshot.data![index].getStatByEnum(stat))} ${chartsShortTitles[stat]}", style: trailingStyle),
|
||||
Stats.vs => Text("${f2.format(snapshot.data![index].getStatByEnum(stat))} ${chartsShortTitles[stat]}", style: trailingStyle),
|
||||
_ => Text("${f4.format(snapshot.data![index].getStatByEnum(stat))} ${chartsShortTitles[stat]}", style: trailingStyle)
|
||||
},
|
||||
Leaderboards.xp => Text("LVL ${f2.format(snapshot.data![index].level)}", style: trailingStyle),
|
||||
Leaderboards.ar => Text("${intf.format(snapshot.data![index].ar)} AR", style: trailingStyle),
|
||||
Leaderboards.sprint => Text(getALittleBitMoreNormalTime(snapshot.data![index].stats.finalTime), style: trailingStyle),
|
||||
Leaderboards.blitz => Text(intf.format(snapshot.data![index].stats.score), style: trailingStyle),
|
||||
Leaderboards.zenith => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle),
|
||||
Leaderboards.zenithex => Text("${f2.format(snapshot.data![index].stats.zenith!.altitude)} m", style: trailingStyle)
|
||||
},
|
||||
subtitle: width >= eukjsakjas ? 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.fullTL => switch (stat) {
|
||||
Stats.tr => "${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",
|
||||
_ => "${f2.format(snapshot.data![index].tr)} TR, ${snapshot.data![index].rank.toUpperCase()} rank"
|
||||
},
|
||||
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 => "${snapshot.data?[index]?.stats?.finesse?.faults != null ? intf.format(snapshot.data![index].stats.finesse.faults) : "?"} FF, ${f2.format(snapshot.data![index].stats.kpp)} KPP, ${f2.format(snapshot.data![index].stats.kps)} KPS, ${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)) : null,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UserView(searchFor: snapshot.data![index].userId),
|
||||
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError){ return FutureError(snapshot); }
|
||||
}
|
||||
return Text("huh?");
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.constraints.maxWidth > transformThreshold ? 300.0 : widget.constraints.maxWidth,
|
||||
height: widget.constraints.maxHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Spacer(),
|
||||
Text(t.leaderboardsDestination.title, style: Theme.of(context).textTheme.headlineMedium!.copyWith(fontSize: 32)),
|
||||
Spacer()
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: leaderboards.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(leaderboards.values.elementAt(index)),
|
||||
trailing: Icon(Icons.arrow_right, color: _currentLb.index == index ? Colors.white : Colors.grey),
|
||||
subtitle: index == 1 ? Text(t.TLfullLBnote, style: TextStyle(color: Colors.grey, fontSize: 12)) : null,
|
||||
onTap: () {
|
||||
if (widget.constraints.maxWidth <= transformThreshold) Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: rightSide(widget.constraints.maxWidth)
|
||||
)
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
_currentLb = leaderboards.keys.elementAt(index);
|
||||
list.clear();
|
||||
prisecter = null;
|
||||
_reachedTheEnd = false;
|
||||
_fetchData();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.constraints.maxWidth > transformThreshold) rightSide(widget.constraints.maxWidth - 300 - (widget.noSidebar ? 0 : 88)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/views/state_view.dart';
|
||||
import 'package:tetra_stats/widgets/alpha_league_entry_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/future_error.dart';
|
||||
import 'package:tetra_stats/widgets/info_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class DestinationSavedData extends StatefulWidget{
|
||||
final BoxConstraints constraints;
|
||||
|
||||
const DestinationSavedData({super.key, required this.constraints});
|
||||
|
||||
@override
|
||||
State<DestinationSavedData> createState() => _DestinationSavedData();
|
||||
}
|
||||
|
||||
class _DestinationSavedData extends State<DestinationSavedData> {
|
||||
String? selectedID;
|
||||
|
||||
Future<(List<TetraLeague>, List<TetraLeague>, List<TetraLeagueAlphaRecord>)> getDataAbout(String id) async {
|
||||
return (await teto.getStates(id, season: currentSeason), await teto.getStates(id, season: 1), await teto.getTLMatchesbyPlayerID(id));
|
||||
}
|
||||
|
||||
Widget getTetraLeagueListTile(TetraLeague data){
|
||||
return ListTile(
|
||||
title: Text("${timestamp(data.timestamp)}"),
|
||||
subtitle: Text("${data.apm != null ? f2.format(data.apm) : "-.--"} ${t.stats.apm.short}, ${data.pps != null ? f2.format(data.pps) : "-.--"} ${t.stats.pps.short}, ${data.vs != null ? f2.format(data.vs) : "-.--"} ${t.stats.vs.short}, ${intf.format(data.gamesPlayed)} ${t.stats.gp.short}", style: TextStyle(color: Colors.grey)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${data.tr != -1.00 ? f2.format(data.tr) : "-.--"} ${t.stats.tr.short}", style: TextStyle(fontSize: 28)),
|
||||
Image.asset("res/tetrio_tl_alpha_ranks/${data.rank}.png", height: 36)
|
||||
],
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
teto.deleteState(data.id+data.timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.stateRemoved(date: timestamp(data.timestamp)))));
|
||||
}));
|
||||
},
|
||||
icon: Icon(Icons.delete_forever)
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StateView(state: data),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget rightSide(double width, bool hasSidebar){
|
||||
return SizedBox(
|
||||
width: width - (hasSidebar ? 80.0 : 0.00),
|
||||
child: selectedID != null ? FutureBuilder<(List<TetraLeague>, List<TetraLeague>, List<TetraLeagueAlphaRecord>)>(
|
||||
future: getDataAbout(selectedID!),
|
||||
builder: (context, snapshot) {
|
||||
switch(snapshot.connectionState){
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasError){ return FutureError(snapshot); }
|
||||
if (snapshot.hasData){
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
child: TabBar(
|
||||
labelStyle: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 28),
|
||||
labelColor: Theme.of(context).colorScheme.primary,
|
||||
tabs: [
|
||||
Tab(text: t.savedDataDestination.seasonTLstates(s: currentSeason)),
|
||||
Tab(text: t.savedDataDestination.seasonTLstates(s: 1)),
|
||||
Tab(text: t.savedDataDestination.TLrecords)
|
||||
]),
|
||||
),
|
||||
SizedBox(
|
||||
height: widget.constraints.maxHeight - 64,
|
||||
child: TabBarView(children: [
|
||||
ListView.builder(
|
||||
itemCount: snapshot.data!.$1.length,
|
||||
itemBuilder: (context, index) {
|
||||
return getTetraLeagueListTile(snapshot.data!.$1[index]);
|
||||
},),
|
||||
ListView.builder(
|
||||
itemCount: snapshot.data!.$2.length,
|
||||
itemBuilder: (context, index) {
|
||||
return getTetraLeagueListTile(snapshot.data!.$2[index]);
|
||||
},),
|
||||
ListView.builder(
|
||||
itemCount: snapshot.data!.$3.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AlphaLeagueEntryThingy(snapshot.data!.$3[index], selectedID!);
|
||||
},),
|
||||
]
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text("what?");
|
||||
}
|
||||
}
|
||||
) :
|
||||
InfoThingy(t.savedDataDestination.tip)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Map<String, String>>(
|
||||
future: teto.getAllPlayers(),
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState){
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasError){ return FutureError(snapshot); }
|
||||
if (snapshot.hasData){
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.constraints.maxWidth > 900.0 ? 350 : widget.constraints.maxWidth - (widget.constraints.maxWidth <= 768.0 ? 0 : 80),
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Center(child: Text(t.savedDataDestination.title, style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center)),
|
||||
),
|
||||
for (String id in snapshot.data!.keys) Card(
|
||||
child: ListTile(
|
||||
title: Text(snapshot.data![id]!),
|
||||
//subtitle: Text("NaN states, NaN TL records", style: TextStyle(color: Colors.grey)),
|
||||
onTap: () => setState(() {
|
||||
selectedID = id;
|
||||
if (widget.constraints.maxWidth <= 900.0) Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: rightSide(widget.constraints.maxWidth, false)
|
||||
)
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.constraints.maxWidth > 900.0) rightSide(widget.constraints.maxWidth - 350, true)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
return const Text("End of FutureBuilder<FetchResults>");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,686 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/utils/filesizes_converter.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/utils/relative_timestamps.dart';
|
||||
import 'package:tetra_stats/widgets/future_error.dart';
|
||||
|
||||
class DestinationSettings extends StatefulWidget{
|
||||
final BoxConstraints constraints;
|
||||
|
||||
const DestinationSettings({super.key, required this.constraints});
|
||||
|
||||
@override
|
||||
State<DestinationSettings> createState() => _DestinationSettings();
|
||||
}
|
||||
|
||||
enum SettingsCardMod{
|
||||
general,
|
||||
customization,
|
||||
database
|
||||
}
|
||||
|
||||
Map<SettingsCardMod, String> settingsCardTitles = {
|
||||
SettingsCardMod.general: t.settingsDestination.general,
|
||||
SettingsCardMod.customization: t.settingsDestination.customization,
|
||||
SettingsCardMod.database: t.settingsDestination.database
|
||||
};
|
||||
const EdgeInsets descriptionPadding = EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 8.0);
|
||||
|
||||
class _DestinationSettings extends State<DestinationSettings> with SingleTickerProviderStateMixin {
|
||||
SettingsCardMod mod = SettingsCardMod.general;
|
||||
List<DropdownMenuItem<AppLocale>> locales = <DropdownMenuItem<AppLocale>>[];
|
||||
String defaultNickname = t.settingsDestination.checking;
|
||||
String defaultID = "";
|
||||
Color pickerColor = Colors.cyanAccent;
|
||||
Color currentColor = Colors.cyanAccent;
|
||||
late bool oskKagariGimmick;
|
||||
late bool sheetbotRadarGraphs;
|
||||
late int ratingMode;
|
||||
late int timestampMode;
|
||||
late bool showPositions;
|
||||
late bool showAverages;
|
||||
late bool updateInBG;
|
||||
late AnimationController _defaultNicknameAnimController;
|
||||
late Animation _goodDefaultNicknameAnim;
|
||||
late Animation _badDefaultNicknameAnim;
|
||||
late Animation _defaultNicknameAnim = _goodDefaultNicknameAnim;
|
||||
double helperTextOpacity = 0;
|
||||
String helperText = t.settingsDestination.enterToSubmit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
// windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
// windowManager.setTitle("Tetra Stats: ${t.settings}");
|
||||
// }
|
||||
_defaultNicknameAnimController = AnimationController(
|
||||
value: 1.0,
|
||||
duration: Durations.extralong4,
|
||||
vsync: this,
|
||||
);
|
||||
_goodDefaultNicknameAnim = new ColorTween(
|
||||
begin: Colors.greenAccent,
|
||||
end: Colors.grey,
|
||||
).animate(new CurvedAnimation(
|
||||
parent: _defaultNicknameAnimController,
|
||||
curve: Easing.emphasizedAccelerate,
|
||||
//reverseCurve: Cubic(0,.99,.99,1.01)
|
||||
))..addStatusListener((status) {
|
||||
if (status.index == 3) setState((){helperText = t.settingsDestination.enterToSubmit; helperTextOpacity = 0;});
|
||||
});
|
||||
_badDefaultNicknameAnim = new ColorTween(
|
||||
begin: Colors.redAccent,
|
||||
end: Colors.grey,
|
||||
).animate(new CurvedAnimation(
|
||||
parent: _defaultNicknameAnimController,
|
||||
curve: Easing.emphasizedAccelerate,
|
||||
//reverseCurve: Cubic(0,.99,.99,1.01)
|
||||
))..addStatusListener((status) {
|
||||
if (status.index == 3) setState((){helperText = t.settingsDestination.enterToSubmit; helperTextOpacity = 0;});
|
||||
});
|
||||
_getPreferences();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
// if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void changeColor(Color color) {
|
||||
setState(() => pickerColor = color);
|
||||
}
|
||||
|
||||
void _getPreferences() {
|
||||
showPositions = prefs.getBool("showPositions") ?? false;
|
||||
showAverages = prefs.getBool("showAverages") ?? true;
|
||||
updateInBG = prefs.getBool("updateInBG") ?? false;
|
||||
oskKagariGimmick = prefs.getBool("oskKagariGimmick") ?? true;
|
||||
sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")?? false;
|
||||
ratingMode = prefs.getInt("ratingMode") ?? 0;
|
||||
timestampMode = prefs.getInt("timestampMode") ?? 0;
|
||||
_setDefaultNickname(prefs.getString("player")??"").then((v){setState((){});});
|
||||
defaultID = prefs.getString("playerID")??"";
|
||||
}
|
||||
|
||||
Future<bool> _setDefaultNickname(String n) async {
|
||||
if (n.isNotEmpty) {
|
||||
try {
|
||||
if (n.length > 16){
|
||||
defaultNickname = await teto.getNicknameByID(n);
|
||||
await prefs.setString('playerID', n);
|
||||
}else{
|
||||
TetrioPlayer player = await teto.fetchPlayer(n);
|
||||
defaultNickname = player.username;
|
||||
await prefs.setString('playerID', player.userId);
|
||||
}
|
||||
await prefs.setString('player', defaultNickname);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
defaultNickname = "dan63";
|
||||
await prefs.setString('player', "dan63");
|
||||
await prefs.setString('playerID', "6098518e3d5155e6ec429cdc");
|
||||
return true;
|
||||
}
|
||||
//setState(() {});
|
||||
}
|
||||
|
||||
Widget getGeneralSettings(){
|
||||
return Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Center(child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(t.settingsDestination.general, style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.account, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: SizedBox(width: 150.0, child: AnimatedBuilder(
|
||||
animation: _defaultNicknameAnim,
|
||||
builder: (context, child) {
|
||||
return Focus(
|
||||
onFocusChange: (value) {
|
||||
setState((){helperTextOpacity = ((value || helperText != t.settingsDestination.enterToSubmit)) ? 1 : 0;});
|
||||
},
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
hintText: defaultNickname,
|
||||
helper: AnimatedOpacity(
|
||||
opacity: helperTextOpacity,
|
||||
duration: Durations.long1,
|
||||
curve: Easing.standardDecelerate,
|
||||
child: Text(helperText, style: TextStyle(color: _defaultNicknameAnim.value, height: 0.2))
|
||||
),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
helperText = t.settingsDestination.checking;
|
||||
_setDefaultNickname(value).then((v) {
|
||||
_defaultNicknameAnim = v ? _goodDefaultNicknameAnim : _badDefaultNicknameAnim;
|
||||
_defaultNicknameAnimController.forward(from: 0);
|
||||
setState((){ helperText = v ? t.settingsDestination.done : t.settingsDestination.noSuchAccount;});
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.accountDescription),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text("Language", style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: DropdownButton(
|
||||
items: locales,
|
||||
value: LocaleSettings.currentLocale,
|
||||
onChanged: (value){
|
||||
LocaleSettings.setLocale(value!);
|
||||
if(value.languageCode == Platform.localeName.substring(0, 2)){
|
||||
prefs.remove('locale');
|
||||
}else{
|
||||
prefs.setString('locale', value.languageCode);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.languageDescription(languages: t.settingsDestination.languages(n: locales.length))),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.updateInTheBackground, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: Switch(value: updateInBG, onChanged: (bool value){
|
||||
prefs.setBool("updateInBG", value);
|
||||
setState(() {
|
||||
updateInBG = value;
|
||||
});
|
||||
})
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.updateInTheBackgroundDescription),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.compareStats, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: Switch(value: showAverages, onChanged: (bool value){
|
||||
prefs.setBool("showAverages", value);
|
||||
setState(() {
|
||||
showAverages = value;
|
||||
});
|
||||
}),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.compareStatsDescription),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
surfaceTintColor: Colors.redAccent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.showPosition, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: Switch(value: showPositions, onChanged: (bool value){
|
||||
prefs.setBool("showPositions", value);
|
||||
setState(() {
|
||||
showPositions = value;
|
||||
});
|
||||
}),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.showPositionDescription),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Widget getCustomizationSettings(){
|
||||
return Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Center(child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(t.settingsDestination.customization, style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.accentColor, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary), width: 25, height: 25),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.settingsDestination.accentColorModale),
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: changeColor,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
child: Text(t.actions.apply),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
context.findAncestorStateOfType<MyAppState>()?.setAccentColor(pickerColor);
|
||||
prefs.setInt("accentColor", pickerColor.value);
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
]));
|
||||
}
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.accentColorDescription),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.timestamps, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: DropdownButton(
|
||||
value: timestampMode,
|
||||
items: <DropdownMenuItem>[
|
||||
DropdownMenuItem(value: 0, child: Text(t.settingsDestination.timestampsAbsoluteGMT)),
|
||||
DropdownMenuItem(value: 1, child: Text(t.settingsDestination.timestampsAbsoluteLocalTime)),
|
||||
DropdownMenuItem(value: 2, child: Text(t.settingsDestination.timestampsRelative))
|
||||
],
|
||||
onChanged: (dynamic value){
|
||||
prefs.setInt("timestampMode", value);
|
||||
setState(() {
|
||||
timestampMode = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.timestampsDescriptionPart1(d: DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19)))),
|
||||
),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.timestampsDescriptionPart2(y: DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms().format(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19).toLocal()), r: relativeDateTime(DateTime.utc(2023, DateTime.july, 20, 21, 03, 19)))),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.sheetbotLikeGraphs, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){
|
||||
prefs.setBool("sheetbotRadarGraphs", value);
|
||||
setState(() {
|
||||
sheetbotRadarGraphs = value;
|
||||
});
|
||||
}),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.sheetbotLikeGraphsDescription),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.settingsDestination.oskKagariGimmick, style: Theme.of(context).textTheme.displayLarge),
|
||||
trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){
|
||||
prefs.setBool("oskKagariGimmick", value);
|
||||
setState(() {
|
||||
oskKagariGimmick = value;
|
||||
});
|
||||
}),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: descriptionPadding,
|
||||
child: Text(t.settingsDestination.oskKagariGimmickDescription),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget getDatabaseSettings(){
|
||||
return Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Center(child: Column(
|
||||
children: [
|
||||
Text(t.settingsDestination.database, style: Theme.of(context).textTheme.titleLarge),
|
||||
Divider(),
|
||||
FutureBuilder<(int, int, int)>(future: teto.getDatabaseData(),
|
||||
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 RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white),
|
||||
children: [
|
||||
TextSpan(text: "${bytesToSize(snapshot.data!.$1)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)),
|
||||
TextSpan(text: "${t.settingsDestination.bytesOfDataStored}\n"),
|
||||
TextSpan(text: "${intf.format(snapshot.data!.$2)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)),
|
||||
TextSpan(text: "${t.settingsDestination.TLrecordsSaved}\n"),
|
||||
TextSpan(text: "${intf.format(snapshot.data!.$3)} ", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)),
|
||||
TextSpan(text: t.settingsDestination.TLplayerstatesSaved),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError){ return FutureError(snapshot); }
|
||||
}
|
||||
return Text("huh?");
|
||||
}
|
||||
),
|
||||
Divider(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: (){teto.removeDuplicatesFromTLMatches().then((_) => setState((){}));},
|
||||
icon: const Icon(Icons.build),
|
||||
label: Text(t.settingsDestination.fixButton),
|
||||
style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0)))))
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: (){teto.compressDB().then((_) => setState((){}));},
|
||||
icon: const Icon(Icons.compress),
|
||||
label: Text(t.settingsDestination.compressButton),
|
||||
style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0)))))
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)),
|
||||
),
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: Text(t.settingsDestination.exportDB, style: Theme.of(context).textTheme.displayLarge),
|
||||
onTap: () {
|
||||
if (kIsWeb){
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.notForWeb)));
|
||||
} else if (Platform.isAndroid){
|
||||
var downloadFolder = Directory("/storage/emulated/0/Download");
|
||||
File exportedDB = File("${downloadFolder.path}/TetraStats.db");
|
||||
getApplicationDocumentsDirectory().then((value) {
|
||||
exportedDB.writeAsBytes(File("${value.path}/TetraStats.db").readAsBytesSync());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.settingsDestination.androidExportAlertTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: "Eurostile Round Extended")),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(children: [Text(t.settingsDestination.androidExportText(exportedDB: exportedDB))]),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.actions.ok),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
});
|
||||
} else if (Platform.isLinux || Platform.isWindows) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.settingsDestination.desktopExportAlertTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: "Eurostile Round Extended")),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(children: [
|
||||
Text(t.settingsDestination.desktopExportText)
|
||||
]),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.actions.ok),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: Text(t.settingsDestination.importDB, style: Theme.of(context).textTheme.displayLarge),
|
||||
onTap: (){
|
||||
if (kIsWeb){
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.notForWeb)));
|
||||
}else if(Platform.isAndroid){
|
||||
FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
).then((value){
|
||||
if (value != null){
|
||||
var newDB = value.paths[0]!;
|
||||
teto.checkImportingDB(File(newDB)).then((v){
|
||||
if (v){
|
||||
teto.close().then((value){
|
||||
getApplicationDocumentsDirectory().then((value){
|
||||
var oldDB = File("${value.path}/TetraStats.db");
|
||||
oldDB.writeAsBytes(File(newDB).readAsBytesSync()).then((value){
|
||||
teto.open();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.importSuccess)));
|
||||
});
|
||||
});
|
||||
});
|
||||
}else{
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Import Failed: Wrong database sheme")));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.importCancelled)));
|
||||
}
|
||||
});
|
||||
}else{
|
||||
const XTypeGroup typeGroup = XTypeGroup(
|
||||
label: 'Tetra Stats Database',
|
||||
extensions: <String>['db'],
|
||||
);
|
||||
openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]).then((value){
|
||||
if (value != null){
|
||||
var newDB = value.path;
|
||||
teto.checkImportingDB(File(newDB)).then((v){
|
||||
if (v){
|
||||
teto.close().then((value){
|
||||
getApplicationDocumentsDirectory().then((value){
|
||||
var oldDB = File("${value.path}/TetraStats.db");
|
||||
oldDB.writeAsBytes(File(newDB).readAsBytesSync()).then((value){
|
||||
teto.open();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.importSuccess)));
|
||||
});
|
||||
});
|
||||
});
|
||||
}else{
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Import Failed: Wrong database sheme")));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.importCancelled)));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget rightSide(double width, bool hasSidebar){
|
||||
return SizedBox(
|
||||
width: width - (hasSidebar ? 80 : 0),
|
||||
child: SingleChildScrollView(
|
||||
child: switch (mod){
|
||||
SettingsCardMod.general => getGeneralSettings(),
|
||||
SettingsCardMod.customization => getCustomizationSettings(),
|
||||
SettingsCardMod.database => getDatabaseSettings(),
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
if (locales.isEmpty) for (var v in AppLocale.values){
|
||||
locales.add(DropdownMenuItem<AppLocale>(
|
||||
value: v, child: Text(t.locales[v.languageTag]!)));
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.constraints.maxWidth > 900.0 ? 350 : widget.constraints.maxWidth - (widget.constraints.maxWidth <= 768.0 ? 0 : 80),
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Spacer(),
|
||||
Text(t.settingsDestination.title, style: Theme.of(context).textTheme.headlineMedium),
|
||||
Spacer()
|
||||
],
|
||||
),
|
||||
),
|
||||
for (SettingsCardMod m in SettingsCardMod.values) Card(
|
||||
child: ListTile(
|
||||
title: Text(settingsCardTitles[m]!),
|
||||
trailing: Icon(Icons.arrow_right, color: mod == m ? Colors.white : Colors.grey),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
mod = m;
|
||||
});
|
||||
if (widget.constraints.maxWidth <= 900.0) Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: rightSide(widget.constraints.maxWidth, false)
|
||||
)
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.constraints.maxWidth > 900.0) rightSide(widget.constraints.maxWidth - 350, true)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_player.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
|
||||
class FirstTimeView extends StatefulWidget {
|
||||
/// The very first view, that user see when he launch this programm.
|
||||
const FirstTimeView({super.key});
|
||||
|
||||
@override
|
||||
State<FirstTimeView> createState() => _FirstTimeState();
|
||||
}
|
||||
|
||||
class _FirstTimeState extends State<FirstTimeView> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animController;
|
||||
late final Animation<double> _spinAnimation;
|
||||
late Animation<double> _opacity;
|
||||
late Animation<double> _enterNicknameOpacity;
|
||||
late Animation<double> _transform;
|
||||
late Animation<Color?> _badNicknameAnim;
|
||||
late Animation<double> _fadeOutOpacity;
|
||||
late TextEditingController _controller;
|
||||
String title = t.firstTimeView.welcome;
|
||||
String subtitle = t.firstTimeView.description;
|
||||
String helperText = "";
|
||||
String nickname = "";
|
||||
double helperTextOpacity = 0;
|
||||
bool userSet = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_animController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Durations.extralong2
|
||||
);
|
||||
_spinAnimation = Tween<double>(
|
||||
begin: -0.3,
|
||||
end: 0.0000,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animController,
|
||||
curve: Interval(
|
||||
0.0,
|
||||
0.5,
|
||||
curve: Curves.linearToEaseOut,
|
||||
)
|
||||
));
|
||||
_badNicknameAnim = new ColorTween(
|
||||
begin: Colors.redAccent,
|
||||
end: Colors.grey,
|
||||
).animate(new CurvedAnimation(
|
||||
parent: _animController,
|
||||
curve: const Interval(
|
||||
0.5,
|
||||
0.75,
|
||||
curve: Curves.easeInCubic
|
||||
),
|
||||
));
|
||||
_opacity = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animController,
|
||||
curve: const Interval(
|
||||
0.0,
|
||||
0.5,
|
||||
curve: Curves.linear,
|
||||
),
|
||||
),
|
||||
);
|
||||
_enterNicknameOpacity = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animController,
|
||||
curve: const Interval(
|
||||
0.75,
|
||||
0.9,
|
||||
curve: Curves.ease,
|
||||
),
|
||||
),
|
||||
);
|
||||
_transform = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 150.0
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animController,
|
||||
curve: const Interval(
|
||||
0.75,
|
||||
0.9,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
_fadeOutOpacity = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animController,
|
||||
curve: const Interval(
|
||||
0.9,
|
||||
1.0,
|
||||
curve: Curves.ease,
|
||||
),
|
||||
),
|
||||
);
|
||||
_controller = TextEditingController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
_animController.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<bool> _setDefaultNickname(String n) async {
|
||||
setState((){
|
||||
helperTextOpacity = 1;
|
||||
_animController.value = 0.75;
|
||||
helperText = t.settingsDestination.checking;
|
||||
});
|
||||
if (n.isNotEmpty) {
|
||||
try {
|
||||
if (n.length > 16){
|
||||
nickname = await teto.getNicknameByID(n);
|
||||
await prefs.setString('playerID', n);
|
||||
}else{
|
||||
TetrioPlayer player = await teto.fetchPlayer(n);
|
||||
nickname = player.username;
|
||||
await prefs.setString('playerID', player.userId);
|
||||
if(!(await teto.isPlayerTracking(player.userId))) await teto.addPlayerToTrack(player);
|
||||
}
|
||||
await prefs.setString('player', nickname);
|
||||
await prefs.setBool("notFirstTime", true);
|
||||
helperText = "";
|
||||
_animController.animateTo(0.9);
|
||||
setState((){
|
||||
userSet = true;
|
||||
title = t.firstTimeView.niceToSeeYou(n: nickname);
|
||||
subtitle = t.firstTimeView.letsTakeALook;
|
||||
});
|
||||
Timer(Duration(seconds: 2), () => _animController.animateTo(1.0, duration: Duration(seconds: 1)));
|
||||
Timer(Duration(seconds: 3), () => context.replace("/"));
|
||||
return true;
|
||||
} catch (e) {
|
||||
_animController.value = 0.5;
|
||||
_animController.animateTo(0.75, duration: Duration(seconds: 1));
|
||||
setState((){
|
||||
helperText = t.settingsDestination.noSuchAccount;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
_animController.value = 0.5;
|
||||
_animController.animateTo(0.75, duration: Durations.long1);
|
||||
setState((){
|
||||
helperText = t.firstTimeView.emptyInputError;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAnimation(BuildContext context, Widget? child) {
|
||||
return Center(
|
||||
child: Container(
|
||||
transform: Matrix4.translationValues(0, _transform.value, 0),
|
||||
child: Opacity(
|
||||
opacity: _fadeOutOpacity.value,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: RotationTransition(
|
||||
turns: _spinAnimation,
|
||||
child: Image.asset("res/icons/app.png", height: 128, opacity: _opacity)
|
||||
),
|
||||
),
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(subtitle, style: TextStyle(color: Colors.grey)),
|
||||
Opacity(
|
||||
opacity: _enterNicknameOpacity.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(t.firstTimeView.nicknameQuestion, style: Theme.of(context).textTheme.titleSmall),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: SizedBox(width: 400.0, child: Focus(
|
||||
onFocusChange: (value) {
|
||||
setState((){if (value) helperTextOpacity = 0;});
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
maxLength: 16,
|
||||
textAlign: TextAlign.center,
|
||||
enabled: !userSet,
|
||||
decoration: InputDecoration(
|
||||
hintText: t.firstTimeView.inpuntHint,
|
||||
helper: Opacity(
|
||||
opacity: helperTextOpacity,
|
||||
child: Text(helperText, style: TextStyle(fontFamily: "Eurostile Round", color: _badNicknameAnim.value, height: 0.5))
|
||||
),
|
||||
counter: const Offstage()
|
||||
),
|
||||
onSubmitted: (value) => _setDefaultNickname(value),
|
||||
),
|
||||
)),
|
||||
),
|
||||
ElevatedButton.icon(onPressed: !userSet ? () => _setDefaultNickname(_controller.value.text) : null, icon: Icon(Icons.subdirectory_arrow_left), label: Text(t.actions.submit))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Spacer(flex: 2),
|
||||
TextButton(onPressed: (){ context.replace("/"); }, child: Text(t.firstTimeView.skip))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: TweenAnimationBuilder(
|
||||
onEnd: (){
|
||||
_animController.animateTo(0.75);
|
||||
},
|
||||
duration: Durations.long4,
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
curve: Easing.standard,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
transform: Matrix4.translationValues(0, 600-value*600, 0),
|
||||
child: Opacity(opacity: value, child: child),
|
||||
);
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: _animController,
|
||||
builder: _buildAnimation
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,84 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/main.dart' show teto;
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
late String oldWindowTitle;
|
||||
|
||||
class MatchesView extends StatefulWidget {
|
||||
final String userID;
|
||||
final String username;
|
||||
const MatchesView({super.key, required this.userID, required this.username});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => MatchesState();
|
||||
}
|
||||
|
||||
class MatchesState extends State<MatchesView> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${t.matchesViewTitle(nickname: widget.username)}");
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
bool bigScreen = MediaQuery.of(context).size.width > 768;
|
||||
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.matchesViewTitle(nickname: widget.username)),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: FutureBuilder(
|
||||
future: teto.getTLMatchesbyPlayerID(widget.userID),
|
||||
builder: (context, snapshot){
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator(color: Colors.white));
|
||||
case ConnectionState.done:
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: (snapshot.data!.isNotEmpty)
|
||||
? [for (var value in snapshot.data!) ListTile(
|
||||
leading: Text("${value.endContext.firstWhere((element) => element.userId == widget.userID).points} : ${value.endContext.firstWhere((element) => element.userId != widget.userID).points}",
|
||||
style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) :
|
||||
const TextStyle(fontSize: 28)),
|
||||
title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != widget.userID).username}"),
|
||||
subtitle: Text(dateFormat.format(value.timestamp)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_forever),
|
||||
onPressed: () {
|
||||
DateTime nn = value.timestamp;
|
||||
teto.deleteTLMatch(value.ownId).then((value) => setState(() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.matchRemoved(date: dateFormat.format(nn)))));
|
||||
}));
|
||||
},
|
||||
),
|
||||
)]
|
||||
: [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))],
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,552 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/views/main_view.dart' show MainView;
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
var _chartsShortTitlesDropdowns = <DropdownMenuItem>[for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)];
|
||||
Stats _chartsX = Stats.tr;
|
||||
Stats _chartsY = Stats.apm;
|
||||
late TooltipBehavior _tooltipBehavior;
|
||||
late ZoomPanBehavior _zoomPanBehavior;
|
||||
List<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
|
||||
List<_MyScatterSpot> _spots = [];
|
||||
Stats _sortBy = Stats.tr;
|
||||
late List<TetrioPlayerFromLeaderboard> they;
|
||||
bool _reversed = false;
|
||||
List<DropdownMenuItem> _itemCountries = [for (MapEntry e in t.countries.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
|
||||
String _country = "";
|
||||
late String _oldWindowTitle;
|
||||
final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2);
|
||||
final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4);
|
||||
|
||||
class RankView extends StatefulWidget {
|
||||
final List rank;
|
||||
const RankView({super.key, required this.rank});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => RankState();
|
||||
}
|
||||
|
||||
class RankState extends State<RankView> with SingleTickerProviderStateMixin {
|
||||
late ScrollController _scrollController;
|
||||
late TabController _tabController;
|
||||
late String previousAxisTitles;
|
||||
late double minX;
|
||||
late double actualMinX;
|
||||
late double maxX;
|
||||
late double actualMaxX;
|
||||
late double minY;
|
||||
late double actualMinY;
|
||||
late double maxY;
|
||||
late double actualMaxY;
|
||||
late double xScale;
|
||||
late double yScale;
|
||||
String headerTooltip = t.pseudoTooltipHeaderInit;
|
||||
String footerTooltip = t.pseudoTooltipFooterInit;
|
||||
ValueNotifier<int> hoveredPointId = ValueNotifier<int>(-1);
|
||||
double scaleFactor = 5e2;
|
||||
double dragFactor = 7e2;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_scrollController = ScrollController();
|
||||
_tabController = TabController(length: 6, vsync: this);
|
||||
_zoomPanBehavior = ZoomPanBehavior(
|
||||
enablePinching: true,
|
||||
enableSelectionZooming: true,
|
||||
enableMouseWheelZooming : true,
|
||||
enablePanning: true,
|
||||
);
|
||||
_tooltipBehavior = TooltipBehavior(
|
||||
color: Colors.black,
|
||||
borderColor: Colors.white,
|
||||
enable: true,
|
||||
animationDuration: 0,
|
||||
builder: (dynamic data, dynamic point, dynamic series,
|
||||
int pointIndex, int seriesIndex) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"${data.nickname} (${data.rank.toUpperCase()})",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 20),
|
||||
),
|
||||
),
|
||||
Text('${_f4.format(data.x)} ${chartsShortTitles[_chartsX]}\n${_f4.format(data.y)} ${chartsShortTitles[_chartsY]}')
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => _oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}");
|
||||
}
|
||||
super.initState();
|
||||
previousAxisTitles = _chartsX.toString()+_chartsY.toString();
|
||||
they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
|
||||
createSpots();
|
||||
}
|
||||
|
||||
void createSpots(){
|
||||
_spots = [
|
||||
for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"])
|
||||
if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception
|
||||
_MyScatterSpot(
|
||||
entry.getStatByEnum(_chartsX).toDouble(),
|
||||
entry.getStatByEnum(_chartsY).toDouble(),
|
||||
entry.userId,
|
||||
entry.username,
|
||||
entry.rank,
|
||||
rankColors[entry.rank]??Colors.white
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_scrollController.dispose();
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(_oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _justUpdate() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool bigScreen = MediaQuery.of(context).size.width > 768;
|
||||
if (previousAxisTitles != _chartsX.toString()+_chartsY.toString()){
|
||||
createSpots();
|
||||
previousAxisTitles = _chartsX.toString()+_chartsY.toString();
|
||||
}
|
||||
final t = Translations.of(context);
|
||||
//they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (context, value) {
|
||||
return [ SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
Flex(
|
||||
direction: Axis.vertical,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [Image.asset("res/tetrio_tl_alpha_ranks/${widget.rank[0].rank}.png",fit: BoxFit.fitHeight,height: 128), ],
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase()),
|
||||
style: TextStyle(
|
||||
fontFamily: "Eurostile Round Extended",
|
||||
fontSize: bigScreen ? 42 : 28)),
|
||||
Text(
|
||||
t.players(n: widget.rank[1]["entries"].length),
|
||||
style: TextStyle(
|
||||
fontFamily: "Eurostile Round Extended",
|
||||
fontSize: bigScreen ? 42 : 28)),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
)),
|
||||
SliverToBoxAdapter(
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(text: t.chart),
|
||||
Tab(text: t.entries),
|
||||
Tab(text: t.minimums),
|
||||
Tab(text: t.averages),
|
||||
Tab(text: t.maximums),
|
||||
Tab(text: t.other),
|
||||
],
|
||||
)),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.end,
|
||||
spacing: 20,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text("X:", style: TextStyle(fontSize: 22))),
|
||||
DropdownButton(
|
||||
items: _chartsShortTitlesDropdowns,
|
||||
value: _chartsX,
|
||||
onChanged: (value) {
|
||||
_chartsX = value;
|
||||
_justUpdate();
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text("Y:", style: TextStyle(fontSize: 22)),
|
||||
),
|
||||
DropdownButton(
|
||||
items: _chartsShortTitlesDropdowns,
|
||||
value: _chartsY,
|
||||
onChanged: (value) {
|
||||
_chartsY = value;
|
||||
_justUpdate();
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,)
|
||||
],
|
||||
),
|
||||
if (widget.rank[1]["entries"].length > 1)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height - 104,
|
||||
child: Padding(
|
||||
padding: bigScreen ? const EdgeInsets.fromLTRB(40, 10, 40, 20) : const EdgeInsets.fromLTRB(0, 10, 16, 20),
|
||||
child: Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerSignal: (signal) {
|
||||
if (signal is PointerScrollEvent) {
|
||||
setState(() {
|
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView
|
||||
});
|
||||
}
|
||||
},
|
||||
child: SfCartesianChart(
|
||||
tooltipBehavior: _tooltipBehavior,
|
||||
zoomPanBehavior: _zoomPanBehavior,
|
||||
//primaryXAxis: CategoryAxis(),
|
||||
series: [
|
||||
ScatterSeries(
|
||||
enableTooltip: true,
|
||||
dataSource: _spots,
|
||||
animationDuration: 0,
|
||||
pointColorMapper: (data, _) => data.color,
|
||||
xValueMapper: (data, _) => data.x,
|
||||
yValueMapper: (data, _) => data.y,
|
||||
onPointTap: (point) => Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: _spots[point.pointIndex!].nickname), maintainState: false)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text("${t.sortBy}: ", style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||
DropdownButton(
|
||||
items: _itemStats,
|
||||
value: _sortBy,
|
||||
onChanged: ((value) {
|
||||
_sortBy = value;
|
||||
setState(() {
|
||||
they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
|
||||
});
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text("${t.reversed}: ", style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||
Padding(padding: const EdgeInsets.fromLTRB(0, 5.5, 0, 7.5),
|
||||
child: Checkbox(
|
||||
value: _reversed,
|
||||
checkColor: Colors.black,
|
||||
onChanged: ((value) {
|
||||
_reversed = value!;
|
||||
setState(() {
|
||||
they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text("${t.country}: ", style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||
DropdownButton(
|
||||
items: _itemCountries,
|
||||
value: _country,
|
||||
onChanged: ((value) {
|
||||
_country = value;
|
||||
setState(() {
|
||||
they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
|
||||
});
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: they.length,
|
||||
itemBuilder: (context, index) {
|
||||
bool bigScreen = MediaQuery.of(context).size.width > 768;
|
||||
return ListTile(
|
||||
title: Text(they[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
|
||||
subtitle: Text(
|
||||
_sortBy == Stats.tr ? "${_f2.format(they[index].apm)} APM, ${_f2.format(they[index].pps)} PPS, ${_f2.format(they[index].vs)} VS, ${_f2.format(they[index].nerdStats.app)} APP, ${_f2.format(they[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(they[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${_f2.format(they[index].tr)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null),
|
||||
Image.asset("res/tetrio_tl_alpha_ranks/${they[index].rank}.png", height: bigScreen ? 48 : 16),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: they[index].username), maintainState: false));
|
||||
},
|
||||
);
|
||||
}),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(t.lowestValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
_ListEntry(value: widget.rank[1]["lowestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestTRid"], username: widget.rank[1]["lowestTRnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestGlixare"], label: "Glixare", id: widget.rank[1]["lowestGlixareID"], username: widget.rank[1]["lowestGlixareNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["lowestS1trID"], username: widget.rank[1]["lowestS1trNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestGlicko"], label: "Glicko", id: widget.rank[1]["lowestGlickoID"], username: widget.rank[1]["lowestGlickoNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestRdID"], username: widget.rank[1]["lowestRdNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGamesPlayedID"], username: widget.rank[1]["lowestGamesPlayedNick"], approximate: false),
|
||||
_ListEntry(value: widget.rank[1]["lowestGamesWon"], label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGamesWonID"], username: widget.rank[1]["lowestGamesWonNick"], approximate: false),
|
||||
_ListEntry(value: widget.rank[1]["lowestWinrate"] * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestWinrateID"], username: widget.rank[1]["lowestWinrateNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestAPM"], label: t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPMid"], username: widget.rank[1]["lowestAPMnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestPPS"], label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestPPSid"], username: widget.rank[1]["lowestPPSnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestVS"], label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestVSid"], username: widget.rank[1]["lowestVSnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPPid"], username: widget.rank[1]["lowestAPPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestVSAPM"], label: "VS / APM", id: widget.rank[1]["lowestVSAPMid"], username: widget.rank[1]["lowestVSAPMnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestDSSid"], username: widget.rank[1]["lowestDSSnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestDSPid"], username: widget.rank[1]["lowestDSPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAPPDSPid"], username: widget.rank[1]["lowestAPPDSPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestCheeseID"], username: widget.rank[1]["lowestCheeseNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestGBEid"], username: widget.rank[1]["lowestGBEnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestNyaAPPid"], username: widget.rank[1]["lowestNyaAPPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestArea"], label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestAreaID"], username: widget.rank[1]["lowestAreaNick"], approximate: false, fractionDigits: 1),
|
||||
_ListEntry(value: widget.rank[1]["lowestEstTR"], label: t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestEstTRid"], username: widget.rank[1]["lowestEstTRnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["lowestEstAcc"], label: t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["lowestEstAccID"], username: widget.rank[1]["lowestEstAccNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestOpener"], label: "Opener", id: widget.rank[1]["lowestOpenerID"], username: widget.rank[1]["lowestOpenerNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestPlonk"], label: "Plonk", id: widget.rank[1]["lowestPlonkID"], username: widget.rank[1]["lowestPlonkNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestStride"], label: "Stride", id: widget.rank[1]["lowestStrideID"], username: widget.rank[1]["lowestStrideNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["lowestInfDS"], label: "Inf. DS", id: widget.rank[1]["lowestInfDSid"], username: widget.rank[1]["lowestInfDSnick"], approximate: false, fractionDigits: 3)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(t.averageValues, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
|
||||
Expanded(
|
||||
child: ListView(children: [
|
||||
_ListEntry(value: widget.rank[0].tr, label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[0].gxe, label: "Glixare", id: "", username: "", approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[0].s1tr, label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: "", username: "", approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[0].glicko, label: "Glicko", id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[0].rd, label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[0].gamesPlayed, label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 0),
|
||||
_ListEntry(value: widget.rank[0].gamesWon, label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 0),
|
||||
_ListEntry(value: widget.rank[0].winrate * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[0].apm, label: t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[0].pps, label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[0].vs, label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["avgAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgVSAPM"], label: "VS / APM", id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["avgGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgArea"], label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 1),
|
||||
_ListEntry(value: widget.rank[1]["avgEstTR"], label: t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["avgEstAcc"], label: t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgOpener"], label: "Opener", id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgPlonk"], label: "Plonk", id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgStride"], label: "Stride", id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["avgInfDS"], label: "Inf. DS", id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
]))
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(t.highestValues, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
_ListEntry(value: widget.rank[1]["highestTR"], label: t.statCellNum.tr.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestTRid"], username: widget.rank[1]["highestTRnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestGlixare"], label: "Glixare", id: widget.rank[1]["highestGlixareID"], username: widget.rank[1]["highestGlixareNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestS1tr"], label: "S1 ${t.statCellNum.tr.replaceAll(RegExp(r'\n'), " ")}", id: widget.rank[1]["highestS1trID"], username: widget.rank[1]["highestS1trNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestGlicko"], label: "Glicko", id: widget.rank[1]["highestGlickoID"], username: widget.rank[1]["highestGlickoNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestRD"], label: t.statCellNum.rd.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestRdID"], username: widget.rank[1]["highestRdNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestGamesPlayed"], label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGamesPlayedID"], username: widget.rank[1]["highestGamesPlayedNick"], approximate: false),
|
||||
_ListEntry(value: widget.rank[1]["highestGamesWon"], label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGamesWonID"], username: widget.rank[1]["highestGamesWonNick"], approximate: false),
|
||||
_ListEntry(value: widget.rank[1]["highestWinrate"] * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestWinrateID"], username: widget.rank[1]["highestWinrateNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestAPM"], label: t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPMid"], username: widget.rank[1]["highestAPMnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestPPS"], label: t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestPPSid"], username: widget.rank[1]["highestPPSnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestVS"], label: t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestVSid"], username: widget.rank[1]["highestVSnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestAPP"], label: t.statCellNum.app.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPPid"], username: widget.rank[1]["highestAPPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestVSAPM"], label: "VS / APM", id: widget.rank[1]["highestVSAPMid"], username: widget.rank[1]["highestVSAPMnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestDSS"], label: t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestDSSid"], username: widget.rank[1]["highestDSSnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestDSP"], label: t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestDSPid"], username: widget.rank[1]["highestDSPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestAPPDSP"], label: t.statCellNum.appdsp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAPPDSPid"], username: widget.rank[1]["highestAPPDSPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestCheese"], label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestCheeseID"], username: widget.rank[1]["highestCheeseNick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestGBE"], label: t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestGBEid"], username: widget.rank[1]["highestGBEnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestNyaAPP"], label: t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestNyaAPPid"], username: widget.rank[1]["highestNyaAPPnick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestArea"], label: t.statCellNum.area.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestAreaID"], username: widget.rank[1]["highestAreaNick"], approximate: false, fractionDigits: 1),
|
||||
_ListEntry(value: widget.rank[1]["highestEstTR"], label: t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestEstTRid"], username: widget.rank[1]["highestEstTRnick"], approximate: false, fractionDigits: 2),
|
||||
_ListEntry(value: widget.rank[1]["highestEstAcc"], label: t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "), id: widget.rank[1]["highestEstAccID"], username: widget.rank[1]["highestEstAccNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestOpener"], label: "Opener", id: widget.rank[1]["highestOpenerID"], username: widget.rank[1]["highestOpenerNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestPlonk"], label: "Plonk", id: widget.rank[1]["highestPlonkID"], username: widget.rank[1]["highestPlonkNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestStride"], label: "Stride", id: widget.rank[1]["highestStrideID"], username: widget.rank[1]["highestStrideNick"], approximate: false, fractionDigits: 3),
|
||||
_ListEntry(value: widget.rank[1]["highestInfDS"], label: "Inf. DS", id: widget.rank[1]["highestInfDSid"], username: widget.rank[1]["highestInfDSnick"], approximate: false, fractionDigits: 3),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(children: [
|
||||
_ListEntry(value: widget.rank[1]["totalGamesPlayed"], label: t.statCellNum.totalGames, id: "", username: "", approximate: true, fractionDigits: 0),
|
||||
_ListEntry(value: widget.rank[1]["totalGamesWon"], label: t.statCellNum.totalWon, id: "", username: "", approximate: true, fractionDigits: 0),
|
||||
_ListEntry(value: (widget.rank[1]["totalGamesWon"] / widget.rank[1]["totalGamesPlayed"]) * 100, label: t.statCellNum.winrate.replaceAll(RegExp(r'\n'), " "), id: "", username: "", approximate: true, fractionDigits: 3),
|
||||
]))
|
||||
],
|
||||
),
|
||||
],
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
class _ListEntry extends StatelessWidget {
|
||||
final num value;
|
||||
final String label;
|
||||
final String id;
|
||||
final String username;
|
||||
final bool approximate;
|
||||
final int? fractionDigits;
|
||||
const _ListEntry(
|
||||
{required this.value,
|
||||
required this.label,
|
||||
this.fractionDigits,
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.approximate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
NumberFormat f = NumberFormat.decimalPatternDigits(
|
||||
locale: LocaleSettings.currentLocale.languageCode,
|
||||
decimalDigits: fractionDigits ?? 0);
|
||||
return ListTile(
|
||||
title: Text(label),
|
||||
trailing: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(f.format(value),
|
||||
style: const TextStyle(fontSize: 22, height: 0.9)),
|
||||
if (id.isNotEmpty) Text(t.forPlayer(username: username), style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.w100),)
|
||||
],
|
||||
),
|
||||
onTap: id.isNotEmpty
|
||||
? () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MainView(player: id),
|
||||
maintainState: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MyScatterSpot{
|
||||
num x;
|
||||
num y;
|
||||
String id;
|
||||
String nickname;
|
||||
String rank;
|
||||
Color color;
|
||||
_MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color);
|
||||
}
|
|
@ -0,0 +1,455 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/cutoff_tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/widgets/future_error.dart';
|
||||
|
||||
class RankView extends StatefulWidget {
|
||||
final String rank;
|
||||
final double nextRankTR;
|
||||
final double nextRankPercentile;
|
||||
final double nextRankTargetTR;
|
||||
final int totalPlayers;
|
||||
final CutoffTetrio cutoffTetrio;
|
||||
|
||||
const RankView({super.key, required this.rank, required this.nextRankTR, required this.nextRankPercentile, required this.nextRankTargetTR, required this.totalPlayers, required this.cutoffTetrio});
|
||||
|
||||
@override
|
||||
State<RankView> createState() => _RankState();
|
||||
}
|
||||
|
||||
enum CardMod{
|
||||
graph,
|
||||
minimums,
|
||||
maximums
|
||||
}
|
||||
|
||||
class _RankState extends State<RankView> {
|
||||
CardMod cardMod = CardMod.graph;
|
||||
|
||||
Future<List> getRanksAverages(String rank) async {
|
||||
var lb = await teto.fetchTLLeaderboard();
|
||||
return lb.getRankData(rank);
|
||||
}
|
||||
|
||||
Widget partOfTheWidget(List<dynamic>? data){
|
||||
double? avgAPM = data != null ? data[0].apm : widget.cutoffTetrio.apm;
|
||||
double? avgPPS = data != null ? data[0].pps : widget.cutoffTetrio.pps;
|
||||
double? avgVS = data != null ? data[0].vs : widget.cutoffTetrio.vs;
|
||||
double? avgAPP = data != null ? data[1]["avgAPP"] : widget.cutoffTetrio.nerdStats?.app;
|
||||
double? avgVSAPM = data != null ? data[1]["avgVSAPM"] : widget.cutoffTetrio.nerdStats?.vsapm;
|
||||
double? avgDSS = data != null ? data[1]["avgDSS"] : widget.cutoffTetrio.nerdStats?.dss;
|
||||
double? avgDSP = data != null ? data[1]["avgDSP"] : widget.cutoffTetrio.nerdStats?.dsp;
|
||||
double? avgAPPDSP = data != null ? data[1]["avgAPPDSP"] : widget.cutoffTetrio.nerdStats?.appdsp;
|
||||
double? avgCheese = data != null ? data[1]["avgCheese"] : widget.cutoffTetrio.nerdStats?.cheese;
|
||||
double? avgGbE = data != null ? data[1]["avgGBE"] : widget.cutoffTetrio.nerdStats?.gbe;
|
||||
double? avgNyaAPP = data != null ? data[1]["avgNyaAPP"] : widget.cutoffTetrio.nerdStats?.nyaapp;
|
||||
double? avgArea = data != null ? data[1]["avgArea"] : widget.cutoffTetrio.nerdStats?.area;
|
||||
return Column(
|
||||
children: [
|
||||
Divider(),
|
||||
Text(t.rankView.avgStats, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text("${avgAPM != null ? f2.format(avgAPM) : "-.--"} ${t.stats.apm.short} • ${avgPPS != null ? f2.format(avgPPS) : "-.--"} ${t.stats.pps.short} • ${avgVS != null ? f2.format(avgVS) : "-.--"} ${t.stats.vs.short}", style: Theme.of(context).textTheme.displayLarge),
|
||||
Divider(),
|
||||
Center(child: Text(t.rankView.avgNerdStats, style: Theme.of(context).textTheme.displayLarge)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.app.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgAPP != null ? f3.format(avgAPP) : "-.---", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.vsapm.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgVSAPM != null ? f3.format(avgVSAPM) : "-.---", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.dss.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgDSS != null ? f3.format(avgDSS) : "-.---", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.dsp.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgDSP != null ? f3.format(avgDSP) : "-.---", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.appdsp.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgAPPDSP != null ? f3.format(avgAPPDSP) : "-.---", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.cheese.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgCheese != null ? f3.format(avgCheese) : "--.--", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.gbe.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgGbE != null ? f3.format(avgGbE) : "-.---", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.nyaapp.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgNyaAPP != null ? f3.format(avgNyaAPP) : "-.---", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.stats.area.full, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(avgArea != null ? f3.format(avgArea) : "---.-", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget rightSide(double width, bool shortNames){
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: FutureBuilder<List<dynamic>>(
|
||||
future: getRanksAverages(widget.rank),
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState){
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasError){ return FutureError(snapshot); }
|
||||
if (snapshot.hasData){
|
||||
return SingleChildScrollView(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: shortNames ? 140.0 : 200.0,
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(shortNames ? "" : t.stats.cheese.full, style: TextStyle(fontSize: 28, color: Colors.transparent)),
|
||||
Divider(),
|
||||
RankViewEntry(shortNames ? t.stats.tr.short : t.stats.tr.full, null, null),
|
||||
RankViewEntry(t.stats.glicko.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.rd.short : t.stats.rd.full, null, null),
|
||||
RankViewEntry(t.stats.glixare.full, null, null, differentBG: true),
|
||||
RankViewEntry(t.stats.s1tr.short, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.gp.short : t.stats.gp.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.gw.short : t.stats.gw.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.winrate.short : t.stats.winrate.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.apm.short : t.stats.apm.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.pps.short : t.stats.pps.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.vs.short : t.stats.vs.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.app.short : t.stats.app.full, null, null, differentBG: true),
|
||||
RankViewEntry(t.stats.vsapm.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.dss.short : t.stats.dss.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.dsp.short : t.stats.dsp.full, null, null),
|
||||
RankViewEntry(t.stats.appdsp.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.cheese.short : t.stats.cheese.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.gbe.short : t.stats.gbe.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.nyaapp.short : t.stats.nyaapp.full, null, null),
|
||||
RankViewEntry(t.stats.area.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.etr.short : t.stats.etr.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.etracc.short : t.stats.etracc.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.opener.short : t.stats.opener.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.plonk.short : t.stats.plonk.full, null, null, differentBG: true),
|
||||
RankViewEntry(shortNames ? t.stats.stride.short : t.stats.stride.full, null, null),
|
||||
RankViewEntry(shortNames ? t.stats.infds.short : t.stats.infds.full, null, null, differentBG: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(t.rankView.minimums, style: TextStyle(fontSize: 28)),
|
||||
Divider(),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestTR"])}${shortNames ? "" : " ${t.stats.tr.short}"}", snapshot.data![1]["lowestTRnick"], snapshot.data![1]["lowestTRid"]),
|
||||
RankViewEntry(f4.format(snapshot.data![1]["lowestGlicko"]), snapshot.data![1]["lowestGlickoNick"], snapshot.data![1]["lowestGlickoID"], differentBG: true),
|
||||
RankViewEntry(f4.format(snapshot.data![1]["lowestRD"]), snapshot.data![1]["lowestRdNick"], snapshot.data![1]["lowestRdID"]),
|
||||
RankViewEntry(f4.format(snapshot.data![1]["lowestGlixare"]), snapshot.data![1]["lowestGlixareNick"], snapshot.data![1]["lowestGlixareID"], differentBG: true),
|
||||
RankViewEntry(f2.format(snapshot.data![1]["lowestS1tr"]), snapshot.data![1]["lowestS1trNick"], snapshot.data![1]["lowestS1trID"]),
|
||||
RankViewEntry(intf.format(snapshot.data![1]["lowestGamesPlayed"]), snapshot.data![1]["lowestGamesPlayedNick"], snapshot.data![1]["lowestGamesPlayedID"], differentBG: true),
|
||||
RankViewEntry(intf.format(snapshot.data![1]["lowestGamesWon"]), snapshot.data![1]["lowestGamesWonNick"], snapshot.data![1]["lowestGamesWonID"]),
|
||||
RankViewEntry(percentage.format(snapshot.data![1]["lowestWinrate"]), snapshot.data![1]["lowestWinrateNick"], snapshot.data![1]["lowestWinrateID"], differentBG: true),
|
||||
RankViewEntry("${f2.format(snapshot.data![1]["lowestAPM"])}${shortNames ? "" : " ${t.stats.apm.short}"}", snapshot.data![1]["lowestAPMnick"], snapshot.data![1]["lowestAPMid"]),
|
||||
RankViewEntry("${f2.format(snapshot.data![1]["lowestPPS"])}${shortNames ? "" : " ${t.stats.pps.short}"}", snapshot.data![1]["lowestPPSnick"], snapshot.data![1]["lowestPPSid"], differentBG: true),
|
||||
RankViewEntry("${f2.format(snapshot.data![1]["lowestVS"])}${shortNames ? "" : " ${t.stats.vs.short}"}", snapshot.data![1]["lowestVSnick"], snapshot.data![1]["lowestVSid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestAPP"])}${shortNames ? "" : " ${t.stats.app.short}"}", snapshot.data![1]["lowestAPPnick"], snapshot.data![1]["lowestAPPid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestVSAPM"])}${shortNames ? "" : " ${t.stats.vsapm.short}"}", snapshot.data![1]["lowestVSAPMnick"], snapshot.data![1]["lowestVSAPMid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestDSS"])}${shortNames ? "" : " ${t.stats.dss.short}"}", snapshot.data![1]["lowestDSSnick"], snapshot.data![1]["lowestDSSid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestDSP"])}${shortNames ? "" : " ${t.stats.dsp.short}"}", snapshot.data![1]["lowestDSPnick"], snapshot.data![1]["lowestDSPid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestAPPDSP"])}${shortNames ? "" : " ${t.stats.appdsp.short}"}", snapshot.data![1]["lowestAPPDSPnick"], snapshot.data![1]["lowestAPPDSPid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestCheese"])}${shortNames ? "" : " ${t.stats.cheese.short}"}", snapshot.data![1]["lowestCheeseNick"], snapshot.data![1]["lowestCheeseID"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestGBE"])}${shortNames ? "" : " ${t.stats.gbe.short}"}", snapshot.data![1]["lowestGBEnick"], snapshot.data![1]["lowestGBEid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestNyaAPP"])}${shortNames ? "" : " ${t.stats.nyaapp.short}"}", snapshot.data![1]["lowestNyaAPPnick"], snapshot.data![1]["lowestNyaAPPid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestArea"])}${shortNames ? "" : " ${t.stats.area.short}"}", snapshot.data![1]["lowestAreaNick"], snapshot.data![1]["lowestAreaID"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestEstTR"])}${shortNames ? "" : " ${t.stats.etr.short}"}", snapshot.data![1]["lowestEstTRnick"], snapshot.data![1]["lowestEstTRid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestEstAcc"])}${shortNames ? "" : " ${t.stats.etracc.short}"}", snapshot.data![1]["lowestEstAccNick"], snapshot.data![1]["lowestEstAccID"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestOpener"])}", snapshot.data![1]["lowestOpenerNick"], snapshot.data![1]["lowestOpenerID"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestPlonk"])}", snapshot.data![1]["lowestPlonkNick"], snapshot.data![1]["lowestPlonkID"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestStride"])}", snapshot.data![1]["lowestStrideNick"], snapshot.data![1]["lowestStrideID"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["lowestInfDS"])}", snapshot.data![1]["lowestInfDSnick"], snapshot.data![1]["lowestInfDSid"], differentBG: true)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(t.rankView.maximums, style: TextStyle(fontSize: 28)),
|
||||
Divider(),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestTR"])}${shortNames ? "" : " ${t.stats.tr.short}"}", snapshot.data![1]["highestTRnick"], snapshot.data![1]["highestTRid"]),
|
||||
RankViewEntry(f4.format(snapshot.data![1]["highestGlicko"]), snapshot.data![1]["highestGlickoNick"], snapshot.data![1]["highestGlickoID"], differentBG: true),
|
||||
RankViewEntry(f4.format(snapshot.data![1]["highestRD"]), snapshot.data![1]["highestRdNick"], snapshot.data![1]["highestRdID"]),
|
||||
RankViewEntry(f4.format(snapshot.data![1]["highestGlixare"]), snapshot.data![1]["highestGlixareNick"], snapshot.data![1]["highestGlixareID"], differentBG: true),
|
||||
RankViewEntry(f2.format(snapshot.data![1]["highestS1tr"]), snapshot.data![1]["highestS1trNick"], snapshot.data![1]["highestS1trID"]),
|
||||
RankViewEntry(intf.format(snapshot.data![1]["highestGamesPlayed"]), snapshot.data![1]["highestGamesPlayedNick"], snapshot.data![1]["highestGamesPlayedID"], differentBG: true),
|
||||
RankViewEntry(intf.format(snapshot.data![1]["highestGamesWon"]), snapshot.data![1]["highestGamesWonNick"], snapshot.data![1]["highestGamesWonID"]),
|
||||
RankViewEntry(percentage.format(snapshot.data![1]["highestWinrate"]), snapshot.data![1]["highestWinrateNick"], snapshot.data![1]["highestWinrateID"], differentBG: true),
|
||||
RankViewEntry("${f2.format(snapshot.data![1]["highestAPM"])}${shortNames ? "" : " ${t.stats.apm.short}"}", snapshot.data![1]["highestAPMnick"], snapshot.data![1]["highestAPMid"]),
|
||||
RankViewEntry("${f2.format(snapshot.data![1]["highestPPS"])}${shortNames ? "" : " ${t.stats.pps.short}"}", snapshot.data![1]["highestPPSnick"], snapshot.data![1]["highestPPSid"], differentBG: true),
|
||||
RankViewEntry("${f2.format(snapshot.data![1]["highestVS"])}${shortNames ? "" : " ${t.stats.vs.short}"}", snapshot.data![1]["highestVSnick"], snapshot.data![1]["highestVSid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestAPP"])}${shortNames ? "" : " ${t.stats.app.short}"}", snapshot.data![1]["highestAPPnick"], snapshot.data![1]["highestAPPid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestVSAPM"])}${shortNames ? "" : " ${t.stats.vsapm.short}"}", snapshot.data![1]["highestVSAPMnick"], snapshot.data![1]["highestVSAPMid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestDSS"])}${shortNames ? "" : " ${t.stats.dss.short}"}", snapshot.data![1]["highestDSSnick"], snapshot.data![1]["highestDSSid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestDSP"])}${shortNames ? "" : " ${t.stats.dsp.short}"}", snapshot.data![1]["highestDSPnick"], snapshot.data![1]["highestDSPid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestAPPDSP"])}${shortNames ? "" : " ${t.stats.appdsp.short}"}", snapshot.data![1]["highestAPPDSPnick"], snapshot.data![1]["highestAPPDSPid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestCheese"])}${shortNames ? "" : " ${t.stats.cheese.short}"}", snapshot.data![1]["highestCheeseNick"], snapshot.data![1]["highestCheeseID"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestGBE"])}${shortNames ? "" : " ${t.stats.gbe.short}"}", snapshot.data![1]["highestGBEnick"], snapshot.data![1]["highestGBEid"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestNyaAPP"])}${shortNames ? "" : " ${t.stats.nyaapp.short}"}", snapshot.data![1]["highestNyaAPPnick"], snapshot.data![1]["highestNyaAPPid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestArea"])}${shortNames ? "" : " ${t.stats.area.short}"}", snapshot.data![1]["highestAreaNick"], snapshot.data![1]["highestAreaID"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestEstTR"])}${shortNames ? "" : " ${t.stats.etr.short}"}", snapshot.data![1]["highestEstTRnick"], snapshot.data![1]["highestEstTRid"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestEstAcc"])}${shortNames ? "" : " ${t.stats.etracc.short}"}", snapshot.data![1]["highestEstAccNick"], snapshot.data![1]["highestEstAccID"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestOpener"])}", snapshot.data![1]["highestOpenerNick"], snapshot.data![1]["highestOpenerID"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestPlonk"])}", snapshot.data![1]["highestPlonkNick"], snapshot.data![1]["highestPlonkID"], differentBG: true),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestStride"])}", snapshot.data![1]["highestStrideNick"], snapshot.data![1]["highestStrideID"]),
|
||||
RankViewEntry("${f4.format(snapshot.data![1]["highestInfDS"])}", snapshot.data![1]["highestInfDSnick"], snapshot.data![1]["highestInfDSid"], differentBG: true)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const Text("End of FutureBuilder<List>");
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double percentileGap = widget.cutoffTetrio.percentile - widget.nextRankPercentile;
|
||||
int supposedToBePlayers = (widget.totalPlayers * percentileGap).floor();
|
||||
return Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: constraints.maxWidth <= 768.0 ? constraints.maxWidth : 350.0,
|
||||
height: constraints.maxHeight,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Card(child: Center(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 8.0, 5.0, 10.0),
|
||||
child: Text(widget.rank == "" ? t.rankView.everyoneTitle : t.rankView.rankTitle(rank: widget.rank.toUpperCase()), style: TextStyle(fontSize: 28)),
|
||||
))),
|
||||
Card(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset("res/tetrio_tl_alpha_ranks/${widget.rank == "" ? "z" : widget.rank}.png",fit: BoxFit.fitHeight,height: 128),
|
||||
Text(t.stats.players(n: widget.cutoffTetrio.count), style: Theme.of(context).textTheme.titleSmall,),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.rankView.trRange, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text("${f2.format(widget.cutoffTetrio.tr)} — ${f2.format(widget.nextRankTR)}", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
Text("(${t.rankView.trGap(value: f2.format(widget.nextRankTR - widget.cutoffTetrio.tr))})", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14))
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.rank != "") Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.rankView.supposedToBe, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text("${intf.format(widget.cutoffTetrio.targetTr)} — ${intf.format(widget.nextRankTargetTR)}", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
if (widget.rank != "") Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
Text("(${t.rankView.trGap(value: intf.format(widget.nextRankTargetTR - widget.cutoffTetrio.targetTr))})", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14))
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.nextRankTargetTR < widget.nextRankTR) Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.rankView.inflationGap, style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.redAccent)),
|
||||
Text("${f2.format(widget.nextRankTR - widget.nextRankTargetTR)} ${t.stats.tr.short}", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.redAccent))
|
||||
],
|
||||
),
|
||||
if (widget.cutoffTetrio.tr < widget.cutoffTetrio.targetTr) Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.rankView.deflationGap, style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.greenAccent)),
|
||||
Text("${f2.format(widget.cutoffTetrio.targetTr - widget.cutoffTetrio.tr)} ${t.stats.tr.short}", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.greenAccent))
|
||||
],
|
||||
),
|
||||
if (widget.rank != "") Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.rankView.LBposRange, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text("${percentage.format(widget.cutoffTetrio.percentile)} — ${percentage.format(widget.nextRankPercentile)}", style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
if (widget.rank != "") Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
Text("(${t.rankView.gap(value: percentage.format(percentileGap))})", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14))
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.rank != "") Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t.rankView.supposedToBe, style: Theme.of(context).textTheme.displayLarge),
|
||||
Text(t.stats.players(n: supposedToBePlayers), style: Theme.of(context).textTheme.displayLarge)
|
||||
],
|
||||
),
|
||||
if (widget.rank != "") Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
if (widget.cutoffTetrio.count > supposedToBePlayers) Text("(${t.rankView.overpopulated(players: t.stats.players(n: widget.cutoffTetrio.count - supposedToBePlayers))})", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14))
|
||||
else if (widget.cutoffTetrio.count < supposedToBePlayers) Text("(${t.rankView.overpopulated(players: t.stats.players(n: supposedToBePlayers - widget.cutoffTetrio.count))})", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14))
|
||||
else Text("(${t.rankView.PlayersEqualSupposedToBe})", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14))
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.rank == "") FutureBuilder<List<dynamic>>(
|
||||
future: getRanksAverages(widget.rank),
|
||||
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 partOfTheWidget(snapshot.data);
|
||||
}
|
||||
if (snapshot.hasError) return FutureError(snapshot);
|
||||
}
|
||||
return Text("End of the FutureBuilder");
|
||||
},
|
||||
)
|
||||
else partOfTheWidget(null),
|
||||
if (constraints.maxWidth <= 768.0) Divider(),
|
||||
if (constraints.maxWidth <= 768.0) rightSide(constraints.maxWidth, true)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
if (constraints.maxWidth > 768.0) rightSide(constraints.maxWidth - 350, false)
|
||||
],
|
||||
);
|
||||
},),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RankViewEntry extends StatelessWidget {
|
||||
final String formattedValue;
|
||||
final String? username;
|
||||
final String? userId;
|
||||
final bool differentBG;
|
||||
|
||||
const RankViewEntry(this.formattedValue, this.username, this.userId, {this.differentBG = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(color: differentBG ? Colors.black26 : null),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: username != null ? EdgeInsets.only(bottom: 4.0) : EdgeInsets.all(0),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: formattedValue,
|
||||
style: Theme.of(context).textTheme.displayLarge,
|
||||
children: [
|
||||
TextSpan(text: username != null ? "\n(${username!.toUpperCase()})" : "\n", style: Theme.of(context).textTheme.displayLarge!.copyWith(color: Colors.grey, fontSize: 14))
|
||||
]
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/utils/text_shadow.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:tetra_stats/main.dart' show teto;
|
||||
|
||||
class RankAveragesView extends StatefulWidget {
|
||||
const RankAveragesView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => RanksAverages();
|
||||
}
|
||||
|
||||
late String oldWindowTitle;
|
||||
|
||||
class RanksAverages extends State<RankAveragesView> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${t.rankAveragesViewTitle}");
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.rankAveragesViewTitle),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<CutoffsTetrio?>(future: teto.fetchCutoffsTetrio(), builder: (context, snapshot){
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator(color: Colors.white));
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasData){
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
width: 900,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
border: TableBorder.all(color: Colors.grey.shade900),
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(48),
|
||||
1: FixedColumnWidth(155),
|
||||
2: FixedColumnWidth(150),
|
||||
3: FixedColumnWidth(90),
|
||||
4: FixedColumnWidth(130),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text("TR", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text("APM", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text("PPS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text("VS", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Text("Advanced", textAlign: TextAlign.right, style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text("Players (${intf.format(snapshot.data!.total)})", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
]
|
||||
),
|
||||
for (String rank in snapshot.data!.data.keys) TableRow(
|
||||
decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[rank]!.withAlpha(200), rankColors[rank]!.withAlpha(100)])),
|
||||
children: [
|
||||
Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/$rank.png", height: 48)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(f2.format(snapshot.data!.data[rank]!.tr), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(f2.format(snapshot.data!.data[rank]!.apm), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(f2.format(snapshot.data!.data[rank]!.pps), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(f2.format(snapshot.data!.data[rank]!.vs), textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text("${f3.format(snapshot.data!.data[rank]!.apm / (snapshot.data!.data[rank]!.pps * 60))} APP\n${f3.format(snapshot.data!.data[rank]!.vs / snapshot.data!.data[rank]!.apm)} VS/APM", textAlign: TextAlign.right, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.right,
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100, color: Colors.white, shadows: textShadow),
|
||||
children: [
|
||||
TextSpan(text: intf.format(snapshot.data!.data[rank]!.count)),
|
||||
TextSpan(text: " (${f2.format(snapshot.data!.data[rank]!.countPercentile * 100)}%)", style: const TextStyle(color: Colors.white60, shadows: null)),
|
||||
TextSpan(text: "\n(from № ${intf.format(snapshot.data!.data[rank]!.pos)})", style: const TextStyle(color: Colors.white60, shadows: null))
|
||||
]
|
||||
))
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp)))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
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),
|
||||
if (snapshot.stackTrace != null) Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
return const Text("end of FutureBuilder");
|
||||
}
|
||||
})
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,302 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/main.dart' show packageInfo, teto, prefs;
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/services/crud_exceptions.dart';
|
||||
import 'package:tetra_stats/utils/open_in_browser.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
late String oldWindowTitle;
|
||||
TextStyle subtitleStyle = const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey);
|
||||
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => SettingsState();
|
||||
}
|
||||
|
||||
class SettingsState extends State<SettingsView> {
|
||||
String defaultNickname = "Checking...";
|
||||
late bool showPositions;
|
||||
late bool updateInBG;
|
||||
final TextEditingController _playertext = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${t.settings}");
|
||||
}
|
||||
_getPreferences();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _getPreferences() {
|
||||
showPositions = prefs.getBool("showPositions") ?? false;
|
||||
updateInBG = prefs.getBool("updateInBG") ?? false;
|
||||
_setDefaultNickname(prefs.getString("player"));
|
||||
}
|
||||
|
||||
Future<void> _setDefaultNickname(String? n) async {
|
||||
if (n != null) {
|
||||
try {
|
||||
defaultNickname = await teto.getNicknameByID(n);
|
||||
} on TetrioPlayerNotExist {
|
||||
defaultNickname = n;
|
||||
}
|
||||
} else {
|
||||
defaultNickname = "dan63047";
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _setPlayer(String player) async {
|
||||
await prefs.setString('player', player);
|
||||
await _setDefaultNickname(player);
|
||||
}
|
||||
|
||||
Future<void> _removePlayer() async {
|
||||
await prefs.remove('player');
|
||||
await _setDefaultNickname("6098518e3d5155e6ec429cdc");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
List<DropdownMenuItem<AppLocale>>? locales = <DropdownMenuItem<AppLocale>>[];
|
||||
for (var v in AppLocale.values){
|
||||
locales.add(DropdownMenuItem<AppLocale>(
|
||||
value: v, child: Text(t.locales[v.languageTag]!)));
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.settings),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(t.exportDB),
|
||||
subtitle: Text(t.exportDBDescription, style: subtitleStyle),
|
||||
onTap: () {
|
||||
if (kIsWeb){
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb)));
|
||||
} else if (Platform.isAndroid){
|
||||
var downloadFolder = Directory("/storage/emulated/0/Download");
|
||||
File exportedDB = File("${downloadFolder.path}/TetraStats.db");
|
||||
getApplicationDocumentsDirectory().then((value) {
|
||||
exportedDB.writeAsBytes(File("${value.path}/TetraStats.db").readAsBytesSync());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.androidExportAlertTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: "Eurostile Round Extended")),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(children: [Text(t.androidExportText(exportedDB: exportedDB))]),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.popupActions.ok),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
});
|
||||
} else if (Platform.isLinux || Platform.isWindows) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.desktopExportAlertTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: "Eurostile Round Extended")),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(children: [
|
||||
Text(t.desktopExportText)
|
||||
]),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.popupActions.ok),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.importDB),
|
||||
subtitle: Text(t.importDBDescription, style: subtitleStyle),
|
||||
onTap: () {
|
||||
if (kIsWeb){
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.notForWeb)));
|
||||
}else if(Platform.isAndroid){
|
||||
FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
).then((value){
|
||||
if (value != null){
|
||||
var newDB = value.paths[0]!;
|
||||
teto.close().then((value){
|
||||
if(!newDB.endsWith("db")){
|
||||
return ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importWrongFileType)));
|
||||
}
|
||||
getApplicationDocumentsDirectory().then((value){
|
||||
var oldDB = File("${value.path}/TetraStats.db");
|
||||
oldDB.writeAsBytes(File(newDB).readAsBytesSync(), flush: true).then((value){
|
||||
teto.open();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importSuccess)));
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importCancelled)));
|
||||
}
|
||||
});
|
||||
}else{
|
||||
const XTypeGroup typeGroup = XTypeGroup(
|
||||
label: 'Tetra Stats Database',
|
||||
extensions: <String>['db'],
|
||||
);
|
||||
openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]).then((value){
|
||||
if (value != null){
|
||||
var newDB = value.path;
|
||||
teto.close().then((value){
|
||||
getApplicationDocumentsDirectory().then((value){
|
||||
var oldDB = File("${value.path}/TetraStats.db");
|
||||
oldDB.writeAsBytes(File(newDB).readAsBytesSync()).then((value){
|
||||
teto.open();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importSuccess)));
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.importCancelled)));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.yourID),
|
||||
subtitle: Text(t.yourIDText, style: subtitleStyle),
|
||||
trailing: Text(defaultNickname),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(t.yourIDAlertTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: "Eurostile Round Extended")),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(children: [
|
||||
Text(t.yourIDText),
|
||||
TextField(controller: _playertext, maxLength: 25)
|
||||
]),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.popupActions.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(t.popupActions.submit),
|
||||
onPressed: () async {
|
||||
if (_playertext.text.isEmpty) {
|
||||
_removePlayer();
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
late TetrioPlayer user;
|
||||
try{
|
||||
user = await teto.fetchPlayer(_playertext.text.toLowerCase().trim());
|
||||
}on Exception{
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.noSuchUser)));
|
||||
return;
|
||||
}
|
||||
_setPlayer(user.userId);
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
],
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.language),
|
||||
subtitle: Text("By default, the system language will be selected (if available among Tetra Stats locales, otherwise English)", style: subtitleStyle),
|
||||
trailing: DropdownButton(
|
||||
items: locales,
|
||||
value: LocaleSettings.currentLocale,
|
||||
onChanged: (value){
|
||||
LocaleSettings.setLocale(value!);
|
||||
if(value.languageCode == Platform.localeName.substring(0, 2)){
|
||||
prefs.remove('locale');
|
||||
}else{
|
||||
prefs.setString('locale', value.languageCode);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(title: Text(t.customization),
|
||||
subtitle: Text(t.customizationDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () {
|
||||
context.go("/settings/customization");
|
||||
},),
|
||||
ListTile(title: Text(t.updateInBackground),
|
||||
subtitle: Text(t.updateInBackgroundDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
|
||||
trailing: Switch(value: updateInBG, onChanged: (bool value){
|
||||
prefs.setBool("updateInBG", value);
|
||||
setState(() {
|
||||
updateInBG = value;
|
||||
});
|
||||
}),),
|
||||
ListTile(title: Text(t.lbStats),
|
||||
subtitle: Text(t.lbStatsDescription, style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
|
||||
trailing: Switch(value: showPositions, onChanged: (bool value){
|
||||
prefs.setBool("showPositions", value);
|
||||
setState(() {
|
||||
showPositions = value;
|
||||
});
|
||||
}),),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
onTap: (){
|
||||
launchInBrowser(Uri.https("github.com", "dan63047/TetraStats"));
|
||||
},
|
||||
title: Text(t.aboutApp, style: const TextStyle(fontWeight: FontWeight.w500),),
|
||||
subtitle: Text(t.aboutAppText(appName: packageInfo.appName, packageName: packageInfo.packageName, version: packageInfo.version, buildNumber: packageInfo.buildNumber)),
|
||||
trailing: const Icon(Icons.arrow_right)
|
||||
),
|
||||
// Wrap(
|
||||
// alignment: WrapAlignment.center,
|
||||
// spacing: 8,
|
||||
// children: [
|
||||
// TextButton(child: Text("Donate to me"), onPressed: (){},),TextButton(child: Text("Donate to NOT me"), onPressed: (){},),TextButton(child: Text("Donate to someone else"), onPressed: (){},),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/data_objects/record_single.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/views/destination_home.dart';
|
||||
import 'package:tetra_stats/widgets/singleplayer_record.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class SingleplayerRecordView extends StatelessWidget {
|
||||
final RecordSingle record;
|
||||
|
@ -15,28 +15,28 @@ class SingleplayerRecordView extends StatelessWidget {
|
|||
//bool bigScreen = MediaQuery.of(context).size.width >= 368;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: Text("${
|
||||
switch (record.gamemode){
|
||||
"40l" => t.sprint,
|
||||
"blitz" => t.blitz,
|
||||
String() => "5000000 Blast",
|
||||
}
|
||||
} ${timestamp(record.timestamp)}"),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
SingleplayerRecord(record: record, hideTitle: true),
|
||||
// TODO: Insert replay link here
|
||||
]
|
||||
)
|
||||
],
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 768
|
||||
),
|
||||
child: switch (record.gamemode){
|
||||
"zenith" => ZenithCard(record, false, [], width: MediaQuery.of(context).size.width),
|
||||
"zenithex" => ZenithCard(record, false, [], width: MediaQuery.of(context).size.width),
|
||||
_ => SingleplayerRecord(record: record, hideTitle: true)
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_constants.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/relative_timestamps.dart';
|
||||
import 'package:tetra_stats/utils/text_shadow.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
late String oldWindowTitle;
|
||||
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode);
|
||||
|
@ -22,16 +19,16 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${t.settings}");
|
||||
}
|
||||
// if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
// windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
// windowManager.setTitle("Tetra Stats: ${t.settings}");
|
||||
// }
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
// if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -40,8 +37,14 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
|||
final t = Translations.of(context);
|
||||
bool bigScreen = MediaQuery.of(context).size.width >= 368;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.sprintAndBlitsViewTitle),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
|
@ -67,11 +70,11 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
|||
Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.sprint, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
child: Text(t.gamemodes["40l"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(t.blitz, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
child: Text(t.gamemodes["blitz"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||
),
|
||||
]
|
||||
),
|
||||
|
@ -81,7 +84,7 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
|||
Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/${sprintEntry.key}.png", height: 48)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(get40lTime(sprintEntry.value.inMicroseconds), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
|
||||
child: Text(getALittleBitMoreNormalTime(sprintEntry.value), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
|
@ -91,7 +94,7 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
|||
)
|
||||
],
|
||||
),
|
||||
Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 5, 26))))
|
||||
Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 8, 25))))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -2,11 +2,12 @@ import 'dart:io';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/widgets/graphs.dart';
|
||||
import 'package:tetra_stats/widgets/nerd_stats_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
import 'package:tetra_stats/widgets/tl_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/user_thingy.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
|
||||
|
@ -29,7 +30,7 @@ class StateState extends State<StateView> {
|
|||
_scrollController = ScrollController();
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}");
|
||||
windowManager.setTitle(t.stateView.title(date: timestamp(widget.state.timestamp)));
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
@ -41,20 +42,24 @@ class StateState extends State<StateView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void _justUpdate() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
//final t = Translations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("State from ${timestamp(widget.state.timestamp)}"),
|
||||
title: Text(t.stateView.title(date: timestamp(widget.state.timestamp)), style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 28)),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: TLThingy(tl: widget.state, userID: widget.state.id, states: [])
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
TetraLeagueThingy(league: widget.state),
|
||||
if (widget.state.nerdStats != null) NerdStatsThingy(nerdStats: widget.state.nerdStats!),
|
||||
if (widget.state.playstyle != null) Graphs(widget.state.apm!, widget.state.pps!, widget.state.vs!, widget.state.nerdStats!, widget.state.playstyle!)
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart' show teto;
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/views/mathes_view.dart';
|
||||
import 'package:tetra_stats/views/state_view.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class StatesView extends StatefulWidget {
|
||||
final String nickname;
|
||||
final String id;
|
||||
const StatesView({required this.nickname, required this.id, super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => StatesState();
|
||||
}
|
||||
|
||||
late String oldWindowTitle;
|
||||
|
||||
class StatesState extends State<StatesView> {
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
//windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}");
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.statesViewTitle(number: "", nickname: widget.nickname)),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: (){
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MatchesView(userID: widget.id, username: widget.nickname),
|
||||
),
|
||||
);
|
||||
}, icon: const Icon(Icons.list), tooltip: t.viewAllMatches)
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<List<TetraLeague>>(future: teto.getStates(widget.id), builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator(color: Colors.white));
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasData) {
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
prototypeItem: ListTile(
|
||||
title: Text(""),
|
||||
subtitle: Text("", style: TextStyle(color: Colors.grey)),
|
||||
trailing: IconButton(icon: const Icon(Icons.delete_forever), onPressed: (){}),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(timestamp(snapshot.data![index].timestamp)),
|
||||
subtitle: Text(
|
||||
t.statesViewEntry(level: f2.format(snapshot.data![index].tr), games: intf.format(snapshot.data![index].gamesPlayed), glicko: snapshot.data![index].glicko != null ? f2.format(snapshot.data![index].glicko) : "---", rd: snapshot.data![index].rd != null ? f2.format(snapshot.data![index].rd) : "--"),
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_forever),
|
||||
onPressed: () {
|
||||
teto.deleteState(snapshot.data![index].id+snapshot.data![index].timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(snapshot.data![index].timestamp)))));
|
||||
}));
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StateView(state: snapshot.data![index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
} else 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(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return const Center(child: Text('default case of FutureBuilder', style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center));
|
||||
}
|
||||
)));}
|
||||
}
|
|
@ -1,218 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
import 'package:tetra_stats/views/main_view.dart';
|
||||
import 'package:tetra_stats/views/rank_averages_view.dart';
|
||||
import 'package:tetra_stats/views/ranks_averages_view.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
List<DropdownMenuItem> _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
|
||||
Stats _sortBy = Stats.tr;
|
||||
bool reversed = false;
|
||||
List<DropdownMenuItem> _itemCountries = [for (MapEntry e in t.countries.entries) DropdownMenuItem(value: e.key, child: Text(e.value))];
|
||||
String _country = "";
|
||||
late String _oldWindowTitle;
|
||||
final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4);
|
||||
|
||||
class TLLeaderboardView extends StatefulWidget {
|
||||
const TLLeaderboardView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TLLeaderboardState();
|
||||
}
|
||||
|
||||
class TLLeaderboardState extends State<TLLeaderboardView> {
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.getTitle().then((value) => _oldWindowTitle = value);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(_oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final NumberFormat f2 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.tlLeaderboard),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const RankAveragesView(),
|
||||
maintainState: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.compress),
|
||||
tooltip: t.rankAveragesViewTitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
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: CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
if (snapshot.hasData){
|
||||
final allPlayers = snapshot.data?.getStatRanking(snapshot.data!.leaderboard, _sortBy, reversed: reversed, country: _country);
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle("Tetra Stats: ${t.tlLeaderboard} - ${t.players(n: allPlayers != null ? allPlayers.length : 0)}");
|
||||
bool bigScreen = MediaQuery.of(context).size.width > 768;
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (context, value) {
|
||||
return [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${t.players(n: allPlayers.length)} • ${t.sprintAndBlitsRelevance(date: timestamp(snapshot.data!.timestamp))}",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 25),
|
||||
),
|
||||
TextButton(onPressed: (){
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RankView(rank: snapshot.data!.getAverageOfRank("")),
|
||||
),
|
||||
);
|
||||
}, child: Text(t.everyoneAverages,
|
||||
style: const TextStyle(fontSize: 25)))
|
||||
],)
|
||||
)),
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text("${t.sortBy}: ",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||
DropdownButton(items: _itemStats, value: _sortBy, onChanged: ((value) {
|
||||
_sortBy = value;
|
||||
setState(() {});
|
||||
}),),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text("${t.reversed}: ",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 5.5, 0, 7.5),
|
||||
child: Checkbox(value: reversed,
|
||||
checkColor: Colors.black,
|
||||
onChanged: ((value) {
|
||||
reversed = value!;
|
||||
setState(() {});
|
||||
}),),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text("${t.country}: ",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||
DropdownButton(items: _itemCountries, value: _country, onChanged: ((value) {
|
||||
_country = value;
|
||||
setState(() {});
|
||||
}),),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),),
|
||||
const SliverToBoxAdapter(child: Divider())
|
||||
];
|
||||
},
|
||||
body: ListView.builder(
|
||||
itemCount: allPlayers!.length,
|
||||
prototypeItem: ListTile(
|
||||
leading: Text("0", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)),
|
||||
title: Text("ehhh...", style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)),
|
||||
trailing: SizedBox(height: bigScreen ? 48 : 36, width: 1,),
|
||||
subtitle: const Text("eh..."),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
leading: Text(
|
||||
(index+1).toString(),
|
||||
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 28 : 24, height: 0.9)
|
||||
),
|
||||
title: Text(allPlayers[index].username, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round Extended" : "Eurostile Round", height: 0.9)),
|
||||
subtitle: (bigScreen || _sortBy != Stats.tr) ? Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}",
|
||||
style: TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: bigScreen ? null : 13, color: _sortBy == Stats.tr ? Colors.grey : null)) : null,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${f2.format(allPlayers[index].tr)} TR", style: const TextStyle(fontSize: 28)),
|
||||
Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 36),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MainView(player: allPlayers[index].userId),
|
||||
maintainState: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}));
|
||||
}
|
||||
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),
|
||||
if (snapshot.stackTrace != null) Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
return const Text("end of FutureBuilder");
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
// ignore_for_file: use_build_context_synchronously, type_literal_in_constant_pattern
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:tetra_stats/data_objects/beta_record.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
|
||||
import 'package:tetra_stats/utils/relative_timestamps.dart';
|
||||
import 'package:tetra_stats/views/compare_view.dart' show CompareThingy;
|
||||
import 'package:tetra_stats/widgets/compare_thingy.dart';
|
||||
import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
import 'package:tetra_stats/widgets/vs_graphs.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/open_in_browser.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
@ -43,60 +43,23 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
late String reason;
|
||||
Duration totalTime = const Duration();
|
||||
List<Duration> roundLengths = [];
|
||||
List<BetaLeagueStats> timeWeightedStats = [];
|
||||
late bool initPlayerWon;
|
||||
|
||||
@override
|
||||
void initState(){
|
||||
rounds = [DropdownMenuItem(value: -1, child: Text(t.match))];
|
||||
rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]);
|
||||
if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch)));
|
||||
rounds = [DropdownMenuItem(value: -1, child: Text(t.tlMatchView.match))];
|
||||
rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.tlMatchView.roundNumber(n: i+1)))]);
|
||||
greenSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id == widget.initPlayerId);
|
||||
redSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id != widget.initPlayerId);
|
||||
List<double> apmMultipliedByWeights = [0, 0];
|
||||
List<double> ppsMultipliedByWeights= [0, 0];
|
||||
List<double> vsMultipliedByWeights = [0, 0];
|
||||
for (var round in widget.record.results.rounds){
|
||||
var longerLifetime = round[0].lifetime.compareTo(round[1].lifetime) == 1 ? round[0].lifetime : round[1].lifetime;
|
||||
roundLengths.add(longerLifetime);
|
||||
totalTime += longerLifetime;
|
||||
|
||||
BetaLeagueRound greenSide = round.firstWhere((element) => element.id == widget.initPlayerId);
|
||||
BetaLeagueRound redSide = round.firstWhere((element) => element.id != widget.initPlayerId);
|
||||
|
||||
apmMultipliedByWeights[0] += greenSide.stats.apm*longerLifetime.inMilliseconds;
|
||||
apmMultipliedByWeights[1] += redSide.stats.apm*longerLifetime.inMilliseconds;
|
||||
ppsMultipliedByWeights[0] += greenSide.stats.pps*longerLifetime.inMilliseconds;
|
||||
ppsMultipliedByWeights[1] += redSide.stats.pps*longerLifetime.inMilliseconds;
|
||||
vsMultipliedByWeights[0] += greenSide.stats.vs*longerLifetime.inMilliseconds;
|
||||
vsMultipliedByWeights[1] += redSide.stats.vs*longerLifetime.inMilliseconds;
|
||||
}
|
||||
timeWeightedStats = [
|
||||
BetaLeagueStats(
|
||||
apm: apmMultipliedByWeights[0]/totalTime.inMilliseconds,
|
||||
pps: ppsMultipliedByWeights[0]/totalTime.inMilliseconds,
|
||||
vs: vsMultipliedByWeights[0]/totalTime.inMilliseconds,
|
||||
garbageSent: widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent,
|
||||
garbageReceived: widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived,
|
||||
kills: widget.record.results.leaderboard[greenSidePlayer].stats.kills,
|
||||
altitude: widget.record.results.leaderboard[greenSidePlayer].stats.altitude,
|
||||
rank: widget.record.results.leaderboard[greenSidePlayer].stats.rank
|
||||
),
|
||||
BetaLeagueStats(
|
||||
apm: apmMultipliedByWeights[1]/totalTime.inMilliseconds,
|
||||
pps: ppsMultipliedByWeights[1]/totalTime.inMilliseconds,
|
||||
vs: vsMultipliedByWeights[1]/totalTime.inMilliseconds,
|
||||
garbageSent: widget.record.results.leaderboard[redSidePlayer].stats.garbageSent,
|
||||
garbageReceived: widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived,
|
||||
kills: widget.record.results.leaderboard[redSidePlayer].stats.kills,
|
||||
altitude: widget.record.results.leaderboard[redSidePlayer].stats.altitude,
|
||||
rank: widget.record.results.leaderboard[redSidePlayer].stats.rank
|
||||
),
|
||||
];
|
||||
initPlayerWon = widget.record.results.leaderboard[greenSidePlayer].wins > widget.record.results.leaderboard[redSidePlayer].wins;
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}");
|
||||
windowManager.setTitle("Tetra Stats: ${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.tlMatchView.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${timestamp(widget.record.ts)}");
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
@ -112,11 +75,11 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
bool bigScreen = width >= 768;
|
||||
if (roundSelector.isNegative){
|
||||
time = totalTime;
|
||||
readableTime = !time.isNegative ? "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "${t.matchLength}: ---";
|
||||
readableTime = !time.isNegative ? "${t.tlMatchView.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "${t.tlMatchView.matchLength}: ---";
|
||||
}else{
|
||||
time = roundLengths[roundSelector];
|
||||
int alive = widget.record.results.rounds[roundSelector].indexWhere((element) => element.alive);
|
||||
readableTime = "${t.roundLength}: ${!time.isNegative ? "${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "---"}\n${t.winner}: ${alive == -1 ? "idk" : widget.record.results.rounds[roundSelector][alive].username}";
|
||||
readableTime = "${t.tlMatchView.roundLength}: ${!time.isNegative ? "${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "---"}\n${t.tlMatchView.winner}: ${alive == -1 ? "idk" : widget.record.results.rounds[roundSelector][alive].username}";
|
||||
}
|
||||
return SizedBox(
|
||||
width: width,
|
||||
|
@ -189,7 +152,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text("${t.statsFor}: ",
|
||||
Text("${t.tlMatchView.statsFor}: ",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||
DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) {
|
||||
roundSelector = value;
|
||||
|
@ -199,9 +162,6 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (widget.record.id == widget.record.replayID && showMobileSelector) SliverToBoxAdapter(
|
||||
child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)),
|
||||
),
|
||||
if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(readableTime, textAlign: TextAlign.center))),
|
||||
const SliverToBoxAdapter(
|
||||
child: Divider(),
|
||||
|
@ -213,38 +173,32 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
Column(
|
||||
children: [
|
||||
CompareThingy(
|
||||
label: "APM",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].apm :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].apm :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
|
||||
label: t.stats.apm.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
|
||||
redSide: roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
|
||||
fractionDigits: 2,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "PPS",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].pps :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].pps :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
|
||||
label: t.stats.pps.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
|
||||
redSide: roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
|
||||
fractionDigits: 2,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "VS",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].vs :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].vs :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs,
|
||||
label: t.stats.vs.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
|
||||
redSide: roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs,
|
||||
fractionDigits: 2,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageSent,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageSent,
|
||||
label: "Sent", higherIsBetter: true),
|
||||
label: t.stats.sent, higherIsBetter: true),
|
||||
if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageReceived,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageReceived,
|
||||
label: "Received", higherIsBetter: true), const Divider(),
|
||||
label: t.stats.received, higherIsBetter: true), const Divider(),
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
|
@ -255,142 +209,114 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
fontSize: bigScreen ? 42 : 28)),
|
||||
),
|
||||
CompareThingy(
|
||||
label: "APP",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.app :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.app,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.app :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.app,
|
||||
label: t.stats.app.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.app,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.app,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "VS/APM",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.vsapm :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.vsapm,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.vsapm :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.vsapm,
|
||||
label: t.stats.vsapm.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.vsapm,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.vsapm,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "DS/S",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dss :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dss,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dss :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dss,
|
||||
label: t.stats.dss.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dss,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dss,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "DS/P",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dsp :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dsp,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dsp :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dsp,
|
||||
label: t.stats.dsp.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dsp,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dsp,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "APP + DS/P",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.appdsp :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.appdsp,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.appdsp :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.appdsp,
|
||||
label: t.stats.appdsp.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.appdsp,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.appdsp,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "),
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.cheese :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.cheese,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.cheese :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.cheese,
|
||||
label: t.stats.cheese.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.cheese,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.cheese,
|
||||
fractionDigits: 2,
|
||||
higherIsBetter: false,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "Gb Eff.",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.gbe :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.gbe,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.gbe :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.gbe,
|
||||
label: t.stats.gbe.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.gbe,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.gbe,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "wAPP",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.nyaapp :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.nyaapp,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.nyaapp :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.nyaapp,
|
||||
label: t.stats.nyaapp.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.nyaapp,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.nyaapp,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "Area",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.area :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.area,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.area :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.area,
|
||||
label: t.stats.area.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.area,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.area,
|
||||
fractionDigits: 2,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: t.statCellNum.estOfTRShort,
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].estTr.esttr :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].estTr.esttr :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.estTr.esttr,
|
||||
label: t.stats.etr.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.estTr.esttr,
|
||||
fractionDigits: 2,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "Opener",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.opener :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.opener,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.opener :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.opener,
|
||||
label: t.stats.opener.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.opener,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.opener,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "Plonk",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.plonk :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.plonk :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk,
|
||||
label: t.stats.plonk.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.plonk : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.plonk : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "Stride",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.stride :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.stride,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.stride :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.stride,
|
||||
label: t.stats.stride.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.stride,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.stride,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
CompareThingy(
|
||||
label: "Inf. DS",
|
||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.infds :
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.infds,
|
||||
redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.infds :
|
||||
roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.infds,
|
||||
label: t.stats.infds.short,
|
||||
greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.infds,
|
||||
redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.infds,
|
||||
fractionDigits: 3,
|
||||
higherIsBetter: true,
|
||||
),
|
||||
VsGraphs(
|
||||
roundSelector == -2 ? timeWeightedStats[0].apm : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
|
||||
roundSelector == -2 ? timeWeightedStats[0].pps : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
|
||||
roundSelector == -2 ? timeWeightedStats[0].vs : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
|
||||
roundSelector == -2 ? timeWeightedStats[0].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats,
|
||||
roundSelector == -2 ? timeWeightedStats[0].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle,
|
||||
roundSelector == -2 ? timeWeightedStats[1].apm : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
|
||||
roundSelector == -2 ? timeWeightedStats[1].pps : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
|
||||
roundSelector == -2 ? timeWeightedStats[1].vs : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs,
|
||||
roundSelector == -2 ? timeWeightedStats[1].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats,
|
||||
roundSelector == -2 ? timeWeightedStats[1].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats,
|
||||
roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -447,7 +373,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(t.matchLength),
|
||||
Text(t.tlMatchView.matchLength),
|
||||
RichText(
|
||||
text: !totalTime.isNegative ? TextSpan(
|
||||
text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}",
|
||||
|
@ -463,7 +389,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
if (widget.record.id != widget.record.replayID) Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(t.numberOfRounds),
|
||||
Text(t.tlMatchView.numberOfRounds),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: widget.record.results.rounds.length.toString(),
|
||||
|
@ -476,25 +402,15 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
),
|
||||
)
|
||||
],),
|
||||
Column(children: [
|
||||
OverflowBar(
|
||||
alignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
|
||||
],
|
||||
)
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
|
||||
onPressed: () {
|
||||
roundSelector = -1;
|
||||
setState(() {});
|
||||
}, child: Text(t.matchStats)),
|
||||
TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
|
||||
onPressed: timeWeightedStatsAvaliable ? () {
|
||||
roundSelector = -2;
|
||||
setState(() {});
|
||||
} : null, child: Text(t.timeWeightedmatchStats)) ,
|
||||
],
|
||||
)
|
||||
]),
|
||||
],
|
||||
)
|
||||
}, child: Text(t.tlMatchView.matchStats)),
|
||||
)
|
||||
];
|
||||
},
|
||||
|
@ -569,18 +485,21 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
final t = Translations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}"),
|
||||
title: Text(
|
||||
"${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.tlMatchView.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${widget.record.gamemode} ${timestamp(widget.record.ts)}",
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 28),
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
enabled: widget.record.gamemode == "league",
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Text(t.downloadReplay),
|
||||
child: Text(t.tlMatchView.downloadReplay),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
child: Text(t.openReplay),
|
||||
child: Text(t.tlMatchView.openReplay),
|
||||
),
|
||||
],
|
||||
onSelected: (value) async {
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart' show teto;
|
||||
import 'package:tetra_stats/utils/filesizes_converter.dart';
|
||||
import 'package:tetra_stats/views/states_view.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
late String oldWindowTitle;
|
||||
|
||||
class TrackedPlayersView extends StatefulWidget {
|
||||
const TrackedPlayersView({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TrackedPlayersState();
|
||||
}
|
||||
|
||||
class TrackedPlayersState extends State<TrackedPlayersView> {
|
||||
@override
|
||||
void initState() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
windowManager.setTitle("Tetra Stats: ${t.trackedPlayersViewTitle}");
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.trackedPlayersViewTitle),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.settings_backup_restore),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Text(t.duplicatedFix),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
child: Text(t.compressDB),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 1:
|
||||
teto.removeDuplicatesFromTLMatches();
|
||||
break;
|
||||
case 2:
|
||||
teto.compressDB().then((value) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.SpaceSaved(size: bytesToSize(value))))));
|
||||
break;
|
||||
default:
|
||||
}
|
||||
})
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: FutureBuilder(
|
||||
future: teto.getAllPlayers(),
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return const Center(child: CircularProgressIndicator(color: Colors.white));
|
||||
case ConnectionState.done:
|
||||
final allPlayers = (snapshot.data != null) ? snapshot.data as Map<String, String> : <String, String>{};
|
||||
List<String> keys = allPlayers.keys.toList();
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (context, value) {
|
||||
String howManyPlayers(int numberOfPlayers) => Intl.plural(
|
||||
numberOfPlayers,
|
||||
zero: t.trackedPlayersZeroEntrys,
|
||||
one: t.trackedPlayersOneEntry,
|
||||
other: t.trackedPlayersManyEntrys(numberOfPlayers: numberOfPlayers),
|
||||
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) {
|
||||
print(index);
|
||||
return ListTile(
|
||||
title: Text(allPlayers[keys[index]]??"No nickname (huh?)"),
|
||||
subtitle: Text(keys[index], style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_forever),
|
||||
onPressed: () {
|
||||
setState(() {teto.deletePlayer(keys[index]);});
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: allPlayers[keys[index]]??"No nickname (huh?)"))));
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StatesView(nickname: allPlayers[keys[index]]!, id: keys[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}));
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/views/destination_home.dart';
|
||||
import 'package:tetra_stats/views/main_view.dart';
|
||||
|
||||
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
|
||||
|
||||
class UserView extends StatefulWidget {
|
||||
final String searchFor;
|
||||
const UserView({super.key, required this.searchFor});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => UserState();
|
||||
}
|
||||
|
||||
late String oldWindowTitle;
|
||||
|
||||
class UserState extends State<UserView> {
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_scrollController = ScrollController();
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||
// windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||
// windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}");
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
// if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//final t = Translations.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: t.goBackButton,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return DestinationHome(searchFor: widget.searchFor, dataFuture: getData(widget.searchFor), constraints: constraints, noSidebar: true);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
import 'package:tetra_stats/widgets/zenith_thingy.dart';
|
||||
|
||||
class ZenithRecordView extends StatelessWidget {
|
||||
final RecordSingle record;
|
||||
|
||||
const ZenithRecordView({super.key, required this.record});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
//bool bigScreen = MediaQuery.of(context).size.width >= 368;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: Text("${
|
||||
switch (record.gamemode){
|
||||
"zenith" => t.quickPlay,
|
||||
"zenithex" => "${t.quickPlay} ${t.expert}",
|
||||
String() => "5000000 Blast",
|
||||
}
|
||||
} ${timestamp(record.timestamp)}"),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: SingleChildScrollView(
|
||||
child: ZenithThingy(record: record, switchable: false),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart';
|
||||
import 'package:tetra_stats/utils/text_shadow.dart';
|
||||
import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class AlphaLeagueEntryThingy extends StatelessWidget{
|
||||
final TetraLeagueAlphaRecord record;
|
||||
final String userID;
|
||||
|
||||
const AlphaLeagueEntryThingy(this.record, this.userID);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var accentColor = record.endContext.firstWhere((element) => element.userId == userID).success ? Colors.green : Colors.red;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
stops: const [0, 0.05],
|
||||
colors: [accentColor, Colors.transparent]
|
||||
)
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Text("${record.endContext.firstWhere((element) => element.userId == userID).points} : ${record.endContext.firstWhere((element) => element.userId != userID).points}",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow)),
|
||||
title: Text("vs. ${record.endContext.firstWhere((element) => element.userId != userID).username}"),
|
||||
subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey)),
|
||||
trailing: TrailingStats(
|
||||
record.endContext.firstWhere((element) => element.userId == userID).secondary,
|
||||
record.endContext.firstWhere((element) => element.userId == userID).tertiary,
|
||||
record.endContext.firstWhere((element) => element.userId == userID).extra,
|
||||
record.endContext.firstWhere((element) => element.userId != userID).secondary,
|
||||
record.endContext.firstWhere((element) => element.userId != userID).tertiary,
|
||||
record.endContext.firstWhere((element) => element.userId != userID).extra
|
||||
),
|
||||
//onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: record, initPlayerId: userID))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:tetra_stats/data_objects/badge.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/services/tetrio_crud.dart' show webVersionDomain;
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class BadgesThingy extends StatelessWidget{
|
||||
final List<Badge> badges;
|
||||
// TODO: make it obvious, that it's scrollable
|
||||
const BadgesThingy({super.key, required this.badges});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(t.badges, style: TextStyle(fontFamily: "Eurostile Round Extended")),
|
||||
const Spacer(),
|
||||
Text(intf.format(badges.length))
|
||||
],
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (var badge in badges)
|
||||
IconButton(
|
||||
onPressed: () => showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(badge.label, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 25,
|
||||
children: [
|
||||
Image.network(
|
||||
kIsWeb ? "https://${webVersionDomain}/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png",
|
||||
errorBuilder:(context, error, stackTrace) {
|
||||
return ErrorWidget(error);
|
||||
}
|
||||
),
|
||||
Text(badge.ts != null
|
||||
? t.obtainDate(date: timestamp(badge.ts!))
|
||||
: t.assignedManualy),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(t.actions.ok),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
tooltip: badge.label,
|
||||
icon: Image.network(
|
||||
kIsWeb ? "https://${webVersionDomain}/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png",
|
||||
height: 32,
|
||||
errorBuilder:(context, error, stackTrace) {
|
||||
return Image.asset("res/icons/kagari.png", height: 32, width: 32);
|
||||
}
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/beta_record.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/views/tl_match_view.dart';
|
||||
import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart';
|
||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||
|
||||
class BetaLeagueEntryThingy extends StatelessWidget{
|
||||
final BetaRecord record;
|
||||
final String userID;
|
||||
// TODO: Rating delta string is too long for small screens
|
||||
const BetaLeagueEntryThingy(this.record, this.userID);
|
||||
|
||||
TextSpan matchResult(String result){
|
||||
return switch(result){
|
||||
"victory" => TextSpan(
|
||||
text: t.matchResult.victory,
|
||||
style: TextStyle(color: Colors.greenAccent)
|
||||
),
|
||||
"defeat" => TextSpan(
|
||||
text: t.matchResult.defeat,
|
||||
style: TextStyle(color: Colors.redAccent)
|
||||
),
|
||||
"tie" => TextSpan(
|
||||
text: t.matchResult.tie,
|
||||
style: TextStyle(color: Colors.white)
|
||||
),
|
||||
"dqvictory" => TextSpan(
|
||||
text: t.matchResult.dqvictory,
|
||||
style: TextStyle(color: Colors.lightGreenAccent)
|
||||
),
|
||||
"dqdefeat" => TextSpan(
|
||||
text: t.matchResult.dqdefeat,
|
||||
style: TextStyle(color: Colors.red)
|
||||
),
|
||||
"nocontest" => TextSpan(
|
||||
text: t.matchResult.nocontest,
|
||||
style: TextStyle(color: Colors.blueAccent)
|
||||
),
|
||||
"nullified" => TextSpan(
|
||||
text: t.matchResult.nullified,
|
||||
style: TextStyle(color: Colors.purpleAccent)
|
||||
),
|
||||
_ => TextSpan(
|
||||
text: "${result.toUpperCase()}",
|
||||
style: TextStyle(color: Colors.orangeAccent)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Color deltaColor(double? delta){
|
||||
if (delta == null || delta.isNaN || ["nocontest", "nullified"].contains(record.extras.result)) return Colors.grey;
|
||||
if (delta.isNegative) return Colors.redAccent;
|
||||
else return Colors.greenAccent;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double? deltaTR = (record.extras.league[userID]?[1]?.tr != null && record.extras.league[userID]?[0]?.tr != null) ? record.extras.league[userID]![1]!.tr - record.extras.league[userID]![0]!.tr : null;
|
||||
double? deltaGlicko = (record.extras.league[userID]?[1]?.glicko != null && record.extras.league[userID]?[0]?.glicko != null) ? record.extras.league[userID]![1]!.glicko - record.extras.league[userID]![0]!.glicko : null;
|
||||
double? deltaRD = (record.extras.league[userID]?[1]?.rd != null && record.extras.league[userID]?[0]?.rd != null) ? record.extras.league[userID]![1]!.rd - record.extras.league[userID]![0]!.rd : null;
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
"${record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).wins} - ${record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).wins} ",
|
||||
style: TextStyle(fontSize: 26, height: 0.75, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
"vs.\n${record.enemyUsername}",
|
||||
style: TextStyle(fontSize: 14, height: 0.8, fontWeight: FontWeight.w100),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
|
||||
children: [
|
||||
matchResult(record.extras.result),
|
||||
TextSpan(
|
||||
text: ", ${timestamp(record.ts)}\n"
|
||||
),
|
||||
TextSpan(
|
||||
text: deltaTR != null ? "${fDiff.format(deltaTR)} TR" : "??? TR",
|
||||
style: TextStyle(
|
||||
color: deltaColor(deltaTR)
|
||||
)
|
||||
),
|
||||
TextSpan(
|
||||
text: ", "
|
||||
),
|
||||
TextSpan(
|
||||
text: deltaGlicko != null ? "${fDiff.format(deltaGlicko)} Glicko" : "??? Glicko",
|
||||
style: TextStyle(
|
||||
color: deltaColor(deltaGlicko)
|
||||
)
|
||||
),
|
||||
TextSpan(
|
||||
text: ", "
|
||||
),
|
||||
TextSpan(
|
||||
text: deltaRD != null ? "${fDiff.format(deltaRD)} RD" : "??? RD",
|
||||
style: TextStyle(
|
||||
color: Colors.grey
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: TrailingStats(
|
||||
record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.apm,
|
||||
record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.pps,
|
||||
record.results.leaderboard.firstWhere((element) => element.id != record.enemyID).stats.vs,
|
||||
record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.apm,
|
||||
record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.pps,
|
||||
record.results.leaderboard.firstWhere((element) => element.id == record.enemyID).stats.vs,
|
||||
),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: record, initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
|
||||
const TextStyle verdictStyle = TextStyle(fontSize: 14, fontFamily: "Eurostile Round Condensed", color: Colors.grey, height: 1.1);
|
||||
|
||||
class CompareThingy extends StatelessWidget {
|
||||
final num greenSide;
|
||||
final num redSide;
|
||||
final String label;
|
||||
final bool higherIsBetter;
|
||||
final int? fractionDigits;
|
||||
final String? postfix;
|
||||
final String? prefix;
|
||||
const CompareThingy(
|
||||
{super.key,
|
||||
required this.greenSide,
|
||||
required this.redSide,
|
||||
required this.label,
|
||||
required this.higherIsBetter,
|
||||
this.fractionDigits,
|
||||
this.prefix,
|
||||
this.postfix});
|
||||
|
||||
String verdict(num greenSide, num redSide, int fraction) {
|
||||
var f = NumberFormat("+#,###.##;-#,###.##");
|
||||
f.maximumFractionDigits = fraction;
|
||||
return f.format((greenSide - redSide)) + (postfix ?? "");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var f = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode);
|
||||
f.maximumFractionDigits = fractionDigits ?? 0;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: const [Colors.green, Colors.transparent],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
transform: const GradientRotation(0.6),
|
||||
stops: [
|
||||
0.0,
|
||||
higherIsBetter
|
||||
? greenSide > redSide
|
||||
? 0.6
|
||||
: 0
|
||||
: greenSide < redSide
|
||||
? 0.6
|
||||
: 0
|
||||
],
|
||||
)
|
||||
),
|
||||
child: Text(
|
||||
(prefix ?? "") + f.format(greenSide) + (postfix ?? ""),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
offset: Offset(0.0, 0.0),
|
||||
blurRadius: 1.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
Shadow(
|
||||
offset: Offset(0.0, 0.0),
|
||||
blurRadius: 2.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
Shadow(
|
||||
offset: Offset(0.0, 0.0),
|
||||
blurRadius: 8.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
)),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 22),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
verdict(greenSide, redSide,
|
||||
fractionDigits != null ? fractionDigits! + 2 : 0),
|
||||
style: verdictStyle,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: const [Colors.red, Colors.transparent],
|
||||
begin: Alignment.centerRight,
|
||||
end: Alignment.centerLeft,
|
||||
transform: const GradientRotation(-0.6),
|
||||
stops: [
|
||||
0.0,
|
||||
higherIsBetter
|
||||
? redSide > greenSide
|
||||
? 0.6
|
||||
: 0
|
||||
: redSide < greenSide
|
||||
? 0.6
|
||||
: 0
|
||||
],
|
||||
)),
|
||||
child: Text(
|
||||
(prefix ?? "") + f.format(redSide) + (postfix ?? ""),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
offset: Offset(0.0, 0.0),
|
||||
blurRadius: 3.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
Shadow(
|
||||
offset: Offset(0.0, 0.0),
|
||||
blurRadius: 8.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:tetra_stats/data_objects/distinguishment.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
|
||||
class DistinguishmentThingy extends StatelessWidget{
|
||||
final Distinguishment distinguishment;
|
||||
|
||||
const DistinguishmentThingy(this.distinguishment, {super.key});
|
||||
|
||||
List<InlineSpan> getDistinguishmentTitle(String? text) {
|
||||
// TWC champions don't have header in their distinguishments
|
||||
if (distinguishment.type == "twc") return [TextSpan(text: t.distinguishments.twc, style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.yellowAccent))];
|
||||
// In case if it missing for some other reason, return this
|
||||
if (text == null) return [TextSpan(text: t.distinguishments.noHeader, style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.redAccent))];
|
||||
|
||||
// Handling placeholders for logos
|
||||
var exploded = text.split(" "); // wtf PHP reference?
|
||||
List<InlineSpan> result = [];
|
||||
for (String shit in exploded){
|
||||
switch (shit) { // if %% thingy was found, insert svg of icon
|
||||
case "%osk%":
|
||||
result.add(WidgetSpan(child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: SvgPicture.asset("res/icons/osk.svg", height: 28),
|
||||
)));
|
||||
break;
|
||||
case "%tetrio%":
|
||||
result.add(WidgetSpan(child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: SvgPicture.asset("res/icons/tetrio-logo.svg", height: 28),
|
||||
)));
|
||||
break;
|
||||
default: // if not, insert text span
|
||||
result.add(TextSpan(text: " $shit", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Distinguishment title is barely predictable thing.
|
||||
/// Receives [text], which is footer and returns sets of widgets for RichText widget
|
||||
String getDistinguishmentSubtitle(String? text){
|
||||
// TWC champions don't have footer in their distinguishments
|
||||
if (distinguishment.type == "twc") return "${t.distinguishments.twcYear(year: distinguishment.detail!)}";
|
||||
// In case if it missing for some other reason, return this
|
||||
if (text == null) return t.distinguishments.noFooter;
|
||||
// If everything ok, return as it is
|
||||
return text;
|
||||
}
|
||||
|
||||
Color getCardTint(String type, String detail){
|
||||
switch(type){
|
||||
case "staff":
|
||||
switch(detail){
|
||||
case "founder": return const Color(0xAAFD82D4);
|
||||
case "kagarin": return const Color(0xAAFF0060);
|
||||
case "team": return const Color(0xAAFACC2E);
|
||||
case "team-minor": return const Color(0xAAF5BD45);
|
||||
case "administrator": return const Color(0xAAFF4E8A);
|
||||
case "globalmod": return const Color(0xAAE878FF);
|
||||
case "communitymod": return const Color(0xAA4E68FB);
|
||||
case "alumni": return const Color(0xAA6057DB);
|
||||
default: return theme.colorScheme.surface;
|
||||
}
|
||||
case "champion":
|
||||
switch (detail){
|
||||
case "blitz":
|
||||
case "40l": return const Color(0xAACCF5F6);
|
||||
case "league": return const Color(0xAAFFDB31);
|
||||
}
|
||||
case "twc": return const Color(0xAAFFDB31);
|
||||
default: return theme.colorScheme.surface;
|
||||
}
|
||||
return theme.colorScheme.surface;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
surfaceTintColor: getCardTint(distinguishment.type, distinguishment.detail??"null"),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Text(t.distinguishment, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
|
||||
const Spacer()
|
||||
],
|
||||
),
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: getDistinguishmentTitle(distinguishment.header),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0),
|
||||
child: Text(getDistinguishmentSubtitle(distinguishment.footer), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/services/crud_exceptions.dart';
|
||||
import 'package:tetra_stats/views/destination_home.dart';
|
||||
|
||||
class ErrorThingy extends StatelessWidget{
|
||||
final FetchResults? data;
|
||||
final String? eText;
|
||||
|
||||
const ErrorThingy({this.data, this.eText});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
IconData icon = Icons.error_outline;
|
||||
String errText = eText??"";
|
||||
String? subText;
|
||||
if (data?.exception != null) switch (data!.exception!.runtimeType){
|
||||
case TetrioPlayerNotExist:
|
||||
icon = Icons.search_off;
|
||||
errText = t.errors.noSuchUser;
|
||||
subText = t.errors.noSuchUserSub;
|
||||
break;
|
||||
case TetrioDiscordNotExist:
|
||||
icon = Icons.search_off;
|
||||
errText = t.errors.discordNotAssigned;
|
||||
subText = t.errors.discordNotAssignedSub;
|
||||
case ConnectionIssue:
|
||||
var err = data!.exception as ConnectionIssue;
|
||||
errText = t.errors.connection(code: err.code, message: err.message);
|
||||
break;
|
||||
case TetrioForbidden:
|
||||
icon = Icons.remove_circle;
|
||||
errText = t.errors.forbidden;
|
||||
subText = t.errors.forbiddenSub(nickname: 'osk');
|
||||
break;
|
||||
case TetrioTooManyRequests:
|
||||
errText = t.errors.tooManyRequests;
|
||||
subText = t.errors.tooManyRequestsSub;
|
||||
break;
|
||||
case TetrioOskwareBridgeProblem:
|
||||
errText = t.errors.oskwareBridge;
|
||||
subText = t.errors.oskwareBridgeSub;
|
||||
break;
|
||||
case TetrioInternalProblem:
|
||||
errText = kIsWeb ? t.errors.internalWebVersion : t.errors.internal;
|
||||
subText = kIsWeb ? t.errors.internalWebVersionSub : t.errors.internalSub;
|
||||
break;
|
||||
case ClientException:
|
||||
errText = t.errors.clientException;
|
||||
break;
|
||||
default:
|
||||
errText = data!.exception.toString();
|
||||
}
|
||||
return TweenAnimationBuilder(
|
||||
duration: Durations.medium3,
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
curve: Easing.standard,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
transform: Matrix4.translationValues(0, 50-value*50, 0),
|
||||
child: Opacity(opacity: value, child: child),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Spacer(),
|
||||
Icon(icon, size: 128.0, color: Colors.red, shadows: [
|
||||
Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red),
|
||||
Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red),
|
||||
]),
|
||||
Text(errText, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
|
||||
if (subText != null) Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(subText, textAlign: TextAlign.center),
|
||||
),
|
||||
Spacer()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/main.dart';
|
||||
|
||||
class FakeDistinguishmentThingy extends StatelessWidget{
|
||||
final bool banned;
|
||||
final bool badStanding;
|
||||
final bool bot;
|
||||
final String? botMaintainers;
|
||||
|
||||
FakeDistinguishmentThingy({super.key, this.banned = false, this.badStanding = false, this.bot = false, this.botMaintainers});
|
||||
|
||||
Color getCardTint(){
|
||||
if (banned) return Colors.red;
|
||||
if (badStanding) return Colors.redAccent;
|
||||
if (bot) return const Color.fromARGB(255, 60, 93, 55);
|
||||
return theme.colorScheme.surface;
|
||||
}
|
||||
|
||||
InlineSpan getDistinguishmentTitle() {
|
||||
String text = "";
|
||||
if (banned) text = t.banned;
|
||||
if (badStanding) text = t.badStanding;
|
||||
if (bot) text = t.botAccount;
|
||||
return TextSpan(text: text.toUpperCase(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white));
|
||||
}
|
||||
|
||||
String getDistinguishmentSubtitle(){
|
||||
if (banned) return t.bannedSubtext;
|
||||
if (badStanding) return t.badStandingSubtext;
|
||||
if (bot) return t.botAccountSubtext(botMaintainers: botMaintainers!);
|
||||
return "";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
surfaceTintColor: getCardTint(),
|
||||
child: Container(
|
||||
decoration: banned ? const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.transparent, Color.fromARGB(171, 244, 67, 54), Color.fromARGB(171, 244, 67, 54)],
|
||||
stops: [0.1, 0.9, 0.01],
|
||||
tileMode: TileMode.mirror,
|
||||
begin: Alignment.topLeft,
|
||||
end: AlignmentDirectional(-0.95, -0.95)
|
||||
)
|
||||
) : null,
|
||||
child: Column(
|
||||
children: [
|
||||
Center(
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [getDistinguishmentTitle()],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0),
|
||||
child: Text(getDistinguishmentSubtitle(), style: Theme.of(context).textTheme.displayLarge, textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/data_objects/finesse.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
import 'package:tetra_stats/utils/text_shadow.dart';
|
||||
|
||||
|
@ -28,7 +29,7 @@ class FinesseThingy extends StatelessWidget{
|
|||
fontSize: 65,
|
||||
height: 1.2,
|
||||
)),
|
||||
const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))),
|
||||
Positioned(left: 25, top: 20, child: Text(t.stats.finesse.widgetTitle, style: TextStyle(fontFamily: "Eurostile Round Extended"))),
|
||||
Positioned(
|
||||
right: 0, top: 20,
|
||||
child: Text("${finesse != null ? finesse!.faults : "---"}F", style: TextStyle(
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FutureError extends StatelessWidget{
|
||||
final AsyncSnapshot snapshot;
|
||||
|
||||
FutureError(this.snapshot);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TweenAnimationBuilder(
|
||||
duration: Durations.medium3,
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
curve: Easing.standard,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
transform: Matrix4.translationValues(0, 50-value*50, 0),
|
||||
child: Opacity(opacity: value, child: child),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Spacer(),
|
||||
Icon(Icons.error_outline, size: 128.0, color: Colors.red, shadows: [
|
||||
Shadow(offset: Offset(0.0, 0.0), blurRadius: 30.0, color: Colors.red),
|
||||
Shadow(offset: Offset(0.0, 0.0), blurRadius: 80.0, color: Colors.red),
|
||||
]),
|
||||
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.left, style: TextStyle(fontFamily: "Monospace")),
|
||||
),
|
||||
Spacer()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
||||
import 'package:tetra_stats/gen/strings.g.dart';
|
||||
import 'package:tetra_stats/utils/colors_functions.dart';
|
||||
import 'package:tetra_stats/utils/numers_formats.dart';
|
||||
|
||||
class GaugetNum extends StatelessWidget {
|
||||
final num playerStat;
|
||||
final num? oldPlayerStat;
|
||||
final bool higherIsBetter;
|
||||
final List<GaugeRange> ranges;
|
||||
final double minimum;
|
||||
final double maximum;
|
||||
final String playerStatLabel;
|
||||
final String? okText;
|
||||
final String? alertTitle;
|
||||
final List<Widget>? alertWidgets;
|
||||
final LeaderboardPosition? pos;
|
||||
final num? averageStat;
|
||||
|
||||
const GaugetNum(
|
||||
{super.key,
|
||||
required this.playerStat,
|
||||
required this.playerStatLabel,
|
||||
this.alertWidgets,
|
||||
this.oldPlayerStat,
|
||||
required this.higherIsBetter,
|
||||
required this.minimum,
|
||||
required this.maximum,
|
||||
required this.ranges,
|
||||
this.okText, this.alertTitle, this.pos, this.averageStat});
|
||||
|
||||
Color getStatColor(){
|
||||
if (averageStat == null) return Colors.white;
|
||||
num percentile = (higherIsBetter ? playerStat / averageStat! : averageStat! / playerStat).abs();
|
||||
if (percentile > 1.50) return Colors.purpleAccent;
|
||||
else if (percentile > 1.20) return Colors.blueAccent;
|
||||
else if (percentile > 0.90) return Colors.greenAccent;
|
||||
else if (percentile > 0.70) return Colors.yellowAccent;
|
||||
else return Colors.redAccent;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
height: 120,
|
||||
child: SfRadialGauge(
|
||||
title: GaugeTitle(text: playerStatLabel),
|
||||
axes: [RadialAxis(
|
||||
startAngle: 180,
|
||||
endAngle: 360,
|
||||
showLabels: false,
|
||||
showTicks: false,
|
||||
radiusFactor: 2.1,
|
||||
centerY: 0.5,
|
||||
minimum: minimum,
|
||||
maximum: maximum,
|
||||
ranges: ranges,
|
||||
pointers: [
|
||||
NeedlePointer(
|
||||
value: playerStat as double,
|
||||
enableAnimation: true,
|
||||
needleLength: 0.9,
|
||||
needleStartWidth: 2,
|
||||
needleEndWidth: 15,
|
||||
knobStyle: const KnobStyle(color: Colors.transparent),
|
||||
gradient: const LinearGradient(colors: [Colors.transparent, Colors.white], begin: Alignment.bottomCenter, end: Alignment.topCenter, stops: [0.5, 1]),)
|
||||
],
|
||||
annotations: [GaugeAnnotation(
|
||||
widget: TextButton(child: Text(f3.format(playerStat),
|
||||
style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, color: getStatColor())),
|
||||
onPressed: (){
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(alertTitle??playerStatLabel, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
|
||||
content: SingleChildScrollView(child: ListBody(children: alertWidgets!)),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(okText??t.popupActions.ok),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
));
|
||||
},), verticalAlignment: GaugeAlignment.far, positionFactor: 0.05),
|
||||
if (oldPlayerStat != null || pos != null) GaugeAnnotation(
|
||||
widget: RichText(text: TextSpan(
|
||||
text: "",
|
||||
style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey),
|
||||
children: [
|
||||
if (oldPlayerStat != null) TextSpan(text: comparef.format(playerStat - oldPlayerStat!), style: TextStyle(
|
||||
color: higherIsBetter ?
|
||||
oldPlayerStat! > playerStat ? Colors.redAccent : Colors.greenAccent :
|
||||
oldPlayerStat! < playerStat ? Colors.redAccent : Colors.greenAccent
|
||||
),),
|
||||
if (oldPlayerStat != null && pos != null) const TextSpan(text: " • "),
|
||||
if (pos != null) TextSpan(text: pos!.position >= 1000 ? "${t.top} ${f2.format(pos!.percentage*100)}%" : "№${pos!.position}", style: TextStyle(color: getColorOfRank(pos!.position)))
|
||||
]
|
||||
),
|
||||
),
|
||||
positionFactor: 0.05)],
|
||||
)],),
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue