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
|
discussionCategory: autobuilded-releases
|
||||||
artifacts: "build/windows/x64/runner/Release/TetraStats-${{github.ref_name}}-windows.zip"
|
artifacts: "build/windows/x64/runner/Release/TetraStats-${{github.ref_name}}-windows.zip"
|
||||||
tag: Auto-${{ github.run_number }}
|
tag: Auto-${{ github.run_number }}
|
||||||
body: Builded with GitHub Action workflow
|
body: Build with GitHub Action workflow
|
||||||
token: ${{ secrets.TOKEN }}
|
token: ${{ secrets.TOKEN }}
|
||||||
build-and-release-linux:
|
build-and-release-linux:
|
||||||
name: Build Linux App
|
name: Build Linux App
|
||||||
|
@ -71,7 +71,7 @@ jobs:
|
||||||
discussionCategory: autobuilded-releases
|
discussionCategory: autobuilded-releases
|
||||||
artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
|
artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
|
||||||
tag: Auto-${{ github.run_number }}
|
tag: Auto-${{ github.run_number }}
|
||||||
body: Builded with GitHub Action workflow
|
body: Build with GitHub Action workflow
|
||||||
token: ${{ secrets.TOKEN }}
|
token: ${{ secrets.TOKEN }}
|
||||||
# build-and-release-android:
|
# build-and-release-android:
|
||||||
# name: Build Android App
|
# name: Build Android App
|
||||||
|
@ -96,5 +96,5 @@ jobs:
|
||||||
# discussionCategory: autobuilded-releases
|
# discussionCategory: autobuilded-releases
|
||||||
# artifacts: "build/app/outputs/flutter-apk/*"
|
# artifacts: "build/app/outputs/flutter-apk/*"
|
||||||
# tag: Auto-${{ github.run_number }}
|
# tag: Auto-${{ github.run_number }}
|
||||||
# body: Builded with GitHub Action workflow
|
# body: Build with GitHub Action workflow
|
||||||
# token: ${{ secrets.TOKEN }}
|
# token: ${{ secrets.TOKEN }}
|
19
README.md
19
README.md
|
@ -2,26 +2,27 @@
|
||||||
|
|
||||||
Track your and other players stats in TETR.IO
|
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).
|
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)
|
![Screenshot of the app 1](https://imgur.com/e8CYvj3.png)
|
||||||
|
|
||||||
# Available functionality
|
# Available functionality
|
||||||
- Advanced stats for players
|
- Advanced stats for players
|
||||||
|
- Charts for analyzing players Tetra League standing and Tetra League itself
|
||||||
- Ranks cutoffs
|
- Ranks cutoffs
|
||||||
- Minimums, averages, and maximums for every stat of every rank, as well, as whole leaderboard
|
- Full and sortable Tetra League leagerboard
|
||||||
- Chart for analyzing tetra league state
|
- Stats and Damage Calculator
|
||||||
- Local database, that can store players data
|
- 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
|
# Special thanks
|
||||||
- **kerrmunism** — formulas
|
- **kerrmunism** — formulas
|
||||||
- **p1nkl0bst3r** — providing players history and peak TR
|
- **p1nkl0bst3r** — providing players history and peak TR
|
||||||
|
- **neko_ab4093** — Simplified Chinese localization
|
||||||
- **osk** and his team — TETR.IO
|
- **osk** and his team — TETR.IO
|
||||||
|
|
||||||
## Legal note
|
## Legal notes
|
||||||
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)
|
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,
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
# 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:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
|
|
|
@ -51,6 +51,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.dan63.tetra_stats"
|
applicationId "com.dan63.tetra_stats"
|
||||||
|
testApplicationId "com.dan63.tetra_stats.dev_build"
|
||||||
// You can update the following values to match your application needs.
|
// 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.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
minSdkVersion flutter.minSdkVersion
|
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:math';
|
||||||
import 'dart:typed_data';
|
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
|
// I want to implement those fancy TWC stats
|
||||||
// So, i'm going to read replay for things
|
// So, i'm going to read replay for things
|
||||||
|
@ -32,8 +37,11 @@ int biggestSpikeFromReplay(events){
|
||||||
class Garbage{ // charsys where???
|
class Garbage{ // charsys where???
|
||||||
late int sent;
|
late int sent;
|
||||||
late int recived;
|
late int recived;
|
||||||
late int attack;
|
int? attack;
|
||||||
late int cleared;
|
int? cleared;
|
||||||
|
int? sent_normal;
|
||||||
|
int? maxspike;
|
||||||
|
int? maxspike_nomult;
|
||||||
|
|
||||||
Garbage({
|
Garbage({
|
||||||
required this.sent,
|
required this.sent,
|
||||||
|
@ -47,6 +55,9 @@ class Garbage{ // charsys where???
|
||||||
recived = json['received'];
|
recived = json['received'];
|
||||||
attack = json['attack'];
|
attack = json['attack'];
|
||||||
cleared = json['cleared'];
|
cleared = json['cleared'];
|
||||||
|
sent_normal = json['sent_normal'];
|
||||||
|
maxspike = json['maxspike'];
|
||||||
|
maxspike_nomult = json['maxspike_nomult'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Garbage.toJson(){
|
Garbage.toJson(){
|
||||||
|
@ -54,7 +65,7 @@ class Garbage{ // charsys where???
|
||||||
}
|
}
|
||||||
|
|
||||||
Garbage operator + (Garbage other){
|
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:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:tetra_stats/services/tetrio_crud.dart';
|
import 'package:tetra_stats/services/tetrio_crud.dart';
|
||||||
import 'package:tetra_stats/views/customization_view.dart';
|
import 'package:tetra_stats/views/first_time_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:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
|
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
|
||||||
import 'package:tetra_stats/gen/strings.g.dart';
|
import 'package:tetra_stats/gen/strings.g.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:tetra_stats/views/main_view.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';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
late final PackageInfo packageInfo;
|
late final PackageInfo packageInfo;
|
||||||
late SharedPreferences prefs;
|
late SharedPreferences prefs;
|
||||||
late TetrioService teto;
|
late TetrioService teto;
|
||||||
|
late GoRouter router;
|
||||||
|
|
||||||
ThemeData theme = ThemeData(
|
ThemeData theme = ThemeData(
|
||||||
fontFamily: 'Eurostile Round',
|
fontFamily: 'Eurostile Round',
|
||||||
colorScheme: const ColorScheme.dark(
|
colorScheme: const ColorScheme.dark(
|
||||||
|
@ -32,6 +28,12 @@ ThemeData theme = ThemeData(
|
||||||
surface: Color.fromARGB(255, 10, 10, 10),
|
surface: Color.fromARGB(255, 10, 10, 10),
|
||||||
secondary: Color(0xFF00838F),
|
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)),
|
cardTheme: const CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)),
|
||||||
drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)),
|
drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)),
|
||||||
searchBarTheme: const SearchBarThemeData(
|
searchBarTheme: const SearchBarThemeData(
|
||||||
|
@ -44,65 +46,31 @@ ThemeData theme = ThemeData(
|
||||||
),
|
),
|
||||||
segmentedButtonTheme: SegmentedButtonThemeData(
|
segmentedButtonTheme: SegmentedButtonThemeData(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
|
||||||
side: const WidgetStatePropertyAll(BorderSide(color: Colors.transparent)),
|
side: const WidgetStatePropertyAll(BorderSide(color: Colors.transparent)),
|
||||||
surfaceTintColor: const WidgetStatePropertyAll(Colors.cyanAccent),
|
surfaceTintColor: const WidgetStatePropertyAll(Colors.cyanAccent),
|
||||||
iconColor: const WidgetStatePropertyAll(Colors.cyanAccent),
|
iconColor: const WidgetStatePropertyAll(Colors.cyanAccent),
|
||||||
shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200),
|
shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: Colors.black
|
dividerColor: Color.fromARGB(50, 158, 158, 158),
|
||||||
);
|
dividerTheme: DividerThemeData(color: Color.fromARGB(50, 158, 158, 158)),
|
||||||
|
expansionTileTheme: ExpansionTileThemeData(
|
||||||
final router = GoRouter(
|
expansionAnimationStyle: AnimationStyle(curve: Easing.standard, reverseCurve: Easing.standard),
|
||||||
initialLocation: "/",
|
expandedAlignment: Alignment.bottomCenter,
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: "/",
|
|
||||||
builder: (_, __) => const MainView(),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: 'settings',
|
|
||||||
builder: (_, __) => const SettingsView(),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: 'customization',
|
|
||||||
builder: (_, __) => const CustomizationView(),
|
|
||||||
),
|
),
|
||||||
]
|
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(
|
color: Colors.black,
|
||||||
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(),
|
|
||||||
)
|
)
|
||||||
]
|
|
||||||
),
|
|
||||||
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 {
|
void main() async {
|
||||||
|
@ -128,6 +96,24 @@ void main() async {
|
||||||
prefs = await SharedPreferences.getInstance();
|
prefs = await SharedPreferences.getInstance();
|
||||||
teto = TetrioService();
|
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
|
// Choosing the locale
|
||||||
String? locale = prefs.getString("locale");
|
String? locale = prefs.getString("locale");
|
||||||
if (locale == null){
|
if (locale == null){
|
||||||
|
|
|
@ -86,4 +86,24 @@ class DB {
|
||||||
var newDBStats = await dbFile.stat();
|
var newDBStats = await dbFile.stat();
|
||||||
return dbStats.size - newDBStats.size;
|
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:convert';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:sqflite/sql.dart';
|
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.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_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:tetra_stats/main.dart' show packageInfo;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:tetra_stats/services/custom_http_client.dart';
|
import 'package:tetra_stats/services/custom_http_client.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:tetra_stats/services/crud_exceptions.dart';
|
import 'package:tetra_stats/services/crud_exceptions.dart';
|
||||||
import 'package:tetra_stats/services/sqlite_db_controller.dart';
|
import 'package:tetra_stats/services/sqlite_db_controller.dart';
|
||||||
import 'package:tetra_stats/data_objects/tetrio.dart';
|
|
||||||
import 'package:csv/csv.dart';
|
import 'package:csv/csv.dart';
|
||||||
|
|
||||||
const String dbName = "TetraStats.db";
|
const String dbName = "TetraStats.db";
|
||||||
|
const String webVersionDomain = "ts.dan63.by";
|
||||||
const String tetrioUsersTable = "tetrioUsers";
|
const String tetrioUsersTable = "tetrioUsers";
|
||||||
const String tetrioUsersToTrackTable = "tetrioUsersToTrack";
|
const String tetrioUsersToTrackTable = "tetrioUsersToTrack";
|
||||||
const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces";
|
const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces";
|
||||||
|
@ -33,6 +50,11 @@ const String endContext2 = "endContext2";
|
||||||
const String statesCol = "jsonStates";
|
const String statesCol = "jsonStates";
|
||||||
const String player1id = "player1id";
|
const String player1id = "player1id";
|
||||||
const String player2id = "player2id";
|
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
|
/// Table, that store players data, their stats at some moments of time
|
||||||
const String createTetrioUsersTable = '''
|
const String createTetrioUsersTable = '''
|
||||||
CREATE TABLE IF NOT EXISTS "tetrioUsers" (
|
CREATE TABLE IF NOT EXISTS "tetrioUsers" (
|
||||||
|
@ -268,7 +290,7 @@ class TetrioService extends DB {
|
||||||
// If failed, actually trying to retrieve
|
// If failed, actually trying to retrieve
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
|
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
|
} else { // Actually going to hit inoue
|
||||||
url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
|
url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
|
||||||
}
|
}
|
||||||
|
@ -337,6 +359,25 @@ class TetrioService extends DB {
|
||||||
return data;
|
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).
|
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
|
||||||
/// Throws an exception if fails to retrieve.
|
/// Throws an exception if fails to retrieve.
|
||||||
Future<SingleplayerStream> fetchStream(String userID, String stream) async {
|
Future<SingleplayerStream> fetchStream(String userID, String stream) async {
|
||||||
|
@ -345,7 +386,7 @@ class TetrioService extends DB {
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} else {
|
||||||
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/$stream');
|
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/$stream');
|
||||||
}
|
}
|
||||||
|
@ -393,7 +434,7 @@ class TetrioService extends DB {
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS
|
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
|
} else { // Actually going to hit p1nkl0bst3r api
|
||||||
url = Uri.https('api.p1nkl0bst3r.xyz', 'toptr/$id');
|
url = Uri.https('api.p1nkl0bst3r.xyz', 'toptr/$id');
|
||||||
}
|
}
|
||||||
|
@ -444,7 +485,7 @@ class TetrioService extends DB {
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "cutoffs"});
|
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "cutoffs"});
|
||||||
} else {
|
} else {
|
||||||
url = Uri.https('ch.tetr.io', 'api/labs/league_ranks');
|
url = Uri.https('ch.tetr.io', 'api/labs/league_ranks');
|
||||||
}
|
}
|
||||||
|
@ -485,10 +526,10 @@ class TetrioService extends DB {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Cutoffs?> fetchCutoffsBeanserver() async {
|
Future<Cutoffs?> fetchCutoffsBeanserver() async {
|
||||||
Cutoffs? cached = _cache.get("", Cutoffs);
|
Cutoffs? cached = _cache.get("CutoffsTetrioleague_ranks", Cutoffs);
|
||||||
if (cached != null) return cached;
|
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{
|
try{
|
||||||
final response = await client.get(url);
|
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 {
|
Future<TetrioPlayerFromLeaderboard> fetchTopOneFromTheLeaderboard() async {
|
||||||
TetrioPlayerFromLeaderboard? cached = _cache.get("topone", TetrioPlayerFromLeaderboard);
|
TetrioPlayerFromLeaderboard? cached = _cache.get("topone", TetrioPlayerFromLeaderboard);
|
||||||
if (cached != null) return cached;
|
if (cached != null) return cached;
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLTopOne"});
|
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLTopOne"});
|
||||||
} else {
|
} else {
|
||||||
url = Uri.https('ch.tetr.io', 'api/users/by/league', {"after": "25000:0:0", "limit": "1"});
|
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
|
/// 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.
|
/// (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;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} else {
|
||||||
url = Uri.https('api.p1nkl0bst3r.xyz', 'tlhist/$id');
|
url = Uri.https('api.p1nkl0bst3r.xyz', 'tlhist/$id');
|
||||||
}
|
}
|
||||||
|
@ -652,7 +749,7 @@ class TetrioService extends DB {
|
||||||
Future<TetraLeagueAlphaStream> fetchAndSaveOldTLmatches(String userID) async {
|
Future<TetraLeagueAlphaStream> fetchAndSaveOldTLmatches(String userID) async {
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} else {
|
||||||
url = Uri.https('api.p1nkl0bst3r.xyz', 'tlmatches/$userID', {"before": "0", "count": "9000"});
|
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);
|
TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
|
||||||
if (cached != null) return cached;
|
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{
|
try{
|
||||||
final response = await client.get(url);
|
final response = await client.get(url);
|
||||||
|
@ -728,34 +825,121 @@ class TetrioService extends DB {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream<TetrioPlayersLeaderboard> fetchFullLeaderboard() async* {
|
Future<List<TetrioPlayerFromLeaderboard>> fetchTetrioLeaderboard({String? prisecter, String? lb, String? country}) 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 {
|
|
||||||
// TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
|
// TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
|
||||||
// if (cached != null) return cached;
|
// if (cached != null) return cached;
|
||||||
|
|
||||||
// Uri url;
|
Uri url;
|
||||||
// if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
// url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"});
|
url = Uri.https(webVersionDomain, 'oskware_bridge.php', {
|
||||||
// } else {
|
"endpoint": "leaderboard",
|
||||||
// url = Uri.https('ch.tetr.io', 'api/users/lists/league/all');
|
"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(){
|
TetrioPlayersLeaderboard? getCachedLeaderboard(){
|
||||||
return _cache.get("league", TetrioPlayersLeaderboard);
|
return _cache.get("league", TetrioPlayersLeaderboard);
|
||||||
|
@ -768,7 +952,7 @@ class TetrioService extends DB {
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} else {
|
||||||
url = Uri.https('ch.tetr.io', 'api/news/user_${userID.toLowerCase().trim()}', {"limit": "100"});
|
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).
|
/// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream).
|
||||||
/// Throws an exception if fails to retrieve.
|
/// Throws an exception if fails to retrieve.
|
||||||
Future<TetraLeagueBetaStream> fetchTLStream(String userID) async {
|
Future<TetraLeagueBetaStream> fetchTLStream(String userID, {String? prisecter}) async {
|
||||||
TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream);
|
// TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream);
|
||||||
if (cached != null) return cached;
|
// if (cached != null) return cached;
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} 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 {
|
try {
|
||||||
final response = await client.get(url);
|
final response = await client.get(url);
|
||||||
|
@ -946,7 +1137,7 @@ class TetrioService extends DB {
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} else {
|
||||||
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records');
|
url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records');
|
||||||
}
|
}
|
||||||
|
@ -999,7 +1190,7 @@ class TetrioService extends DB {
|
||||||
|
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} else {
|
||||||
url = Uri.https('ch.tetr.io', 'api/users/$id/summaries');
|
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
|
// trying to find player with given discord id
|
||||||
Uri dUrl;
|
Uri dUrl;
|
||||||
if (kIsWeb) {
|
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 {
|
} 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{
|
try{
|
||||||
final response = await client.get(dUrl);
|
final response = await client.get(dUrl);
|
||||||
|
@ -1182,7 +1373,7 @@ class TetrioService extends DB {
|
||||||
// finally going to obtain
|
// finally going to obtain
|
||||||
Uri url;
|
Uri url;
|
||||||
if (kIsWeb) {
|
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 {
|
} else {
|
||||||
url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}');
|
url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}');
|
||||||
}
|
}
|
||||||
|
@ -1191,7 +1382,7 @@ class TetrioService extends DB {
|
||||||
|
|
||||||
switch (response.statusCode) {
|
switch (response.statusCode) {
|
||||||
case 200:
|
case 200:
|
||||||
var json = jsonDecode(response.body);
|
var json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
if (json['success']) {
|
if (json['success']) {
|
||||||
// parse and count stats
|
// 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));
|
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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
Color getColorOfRank(int rank){
|
Color getColorOfRank(int rank){
|
||||||
|
if (rank < 1) return Colors.grey;
|
||||||
if (rank == 1) return Colors.yellowAccent;
|
if (rank == 1) return Colors.yellowAccent;
|
||||||
if (rank == 2) return Colors.blueGrey;
|
if (rank == 2) return Colors.blueGrey;
|
||||||
if (rank == 3) return Colors.brown[400]!;
|
if (rank == 3) return Colors.brown[400]!;
|
||||||
|
@ -8,3 +9,17 @@ Color getColorOfRank(int rank){
|
||||||
if (rank <= 99) return Colors.greenAccent;
|
if (rank <= 99) return Colors.greenAccent;
|
||||||
return Colors.grey;
|
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:intl/intl.dart';
|
||||||
import 'package:tetra_stats/gen/strings.g.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 comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3;
|
||||||
final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2;
|
final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2;
|
||||||
final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0);
|
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 f1 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 1);
|
||||||
final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode);
|
final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode);
|
||||||
final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2;
|
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
|
/// Readable [a] - [b], without sign
|
||||||
String readableIntDifference(int a, int b){
|
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);
|
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){
|
String getMoreNormalTime(Duration time){
|
||||||
return "${nonsecs.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}";
|
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: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/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/singleplayer_record.dart';
|
||||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
|
||||||
|
|
||||||
class SingleplayerRecordView extends StatelessWidget {
|
class SingleplayerRecordView extends StatelessWidget {
|
||||||
final RecordSingle record;
|
final RecordSingle record;
|
||||||
|
@ -15,28 +15,28 @@ class SingleplayerRecordView extends StatelessWidget {
|
||||||
//bool bigScreen = MediaQuery.of(context).size.width >= 368;
|
//bool bigScreen = MediaQuery.of(context).size.width >= 368;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
appBar: AppBar(
|
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
|
||||||
title: Text("${
|
floatingActionButton: Padding(
|
||||||
switch (record.gamemode){
|
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 0.0, 0.0),
|
||||||
"40l" => t.sprint,
|
child: FloatingActionButton(
|
||||||
"blitz" => t.blitz,
|
onPressed: () => Navigator.pop(context),
|
||||||
String() => "5000000 Blast",
|
tooltip: t.goBackButton,
|
||||||
}
|
child: const Icon(Icons.arrow_back),
|
||||||
} ${timestamp(record.timestamp)}"),
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Row(
|
child: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
constraints: BoxConstraints(
|
||||||
children: [
|
maxWidth: 768
|
||||||
Column(
|
),
|
||||||
children: [
|
child: switch (record.gamemode){
|
||||||
SingleplayerRecord(record: record, hideTitle: true),
|
"zenith" => ZenithCard(record, false, [], width: MediaQuery.of(context).size.width),
|
||||||
// TODO: Insert replay link here
|
"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:flutter/material.dart';
|
||||||
import 'package:intl/intl.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/gen/strings.g.dart';
|
||||||
import 'package:tetra_stats/utils/relative_timestamps.dart';
|
import 'package:tetra_stats/utils/relative_timestamps.dart';
|
||||||
import 'package:tetra_stats/utils/text_shadow.dart';
|
import 'package:tetra_stats/utils/text_shadow.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
late String oldWindowTitle;
|
late String oldWindowTitle;
|
||||||
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode);
|
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode);
|
||||||
|
@ -22,16 +19,16 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
// if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
// windowManager.getTitle().then((value) => oldWindowTitle = value);
|
||||||
windowManager.setTitle("Tetra Stats: ${t.settings}");
|
// windowManager.setTitle("Tetra Stats: ${t.settings}");
|
||||||
}
|
// }
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose(){
|
void dispose(){
|
||||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
// if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,8 +37,14 @@ class SprintAndBlitzState extends State<SprintAndBlitzView> {
|
||||||
final t = Translations.of(context);
|
final t = Translations.of(context);
|
||||||
bool bigScreen = MediaQuery.of(context).size.width >= 368;
|
bool bigScreen = MediaQuery.of(context).size.width >= 368;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
|
||||||
title: Text(t.sprintAndBlitsViewTitle),
|
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,
|
backgroundColor: Colors.black,
|
||||||
body: SafeArea(
|
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)),
|
Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
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(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
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)),
|
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(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
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(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.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/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/text_timestamp.dart';
|
||||||
import 'package:tetra_stats/widgets/tl_thingy.dart';
|
import 'package:tetra_stats/widgets/tl_thingy.dart';
|
||||||
import 'package:tetra_stats/widgets/user_thingy.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
|
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
|
||||||
|
@ -29,7 +30,7 @@ class StateState extends State<StateView> {
|
||||||
_scrollController = ScrollController();
|
_scrollController = ScrollController();
|
||||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
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();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@ -41,20 +42,24 @@ class StateState extends State<StateView> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _justUpdate() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = Translations.of(context);
|
//final t = Translations.of(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
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,
|
backgroundColor: Colors.black,
|
||||||
body: SafeArea(
|
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
|
// ignore_for_file: use_build_context_synchronously, type_literal_in_constant_pattern
|
||||||
|
|
||||||
import 'dart:io';
|
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/data_objects/tetrio_multiplayer_replay.dart';
|
||||||
import 'package:tetra_stats/utils/relative_timestamps.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/list_tile_trailing_stats.dart';
|
||||||
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
import 'package:tetra_stats/widgets/text_timestamp.dart';
|
||||||
import 'package:tetra_stats/widgets/vs_graphs.dart';
|
import 'package:tetra_stats/widgets/vs_graphs.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.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/gen/strings.g.dart';
|
||||||
import 'package:tetra_stats/utils/open_in_browser.dart';
|
import 'package:tetra_stats/utils/open_in_browser.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
@ -43,60 +43,23 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
late String reason;
|
late String reason;
|
||||||
Duration totalTime = const Duration();
|
Duration totalTime = const Duration();
|
||||||
List<Duration> roundLengths = [];
|
List<Duration> roundLengths = [];
|
||||||
List<BetaLeagueStats> timeWeightedStats = [];
|
|
||||||
late bool initPlayerWon;
|
late bool initPlayerWon;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState(){
|
void initState(){
|
||||||
rounds = [DropdownMenuItem(value: -1, child: Text(t.match))];
|
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.roundNumber(n: i+1)))]);
|
rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.tlMatchView.roundNumber(n: i+1)))]);
|
||||||
if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch)));
|
|
||||||
greenSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id == widget.initPlayerId);
|
greenSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id == widget.initPlayerId);
|
||||||
redSidePlayer = 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){
|
for (var round in widget.record.results.rounds){
|
||||||
var longerLifetime = round[0].lifetime.compareTo(round[1].lifetime) == 1 ? round[0].lifetime : round[1].lifetime;
|
var longerLifetime = round[0].lifetime.compareTo(round[1].lifetime) == 1 ? round[0].lifetime : round[1].lifetime;
|
||||||
roundLengths.add(longerLifetime);
|
roundLengths.add(longerLifetime);
|
||||||
totalTime += 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;
|
initPlayerWon = widget.record.results.leaderboard[greenSidePlayer].wins > widget.record.results.leaderboard[redSidePlayer].wins;
|
||||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
|
||||||
windowManager.getTitle().then((value) => oldWindowTitle = value);
|
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();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@ -112,11 +75,11 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
bool bigScreen = width >= 768;
|
bool bigScreen = width >= 768;
|
||||||
if (roundSelector.isNegative){
|
if (roundSelector.isNegative){
|
||||||
time = totalTime;
|
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{
|
}else{
|
||||||
time = roundLengths[roundSelector];
|
time = roundLengths[roundSelector];
|
||||||
int alive = widget.record.results.rounds[roundSelector].indexWhere((element) => element.alive);
|
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(
|
return SizedBox(
|
||||||
width: width,
|
width: width,
|
||||||
|
@ -189,7 +152,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
textBaseline: TextBaseline.alphabetic,
|
||||||
children: [
|
children: [
|
||||||
Text("${t.statsFor}: ",
|
Text("${t.tlMatchView.statsFor}: ",
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 25)),
|
style: const TextStyle(color: Colors.white, fontSize: 25)),
|
||||||
DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) {
|
DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) {
|
||||||
roundSelector = 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))),
|
if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(readableTime, textAlign: TextAlign.center))),
|
||||||
const SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
child: Divider(),
|
child: Divider(),
|
||||||
|
@ -213,38 +173,32 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "APM",
|
label: t.stats.apm.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].apm :
|
greenSide: 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.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,
|
||||||
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,
|
|
||||||
fractionDigits: 2,
|
fractionDigits: 2,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "PPS",
|
label: t.stats.pps.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].pps :
|
greenSide: 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.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,
|
||||||
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,
|
|
||||||
fractionDigits: 2,
|
fractionDigits: 2,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "VS",
|
label: t.stats.vs.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].vs :
|
greenSide: 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.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,
|
||||||
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,
|
|
||||||
fractionDigits: 2,
|
fractionDigits: 2,
|
||||||
higherIsBetter: true,
|
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,
|
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,
|
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,
|
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,
|
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(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -255,142 +209,114 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
fontSize: bigScreen ? 42 : 28)),
|
fontSize: bigScreen ? 42 : 28)),
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "APP",
|
label: t.stats.app.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.app :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "VS/APM",
|
label: t.stats.vsapm.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.vsapm :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "DS/S",
|
label: t.stats.dss.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dss :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "DS/P",
|
label: t.stats.dsp.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dsp :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "APP + DS/P",
|
label: t.stats.appdsp.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.appdsp :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "),
|
label: t.stats.cheese.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.cheese :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 2,
|
fractionDigits: 2,
|
||||||
higherIsBetter: false,
|
higherIsBetter: false,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "Gb Eff.",
|
label: t.stats.gbe.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.gbe :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "wAPP",
|
label: t.stats.nyaapp.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.nyaapp :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "Area",
|
label: t.stats.area.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.area :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 2,
|
fractionDigits: 2,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: t.statCellNum.estOfTRShort,
|
label: t.stats.etr.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].estTr.esttr :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 2,
|
fractionDigits: 2,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "Opener",
|
label: t.stats.opener.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.opener :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "Plonk",
|
label: t.stats.plonk.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.plonk :
|
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,
|
||||||
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.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.plonk : 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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "Stride",
|
label: t.stats.stride.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.stride :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
CompareThingy(
|
CompareThingy(
|
||||||
label: "Inf. DS",
|
label: t.stats.infds.short,
|
||||||
greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.infds :
|
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,
|
||||||
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,
|
||||||
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,
|
|
||||||
fractionDigits: 3,
|
fractionDigits: 3,
|
||||||
higherIsBetter: true,
|
higherIsBetter: true,
|
||||||
),
|
),
|
||||||
VsGraphs(
|
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.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.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.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.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.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.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.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.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.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[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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(t.matchLength),
|
Text(t.tlMatchView.matchLength),
|
||||||
RichText(
|
RichText(
|
||||||
text: !totalTime.isNegative ? TextSpan(
|
text: !totalTime.isNegative ? TextSpan(
|
||||||
text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}",
|
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(
|
if (widget.record.id != widget.record.replayID) Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(t.numberOfRounds),
|
Text(t.tlMatchView.numberOfRounds),
|
||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: widget.record.results.rounds.length.toString(),
|
text: widget.record.results.rounds.length.toString(),
|
||||||
|
@ -476,25 +402,15 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],),
|
],),
|
||||||
Column(children: [
|
],
|
||||||
OverflowBar(
|
)
|
||||||
alignment: MainAxisAlignment.spaceEvenly,
|
),
|
||||||
children: <Widget>[
|
SliverToBoxAdapter(
|
||||||
TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
|
child: TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
roundSelector = -1;
|
roundSelector = -1;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}, child: Text(t.matchStats)),
|
}, child: Text(t.tlMatchView.matchStats)),
|
||||||
TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
|
|
||||||
onPressed: timeWeightedStatsAvaliable ? () {
|
|
||||||
roundSelector = -2;
|
|
||||||
setState(() {});
|
|
||||||
} : null, child: Text(t.timeWeightedmatchStats)) ,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
@ -569,18 +485,21 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
||||||
final t = Translations.of(context);
|
final t = Translations.of(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
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: [
|
actions: [
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
enabled: widget.record.gamemode == "league",
|
enabled: widget.record.gamemode == "league",
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 1,
|
value: 1,
|
||||||
child: Text(t.downloadReplay),
|
child: Text(t.tlMatchView.downloadReplay),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 2,
|
value: 2,
|
||||||
child: Text(t.openReplay),
|
child: Text(t.tlMatchView.openReplay),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (value) async {
|
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
|
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/numers_formats.dart';
|
||||||
import 'package:tetra_stats/utils/text_shadow.dart';
|
import 'package:tetra_stats/utils/text_shadow.dart';
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ class FinesseThingy extends StatelessWidget{
|
||||||
fontSize: 65,
|
fontSize: 65,
|
||||||
height: 1.2,
|
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(
|
Positioned(
|
||||||
right: 0, top: 20,
|
right: 0, top: 20,
|
||||||
child: Text("${finesse != null ? finesse!.faults : "---"}F", style: TextStyle(
|
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