Merge pull request #233 from dan63047/master

Sync with stable
This commit is contained in:
dan63047 2025-01-04 00:50:08 +03:00 committed by GitHub
commit 8d0a315ae0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
268 changed files with 21583 additions and 15513 deletions

View File

@ -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.

63
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -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

1
.github/ISSUE_TEMPLATE/config.yaml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -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.

View File

@ -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

View File

@ -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 }}

View File

@ -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)

View File

@ -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

View File

@ -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

5
crowdin.yml Normal file
View File

@ -0,0 +1,5 @@
files:
- source: /res/i18n/strings.i18n.json
ignore:
- /res/i18n/old_*.json
translation: /res/i18n/strings_%locale%.i18n.%file_extension%

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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']);
}
}

View File

@ -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);
}
}
}

View File

@ -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']);
}
}

View File

@ -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);
}
}

View File

@ -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']);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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))
)
)
)
))
);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
// ignore_for_file: hash_and_equals
class LeaderboardPosition{
int position;
double percentage;
LeaderboardPosition(this.position, this.percentage);
}

View File

@ -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;
}
}

View File

@ -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)];
}
}

View File

@ -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']);
}
}

View File

@ -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];
}
}

View File

@ -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;
}
}

View File

@ -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];
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));}
}
}

View File

@ -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']);
}
}

View File

@ -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;
}
}

View File

@ -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}";
}
}

View File

@ -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));}
}
}

View File

@ -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

View File

@ -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]
};

View File

@ -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));
} }
} }

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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'];
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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( dropdownMenuTheme: DropdownMenuThemeData(textStyle: TextStyle(fontFamily: "Eurostile Round", fontSize: 18)),
path: "/", scaffoldBackgroundColor: Colors.black,
builder: (_, __) => const MainView(), tooltipTheme: TooltipThemeData(
routes: [ textStyle: TextStyle(color: Colors.white),
GoRoute( decoration: BoxDecoration(
path: 'settings', borderRadius: BorderRadius.all(Radius.circular(8.0)),
builder: (_, __) => const SettingsView(), border: Border.all(
routes: [ color: Colors.white
GoRoute( ),
path: 'customization', color: Colors.black,
builder: (_, __) => const CustomizationView(),
),
]
),
GoRoute(
path: "leaderboard",
builder: (_, __) => const TLLeaderboardView(),
routes: [
GoRoute(
path: "LBvalues",
builder: (_, __) => const RankAveragesView(),
),
]
),
GoRoute(
path: "LBvalues",
builder: (_, __) => const RankAveragesView(),
),
GoRoute(
path: 'states',
builder: (_, __) => const TrackedPlayersView(),
),
GoRoute(
path: 'calc',
builder: (_, __) => const CalcView(),
),
GoRoute(
path: 'sprintAndBlitzAverages',
builder: (_, __) => const SprintAndBlitzView(),
)
]
),
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){

View File

@ -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;
}
} }

View File

@ -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; // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard);
// int lbLength = 100; // if (cached != null) return cached;
// 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: Uri url;
// Stream<TetrioPlayersLeaderboard> fetchTLLeaderboardAsStream() async { if (kIsWeb) {
// TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); url = Uri.https(webVersionDomain, 'oskware_bridge.php', {
// if (cached != null) return cached; "endpoint": "leaderboard",
"lb": lb??"league",
if (prisecter != null) "after": prisecter,
if (country != null) "country": country
});
} else {
url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', {
"limit": "100",
if (prisecter != null) "after": prisecter,
if (country != null) "country": country
});
}
try{
final response = await client.get(url);
// Uri url; switch (response.statusCode) {
// if (kIsWeb) { case 200:
// url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); _lbPositions.clear();
// } else { var rawJson = jsonDecode(response.body);
// url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); 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);
}
}
// Stream<TetrioPlayersLeaderboard> stream = http.StreamedRequest("GET", url); 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));

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
import 'package:flutter/services.dart';
Future<void> copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text));
}

View File

@ -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){

View File

@ -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))}";
} }

147
lib/views/about_view.dart Normal file
View File

@ -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"));})
])),
],
),
],
)
],
),
)),
);
}
}

View File

@ -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

View File

@ -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;
});
}),)
],
)),
);
}
}

View File

@ -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;
});})
],
);
}
}

View File

@ -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?");
}
);
}
}

View File

@ -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

View File

@ -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),
),
)
],
);
}
}

View File

@ -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)),
],
);
}
}

View File

@ -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>");
},
);
}
}

View File

@ -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)
],
);
}
}

View File

@ -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

View File

@ -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)))],
);
}
}
)
)
);
}
}

View File

@ -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);
}

455
lib/views/rank_view.dart Normal file
View File

@ -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))
]
)),
),
),
);
}
}

View File

@ -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");
}
})
),
);
}
}

View File

@ -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: (){},),
// ],
// ),
],
)),
);
}
}

View File

@ -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)
) },
], ),
) )
) )
), ),

View File

@ -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))))
], ],
), ),
), ),

View File

@ -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!)
],
),
)
) )
); );
} }

View File

@ -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));
}
)));}
}

View File

@ -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");
}
})),
);
}
}

View File

@ -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>[
TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
onPressed: () {
roundSelector = -1;
setState(() {});
}, child: Text(t.matchStats)),
TextButton( style: roundSelector == -2 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
onPressed: timeWeightedStatsAvaliable ? () {
roundSelector = -2;
setState(() {});
} : null, child: Text(t.timeWeightedmatchStats)) ,
],
)
]),
], ],
) )
),
SliverToBoxAdapter(
child: TextButton( style: roundSelector == -1 ? ButtonStyle(backgroundColor: WidgetStatePropertyAll(Colors.grey.shade900)) : null,
onPressed: () {
roundSelector = -1;
setState(() {});
}, child: Text(t.tlMatchView.matchStats)),
) )
]; ];
}, },
@ -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 {

View File

@ -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]),
),
);
},
);
}));
}
})),
);
}
}

64
lib/views/user_view.dart Normal file
View File

@ -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);
}
)
)
);
}
}

View File

@ -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),
),
)
),
);
}
}

View File

@ -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))),
),
);
}
}

View File

@ -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);
}
)
)
],
),
)
],
),
);
}
}

View File

@ -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))),
),
);
}
}

View File

@ -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,
),
)),
],
),
);
}
}

View File

@ -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),
),
],
),
);
}
}

View File

@ -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()
],
),
);
}
}

View File

@ -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),
),
],
),
),
);
}
}

View File

@ -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(

View File

@ -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()
],
),
);
}
}

View File

@ -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