diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index b19a3c6..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -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. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..d9029dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 10cdc29..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..c93b4c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29964e1..737977e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: discussionCategory: autobuilded-releases artifacts: "build/windows/x64/runner/Release/TetraStats-${{github.ref_name}}-windows.zip" tag: Auto-${{ github.run_number }} - body: Builded with GitHub Action workflow + body: Build with GitHub Action workflow token: ${{ secrets.TOKEN }} build-and-release-linux: name: Build Linux App @@ -71,7 +71,7 @@ jobs: discussionCategory: autobuilded-releases artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip" tag: Auto-${{ github.run_number }} - body: Builded with GitHub Action workflow + body: Build with GitHub Action workflow token: ${{ secrets.TOKEN }} # build-and-release-android: # name: Build Android App @@ -96,5 +96,5 @@ jobs: # discussionCategory: autobuilded-releases # artifacts: "build/app/outputs/flutter-apk/*" # tag: Auto-${{ github.run_number }} - # body: Builded with GitHub Action workflow + # body: Build with GitHub Action workflow # token: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 093c477..bf07c5c 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,27 @@ Track your and other players stats in TETR.IO +Tetra Stats works with TETR.IO Tetra Channel API, providing data from it and calculating some addtitional metrics, based on this data. + You can [download an app](https://github.com/dan63047/TetraStats/releases), or [use web version](https://ts.dan63.by). ![Screenshot of the app 1](https://imgur.com/e8CYvj3.png) # Available functionality - Advanced stats for players +- Charts for analyzing players Tetra League standing and Tetra League itself - Ranks cutoffs -- Minimums, averages, and maximums for every stat of every rank, as well, as whole leaderboard -- Chart for analyzing tetra league state +- Full and sortable Tetra League leagerboard +- Stats and Damage Calculator - Local database, that can store players data -- Comparison to players, rank averages, and player stats from the past -- Stats Calculator -- Player history in charts -- Tetra League matches history -- Time-weighted stats in Tetra League matches # Special thanks - **kerrmunism** — formulas - **p1nkl0bst3r** — providing players history and peak TR +- **neko_ab4093** — Simplified Chinese localization - **osk** and his team — TETR.IO -## Legal note -I do NOT own any assets located in `/res/*`, excluding app icon (`/res/icons/app.png`) and localization (`/res/i18n/*`), which is distributed under GNU license (as well, as this software) \ No newline at end of file +## Legal notes +Tetra Stats is not associated with TETR.IO or osk in any capacity. + +I do NOT own any assets located in `/res/*`, excluding app icon (`/res/icons/app.png`), localization (`/res/i18n/*`) and images (`/res/images/*`), which is distributed under GNU license (as well, as this software) \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index a7acf24..f3cece1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,10 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +analyzer: + errors: + use_build_context_synchronously: ignore +in ignoreclude: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the diff --git a/android/app/build.gradle b/android/app/build.gradle index 53aeec3..2fa991b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,6 +51,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.dan63.tetra_stats" + testApplicationId "com.dan63.tetra_stats.dev_build" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..7beef69 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,5 @@ +files: + - source: /res/i18n/strings.i18n.json + ignore: + - /res/i18n/old_*.json + translation: /res/i18n/strings_%locale%.i18n.%file_extension% diff --git a/lib/data_objects/achievement.dart b/lib/data_objects/achievement.dart new file mode 100644 index 0000000..4558d6b --- /dev/null +++ b/lib/data_objects/achievement.dart @@ -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 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 toJson() { + final Map data = {}; + 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; + } +} diff --git a/lib/data_objects/aggregate_stats.dart b/lib/data_objects/aggregate_stats.dart new file mode 100644 index 0000000..7500585 --- /dev/null +++ b/lib/data_objects/aggregate_stats.dart @@ -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 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); + } +} diff --git a/lib/data_objects/badge.dart b/lib/data_objects/badge.dart new file mode 100644 index 0000000..69976e6 --- /dev/null +++ b/lib/data_objects/badge.dart @@ -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 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 toJson() { + final Map data = {}; + 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; +} diff --git a/lib/data_objects/beta_league_leaderboard_entry.dart b/lib/data_objects/beta_league_leaderboard_entry.dart new file mode 100644 index 0000000..34672b4 --- /dev/null +++ b/lib/data_objects/beta_league_leaderboard_entry.dart @@ -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 json){ + id = json['id']; + username = json['username']; + naturalorder = json['naturalorder']; + wins = json['wins']; + stats = BetaLeagueStats.fromJson(json['stats']); + } +} diff --git a/lib/data_objects/beta_league_results.dart b/lib/data_objects/beta_league_results.dart new file mode 100644 index 0000000..2d4ad50 --- /dev/null +++ b/lib/data_objects/beta_league_results.dart @@ -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 leaderboard = []; + List> rounds = []; + + BetaLeagueResults({required this.leaderboard, required this.rounds}); + + BetaLeagueResults.fromJson(Map json){ + for (var lbEntry in json['leaderboard']) { + leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry)); + } + for (var roundEntry in json['rounds']){ + List round = []; + for (var r in roundEntry) { + round.add(BetaLeagueRound.fromJson(r)); + } + rounds.add(round); + } + } +} diff --git a/lib/data_objects/beta_league_round.dart b/lib/data_objects/beta_league_round.dart new file mode 100644 index 0000000..fdaf81b --- /dev/null +++ b/lib/data_objects/beta_league_round.dart @@ -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 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']); + } +} diff --git a/lib/data_objects/beta_league_stats.dart b/lib/data_objects/beta_league_stats.dart new file mode 100644 index 0000000..88e1c56 --- /dev/null +++ b/lib/data_objects/beta_league_stats.dart @@ -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 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); + } +} diff --git a/lib/data_objects/beta_record.dart b/lib/data_objects/beta_record.dart new file mode 100644 index 0000000..aecbe75 --- /dev/null +++ b/lib/data_objects/beta_record.dart @@ -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 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']); + } +} diff --git a/lib/data_objects/clears.dart b/lib/data_objects/clears.dart new file mode 100644 index 0000000..c63a72a --- /dev/null +++ b/lib/data_objects/clears.dart @@ -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 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 toJson() { + final Map data = {}; + 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; + } +} diff --git a/lib/data_objects/connections.dart b/lib/data_objects/connections.dart new file mode 100644 index 0000000..ca0ae83 --- /dev/null +++ b/lib/data_objects/connections.dart @@ -0,0 +1,43 @@ +// ignore_for_file: hash_and_equals + +class Connections { + Discord? discord; + + Connections({this.discord}); + + Connections.fromJson(Map json) { + discord = json['discord'] != null ? Discord.fromJson(json['discord']) : null; + } + @override + bool operator ==(covariant Connections other) => discord == other.discord; + + Map toJson() { + final Map data = {}; + 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 json) { + id = json['id']; + username = json['username']; + } + + @override + bool operator ==(covariant Discord other) => id == other.id; + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['username'] = username; + return data; + } +} diff --git a/lib/data_objects/cutoff_tetrio.dart b/lib/data_objects/cutoff_tetrio.dart new file mode 100644 index 0000000..83b3379 --- /dev/null +++ b/lib/data_objects/cutoff_tetrio.dart @@ -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 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 data = {}; + + CutoffsTetrio.fromJson(Map 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); + } + } +} \ No newline at end of file diff --git a/lib/data_objects/distinguishment.dart b/lib/data_objects/distinguishment.dart new file mode 100644 index 0000000..143ad17 --- /dev/null +++ b/lib/data_objects/distinguishment.dart @@ -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 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 toJson() { + final Map data = {}; + data['type'] = type; + data['detail'] = detail; + data['header'] = header; + data['footer'] = footer; + return data; + } +} diff --git a/lib/data_objects/end_context_multi.dart b/lib/data_objects/end_context_multi.dart new file mode 100644 index 0000000..d92c2d3 --- /dev/null +++ b/lib/data_objects/end_context_multi.dart @@ -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 nerdStatsTracking; + late EstTr estTr; + late List estTrTracking; + late Playstyle playstyle; + late List 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 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 toJson() { + final Map data = {}; + 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; + } +} diff --git a/lib/data_objects/est_tr.dart b/lib/data_objects/est_tr.dart new file mode 100644 index 0000000..d378a4a --- /dev/null +++ b/lib/data_objects/est_tr.dart @@ -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)) + ) + ) + ) + )) + ); + } +} diff --git a/lib/data_objects/finesse.dart b/lib/data_objects/finesse.dart new file mode 100644 index 0000000..6e982b5 --- /dev/null +++ b/lib/data_objects/finesse.dart @@ -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 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 toJson() { + final Map data = {}; + data['combo'] = combo; + data['faults'] = faults; + data['perfectpieces'] = perfectPieces; + return data; + } +} diff --git a/lib/data_objects/handling.dart b/lib/data_objects/handling.dart new file mode 100644 index 0000000..345f3e7 --- /dev/null +++ b/lib/data_objects/handling.dart @@ -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 json) { + arr = json['arr']; + das = json['das']; + dcd = json['dcd']; + sdf = json['sdf']; + safeLock = json['safelock']; + cancel = json['cancel']; + } + + Map toJson() { + final Map data = {}; + data['arr'] = arr.toDouble(); + data['das'] = das.toDouble(); + data['dcd'] = dcd.toDouble(); + data['sdf'] = sdf.toDouble(); + data['safelock'] = safeLock; + data['cancel'] = cancel; + return data; + } +} diff --git a/lib/data_objects/leaderboard_position.dart b/lib/data_objects/leaderboard_position.dart new file mode 100644 index 0000000..3d9e76f --- /dev/null +++ b/lib/data_objects/leaderboard_position.dart @@ -0,0 +1,8 @@ +// ignore_for_file: hash_and_equals + +class LeaderboardPosition{ + int position; + double percentage; + + LeaderboardPosition(this.position, this.percentage); +} diff --git a/lib/data_objects/nerd_stats.dart b/lib/data_objects/nerd_stats.dart new file mode 100644 index 0000000..6ad8730 --- /dev/null +++ b/lib/data_objects/nerd_stats.dart @@ -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; + } +} diff --git a/lib/data_objects/news.dart b/lib/data_objects/news.dart new file mode 100644 index 0000000..ccbca45 --- /dev/null +++ b/lib/data_objects/news.dart @@ -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 news; + + News(this.id, this.news); + + News.fromJson(Map json, String? userID){ + id = userID != null ? "user_$userID" : json['news'].first['stream']; + news = [for (var entry in json['news']) NewsEntry.fromJson(entry)]; + } +} diff --git a/lib/data_objects/news_entry.dart b/lib/data_objects/news_entry.dart new file mode 100644 index 0000000..6d125f1 --- /dev/null +++ b/lib/data_objects/news_entry.dart @@ -0,0 +1,15 @@ +// ignore_for_file: hash_and_equals + +class NewsEntry { + late String type; + late Map data; + late DateTime timestamp; + + NewsEntry({required this.type, required this.data, required this.timestamp}); + + NewsEntry.fromJson(Map json){ + type = json["type"]; + data = json["data"]; + timestamp = DateTime.parse(json['ts']); + } +} diff --git a/lib/data_objects/tetra_stats.dart b/lib/data_objects/p1nkl0bst3r.dart similarity index 100% rename from lib/data_objects/tetra_stats.dart rename to lib/data_objects/p1nkl0bst3r.dart diff --git a/lib/data_objects/player_leaderboard_position.dart b/lib/data_objects/player_leaderboard_position.dart new file mode 100644 index 0000000..3f76ccc --- /dev/null +++ b/lib/data_objects/player_leaderboard_position.dart @@ -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 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]; + } +} diff --git a/lib/data_objects/playstyle.dart b/lib/data_objects/playstyle.dart new file mode 100644 index 0000000..89cf43e --- /dev/null +++ b/lib/data_objects/playstyle.dart @@ -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; + } +} diff --git a/lib/data_objects/record_extras.dart b/lib/data_objects/record_extras.dart new file mode 100644 index 0000000..7003b86 --- /dev/null +++ b/lib/data_objects/record_extras.dart @@ -0,0 +1,45 @@ +// ignore_for_file: hash_and_equals + +class RecordExtras{ + +} + +class ZenithExtras extends RecordExtras{ + List mods = []; + + ZenithExtras.fromJson(Map 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 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> league = {}; + + LeagueExtras.fromJson(Map 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]; + } + } +} diff --git a/lib/data_objects/record_single.dart b/lib/data_objects/record_single.dart new file mode 100644 index 0000000..d875a95 --- /dev/null +++ b/lib/data_objects/record_single.dart @@ -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 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; + switch (ex.keys.firstOrNull){ + case "zenith": + extras = ZenithExtras.fromJson(json['extras']['zenith']); + default: + break; + } + } + + Map toJson() { + final Map data = {}; + data['_id'] = ownId; + data['results']['stats'] = stats.toJson(); + data['ismulti'] = false; + data['replayid'] = replayId; + data['ts'] = timestamp; + data['user_id'] = userId; + return data; + } +} diff --git a/lib/data_objects/results_stats.dart b/lib/data_objects/results_stats.dart new file mode 100644 index 0000000..3178e07 --- /dev/null +++ b/lib/data_objects/results_stats.dart @@ -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 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 toJson() { + final Map data = {}; + 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; + } +} diff --git a/lib/data_objects/singleplayer_stream.dart b/lib/data_objects/singleplayer_stream.dart new file mode 100644 index 0000000..a59f74b --- /dev/null +++ b/lib/data_objects/singleplayer_stream.dart @@ -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 records; + + SingleplayerStream({required this.userId, required this.records, required this.type}); + + SingleplayerStream.fromJson(List json, String userID, String tp) { + userId = userID; + type = tp; + records = []; + for (var value in json) {records.add(RecordSingle.fromJson(value, -1, -1));} + } +} diff --git a/lib/data_objects/summaries.dart b/lib/data_objects/summaries.dart new file mode 100644 index 0000000..78a726a --- /dev/null +++ b/lib/data_objects/summaries.dart @@ -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 achievements; + late TetraLeague league; + Map pastLeague = {}; + late TetrioZen zen; + + Summaries(this.id, this.league, this.zen); + + Summaries.fromJson(Map 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']); + } +} diff --git a/lib/data_objects/tetra_league.dart b/lib/data_objects/tetra_league.dart new file mode 100644 index 0000000..86e5e29 --- /dev/null +++ b/lib/data_objects/tetra_league.dart @@ -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 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 toJson() { + final Map data = {}; + 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; + } +} diff --git a/lib/data_objects/tetra_league_alpha_record.dart b/lib/data_objects/tetra_league_alpha_record.dart new file mode 100644 index 0000000..ad59825 --- /dev/null +++ b/lib/data_objects/tetra_league_alpha_record.dart @@ -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 endContext; + + TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext, required this.replayAvalable}); + + TetraLeagueAlphaRecord.fromJson(Map 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 toJson() { + final Map data = {}; + 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}"; + } +} diff --git a/lib/data_objects/tetra_league_alpha_stream.dart b/lib/data_objects/tetra_league_alpha_stream.dart new file mode 100644 index 0000000..ebce2ac --- /dev/null +++ b/lib/data_objects/tetra_league_alpha_stream.dart @@ -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 records; + + TetraLeagueAlphaStream({required this.userId, required this.records}); + + TetraLeagueAlphaStream.fromJson(List json, String userID) { + userId = userID; + records = []; + for (var value in json) {records.add(TetraLeagueAlphaRecord.fromJson(value));} + } +} diff --git a/lib/data_objects/tetra_league_beta_stream.dart b/lib/data_objects/tetra_league_beta_stream.dart new file mode 100644 index 0000000..f51a135 --- /dev/null +++ b/lib/data_objects/tetra_league_beta_stream.dart @@ -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 records = []; + + TetraLeagueBetaStream({required this.id, required this.records}); + + TetraLeagueBetaStream.fromJson(List json, String userID) { + id = userID; + for (var entry in json) { + records.add(BetaRecord.fromJson(entry)); + } + } + + addFromAlphaStream(List 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 ranks = [ - "d", "d+", "c-", "c", "c+", "b-", "b", "b+", "a-", "a", "a+", "s-", "s", "s+", "ss", "u", "x", "x+" -]; -const Map 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 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 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 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) -}; - -const Map sprintAverages = { // based on https://discord.com/channels/673303546107658242/674421736162197515/1244287342965952562 - 'x': Duration(seconds: 25, milliseconds: 144), - 'u': Duration(seconds: 36, milliseconds: 115), - 'ss': Duration(seconds: 46, milliseconds: 396), - 's+': Duration(seconds: 55, milliseconds: 056), - 's': Duration(seconds: 61, milliseconds: 892), - 's-': Duration(seconds: 68, milliseconds: 918), - 'a+': Duration(seconds: 76, milliseconds: 187), - 'a': Duration(seconds: 83, milliseconds: 529), - 'a-': Duration(seconds: 88, milliseconds: 608), - 'b+': Duration(seconds: 97, milliseconds: 626), - 'b': Duration(seconds: 104, milliseconds: 687), - 'b-': Duration(seconds: 113, milliseconds: 372), - 'c+': Duration(seconds: 123, milliseconds: 461), - 'c': Duration(seconds: 135, milliseconds: 326), - 'c-': Duration(seconds: 147, milliseconds: 056), - 'd+': Duration(seconds: 156, milliseconds: 757), - 'd': Duration(seconds: 167, milliseconds: 393), - //'z': Duration(seconds: 66, milliseconds: 802) -}; - -const Map blitzAverages = { - 'x': 600715, - 'u': 379418, - 'ss': 233740, - 's+': 158295, - 's': 125164, - 's-': 100933, - 'a+': 83593, - 'a': 68613, - 'a-': 60219, - 'b+': 51197, - 'b': 44171, - 'b-': 39045, - 'c+': 34130, - 'c': 28931, - 'c-': 25095, - 'd+': 22944, - 'd': 20728, - //'z': 72084 -}; - -String getStatNameByEnum(Stats stat){ - return t[stat.name]; -} - -Duration doubleSecondsToDuration(double value) { - value = value * 1000000; - return Duration(microseconds: value.floor()); -} - -Duration doubleMillisecondsToDuration(double value) { - value = value * 1000; - return Duration(microseconds: value.floor()); -} - -class TetrioPlayer { - late String userId; - late String username; - late DateTime state; - late String role; - int? avatarRevision; - int? bannerRevision; - DateTime? registrationTime; - List 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, - 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 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 ? doubleSecondsToDuration(json['gametime'].toDouble()) : 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 toJson() { - final Map data = {}; - // data['_id'] = userId; - // data['username'] = username; - data['role'] = role; - if (registrationTime != null) 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); -} - -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 achievements; - late TetraLeague league; - late TetrioZen zen; - - Summaries(this.id, this.league, this.zen); - - Summaries.fromJson(Map 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['40l']['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) zenithCareerBest = RecordSingle.fromJson(json['zenithex']['best']['record'], json['zenith']['best']['rank'], -1); - achievements = [for (var achievement in json['achievements']) Achievement.fromJson(achievement)]; - league = TetraLeague.fromJson(json['league'], DateTime.now(), currentSeason, i); - zen = TetrioZen.fromJson(json['zen']); - } -} - -class Badge { - late String badgeId; - late String label; - DateTime? ts; - - Badge({required this.badgeId, required this.label, this.ts}); - - Badge.fromJson(Map 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 toJson() { - final Map data = {}; - 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; -} - -class Connections { - Discord? discord; - - Connections({this.discord}); - - Connections.fromJson(Map json) { - discord = json['discord'] != null ? Discord.fromJson(json['discord']) : null; - } - @override - bool operator ==(covariant Connections other) => discord == other.discord; - - Map toJson() { - final Map data = {}; - if (discord != null) { - data['discord'] = discord!.toJson(); - } - return data; - } -} - -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 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 toJson() { - final Map data = {}; - 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; - } -} - -class Discord { - late String id; - late String username; - - Discord({required this.id, required this.username}); - - Discord.fromJson(Map json) { - id = json['id']; - username = json['username']; - } - - @override - bool operator ==(covariant Discord other) => id == other.id; - - Map toJson() { - final Map data = {}; - data['id'] = id; - data['username'] = username; - return data; - } -} - -class Finesse { - late int combo; - late int faults; - late int perfectPieces; - - Finesse({required this.combo, required this.faults, required this.perfectPieces}); - - Finesse.fromJson(Map 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 toJson() { - final Map data = {}; - data['combo'] = combo; - data['faults'] = faults; - data['perfectpieces'] = perfectPieces; - return data; - } -} - -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 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 json) { - seed = json['seed']?.toDouble(); - lines = json['lines']; - inputs = json['inputs']; - holds = json['holds'] ?? 0; - finalTime = doubleMillisecondsToDuration(json['finaltime'].toDouble()); - score = json['score']; - level = json['level']; - topCombo = json['topcombo']; - topBtB = json['topbtb']; - tSpins = json['tspins']; - piecesPlaced = json['piecesplaced']; - clears = Clears.fromJson(json['clears']); - kills = json['kills']; - if (json.containsKey("finesse")) finesse = Finesse.fromJson(json['finesse']); - if (json.containsKey("zenith")) zenith = ZenithResults.fromJson(json['zenith']); - } - - Map toJson() { - final Map data = {}; - 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; - } -} - -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 splits; - - ZenithResults.fromJson(Map 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)); - } - } -} - -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 json) { - arr = json['arr']; - das = json['das']; - dcd = json['dcd']; - sdf = json['sdf']; - safeLock = json['safelock']; - cancel = json['cancel']; - } - - Map toJson() { - final Map data = {}; - data['arr'] = arr.toDouble(); - data['das'] = das.toDouble(); - data['dcd'] = dcd.toDouble(); - data['sdf'] = sdf.toDouble(); - data['safelock'] = safeLock; - data['cancel'] = cancel; - return data; - } -} - -class NerdStats { - final double _apm; - final double _pps; - final double _vs; - 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(this._apm, this._pps, this._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; - } -} - -class EstTr { - final double _apm; - final double _pps; - final double _vs; - final double _app; - final double _dss; - final double _dsp; - final double _gbe; - late double esttr; - late double srarea; - late double statrank; - late double estglicko; - - EstTr(this._apm, this._pps, this._vs, this._app, this._dss, this._dsp, this._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)) - ) - ) - ) - )) - ); - } -} - -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}); - - Achievement.fromJson(Map 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 toJson() { - final Map data = {}; - 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; - } -} - - -class Playstyle { - final double _apm; - final double _pps; - //final double _vs; - final double _app; - final double _vsapm; - //final double _dss; - final double _dsp; - final double _gbe; - final double _srarea; - final double _statrank; - late double opener; - late double plonk; - late double stride; - late double infds; - - Playstyle(this._apm, this._pps, this._app, this._vsapm, this._dsp, this._gbe, this._srarea, this._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; - } -} - -class TetraLeagueAlphaStream{ - late String userId; - late List records; - - TetraLeagueAlphaStream({required this.userId, required this.records}); - - TetraLeagueAlphaStream.fromJson(List json, String userID) { - userId = userID; - records = []; - for (var value in json) {records.add(TetraLeagueAlphaRecord.fromJson(value));} - } -} - -class TetraLeagueBetaStream{ - late String id; - List records = []; - - TetraLeagueBetaStream({required this.id, required this.records}); - - TetraLeagueBetaStream.fromJson(List json, String userID) { - id = userID; - for (var entry in json) { - records.add(BetaRecord.fromJson(entry)); - } - } - - addFromAlphaStream(List 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 records; - - SingleplayerStream({required this.userId, required this.records, required this.type}); - - SingleplayerStream.fromJson(List json, String userID, String tp) { - userId = userID; - type = tp; - records = []; - for (var value in json) {records.add(RecordSingle.fromJson(value, -1, -1));} - } -} - -class BetaRecord{ - late String id; - late String replayID; - late String gamemode; - late DateTime ts; - late String enemyUsername; - late String enemyID; - late BetaLeagueResults results; - - 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 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']); - } -} - -class BetaLeagueResults{ - List leaderboard = []; - List> rounds = []; - - BetaLeagueResults({required this.leaderboard, required this.rounds}); - - BetaLeagueResults.fromJson(Map json){ - for (var lbEntry in json['leaderboard']) { - leaderboard.add(BetaLeagueLeaderboardEntry.fromJson(lbEntry)); - } - for (var roundEntry in json['rounds']){ - List round = []; - for (var r in roundEntry) { - round.add(BetaLeagueRound.fromJson(r)); - } - rounds.add(round); - } - } -} - -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 json){ - id = json['id']; - username = json['username']; - naturalorder = json['naturalorder']; - wins = json['wins']; - stats = BetaLeagueStats.fromJson(json['stats']); - } -} - -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 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); - } -} - -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 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']); - } -} - -class TetraLeagueAlphaRecord{ - late String replayId; - late String ownId; - late DateTime timestamp; - late bool replayAvalable; - late List endContext; - - TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext, required this.replayAvalable}); - - TetraLeagueAlphaRecord.fromJson(Map 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 toJson() { - final Map data = {}; - 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}"; - } -} - -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 nerdStatsTracking; - late EstTr estTr; - late List estTrTracking; - late Playstyle playstyle; - late List 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 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 toJson() { - final Map data = {}; - 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; - } -} - -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 json, ts, int s, String i) { - timestamp = ts; - 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'] ?? -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); - - Map toJson() { - final Map data = {}; - 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; - } -} - -class RecordSingle { - late String? userId; - late String replayId; - late String ownId; - late String gamemode; - late DateTime timestamp; - late ResultsStats stats; - late int rank; - late int countryRank; - late AggregateStats aggregateStats; - late RecordExtras extras; - - RecordSingle({required this.userId, required this.replayId, required this.ownId, required this.timestamp, required this.stats, required this.rank, required this.countryRank, required this.aggregateStats}); - - RecordSingle.fromJson(Map 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']; - rank = ran; - countryRank = cran; - aggregateStats = AggregateStats.fromJson(json['results']['aggregatestats']); - var ex = json['extras'] as Map; - switch (ex.keys.firstOrNull){ - case "zenith": - extras = ZenithExtras.fromJson(json['extras']['zenith']); - default: - break; - } - } - - Map toJson() { - final Map data = {}; - data['_id'] = ownId; - data['results']['stats'] = stats.toJson(); - data['ismulti'] = false; - data['replayid'] = replayId; - data['ts'] = timestamp; - data['user_id'] = userId; - return data; - } -} - -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.fromJson(Map 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); - } -} - -class RecordExtras{ - -} - -class ZenithExtras extends RecordExtras{ - List mods = []; - - ZenithExtras.fromJson(Map json){ - for (var mod in json["mods"]) { - mods.add(mod); - } - } -} - -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 json) { - level = json['level']; - score = json['score']; - } - - Map toJson() { - final Map data = {}; - data['level'] = level; - data['score'] = score; - return data; - } -} - -class UserRecords{ - String id; - RecordSingle? sprint; - RecordSingle? blitz; - TetrioZen zen; - - UserRecords(this.id, this.sprint, this.blitz, this.zen); -} - -class Distinguishment { - late String type; - String? detail; - String? header; - String? footer; - - Distinguishment({required this.type, this.detail, this.header, this.footer}); - - Distinguishment.fromJson(Map 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 toJson() { - final Map data = {}; - data['type'] = type; - data['detail'] = detail; - data['header'] = header; - data['footer'] = footer; - return data; - } -} - -class News{ - late String id; - late List news; - - News(this.id, this.news); - - News.fromJson(Map json, String? userID){ - id = userID != null ? "user_$userID" : json['news'].first['stream']; - news = [for (var entry in json['news']) NewsEntry.fromJson(entry)]; - } -} - -class NewsEntry { - //late String id; do i need it? - late String type; - late Map data; - late DateTime timestamp; - - NewsEntry({required this.type, required this.data, required this.timestamp}); - - NewsEntry.fromJson(Map json){ - //id = json["_id"]; - type = json["type"]; - data = json["data"]; - timestamp = DateTime.parse(json['ts']); - } -} - -class PlayerLeaderboardPosition{ - late LeaderboardPosition? apm; - late LeaderboardPosition? pps; - late LeaderboardPosition? vs; - late LeaderboardPosition? gamesPlayed; - late LeaderboardPosition? gamesWon; - late LeaderboardPosition? winrate; - 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.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 results){ - apm = results[0]; - pps = results[1]; - vs = results[2]; - gamesPlayed = results[3]; - gamesWon = results[4]; - winrate = results[5]; - app = results[6]; - vsapm = results[7]; - dss = results[8]; - dsp = results[9]; - appdsp = results[10]; - cheese = results[11]; - gbe = results[12]; - nyaapp = results[13]; - area = results[14]; - estTr = results[15]; - accOfEst = results[16]; - } -} - -class LeaderboardPosition{ - int position; - double percentage; - - LeaderboardPosition(this.position, this.percentage); -} - -class TetrioPlayersLeaderboard { - late String type; - late DateTime timestamp; - late List leaderboard; - - TetrioPlayersLeaderboard(this.type, this.leaderboard); - - @override - String toString(){ - return "$type leaderboard: ${leaderboard.length} players"; - } - - List getStatRanking(List leaderboard, Stats stat, {bool reversed = false, String country = ""}){ - List 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 getAverageOfRank(String rank){ // i tried to refactor it and that's was terrible - if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank"); - List 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; - 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; - avgGamesPlayed = (totalGamesPlayed / filtredLeaderboard.length).floor(); - avgGamesWon = (totalGamesWon / filtredLeaderboard.length).floor(); - 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), - { - "everyone": rank == "", - "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, - "toEnterTR": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].tr : lowestTR, - "toEnterGlicko": rank.toLowerCase() != "z" ? leaderboard[(leaderboard.length * rankCutoffs[rank]!).floor()-1].glicko : 0, - "entries": filtredLeaderboard - }]; - }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(Mapleague) { - if (league.values.first.gamesPlayed == 0) return null; - bool fakePositions = false; - late List 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.apm, Stats.pps, Stats.vs, Stats.gp, Stats.gw, Stats.wr, - Stats.app, Stats.vsapm, Stats.dss, Stats.dsp, Stats.appdsp, Stats.cheese, Stats.gbe, Stats.nyaapp, Stats.area, Stats.eTR, Stats.acceTR]; - List results = []; - for (Stats stat in stats) { - List 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> get averages => { - 'x+': getAverageOfRank("x+"), - 'x': getAverageOfRank("x"), - 'u': getAverageOfRank("u"), - 'ss': getAverageOfRank("ss"), - 's+': getAverageOfRank("s+"), - 's': getAverageOfRank("s"), - 's-': getAverageOfRank("s-"), - 'a+': getAverageOfRank("a+"), - 'a': getAverageOfRank("a"), - 'a-': getAverageOfRank("a-"), - 'b+': getAverageOfRank("b+"), - 'b': getAverageOfRank("b"), - 'b-': getAverageOfRank("b-"), - 'c+': getAverageOfRank("c+"), - 'c': getAverageOfRank("c"), - 'c-': getAverageOfRank("c-"), - 'd+': getAverageOfRank("d+"), - 'd': getAverageOfRank("d"), - 'z': getAverageOfRank("z") - }; - - Map get cutoffs => { - 'x': getAverageOfRank("x")[1]["toEnterTR"], - 'u': getAverageOfRank("u")[1]["toEnterTR"], - 'ss': getAverageOfRank("ss")[1]["toEnterTR"], - 's+': getAverageOfRank("s+")[1]["toEnterTR"], - 's': getAverageOfRank("s")[1]["toEnterTR"], - 's-': getAverageOfRank("s-")[1]["toEnterTR"], - 'a+': getAverageOfRank("a+")[1]["toEnterTR"], - 'a': getAverageOfRank("a")[1]["toEnterTR"], - 'a-': getAverageOfRank("a-")[1]["toEnterTR"], - 'b+': getAverageOfRank("b+")[1]["toEnterTR"], - 'b': getAverageOfRank("b")[1]["toEnterTR"], - 'b-': getAverageOfRank("b-")[1]["toEnterTR"], - 'c+': getAverageOfRank("c+")[1]["toEnterTR"], - 'c': getAverageOfRank("c")[1]["toEnterTR"], - 'c-': getAverageOfRank("c-")[1]["toEnterTR"], - 'd+': getAverageOfRank("d+")[1]["toEnterTR"], - 'd': getAverageOfRank("d")[1]["toEnterTR"] - }; - - Map get cutoffsGlicko => { - 'x': getAverageOfRank("x")[1]["toEnterGlicko"], - 'u': getAverageOfRank("u")[1]["toEnterGlicko"], - 'ss': getAverageOfRank("ss")[1]["toEnterGlicko"], - 's+': getAverageOfRank("s+")[1]["toEnterGlicko"], - 's': getAverageOfRank("s")[1]["toEnterGlicko"], - 's-': getAverageOfRank("s-")[1]["toEnterGlicko"], - 'a+': getAverageOfRank("a+")[1]["toEnterGlicko"], - 'a': getAverageOfRank("a")[1]["toEnterGlicko"], - 'a-': getAverageOfRank("a-")[1]["toEnterGlicko"], - 'b+': getAverageOfRank("b+")[1]["toEnterGlicko"], - 'b': getAverageOfRank("b")[1]["toEnterGlicko"], - 'b-': getAverageOfRank("b-")[1]["toEnterGlicko"], - 'c+': getAverageOfRank("c+")[1]["toEnterGlicko"], - 'c': getAverageOfRank("c")[1]["toEnterGlicko"], - 'c-': getAverageOfRank("c-")[1]["toEnterGlicko"], - 'd+': getAverageOfRank("d+")[1]["toEnterGlicko"], - 'd': getAverageOfRank("d")[1]["toEnterGlicko"] - }; - - TetrioPlayersLeaderboard.fromJson(List json, String t, DateTime ts) { - type = t; - timestamp = ts; - leaderboard = []; - for (Map entry in json) { - leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts)); - } - } - - addPlayers(List list){ - leaderboard.addAll(list); - } -} - -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; - - 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){ - 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 esttracc => estTr.esttr - tr; - double get s1tr => gxe * 250; - - TetrioPlayerFromLeaderboard.fromJson(Map 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']??-1; - 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']; - 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; - } - } -} - -class CutoffTetrio { - late int pos; - late double percentile; - late double tr; - late double targetTr; - late double apm; - late double pps; - late double vs; - late int count; - late double countPercentile; - - CutoffTetrio.fromJson(Map 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; - } -} - -class CutoffsTetrio { - late String id; - late DateTime timestamp; - late int total; - Map data = {}; - - CutoffsTetrio.fromJson(Map 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); - } - } -} \ No newline at end of file diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart new file mode 100644 index 0000000..7a031f5 --- /dev/null +++ b/lib/data_objects/tetrio_constants.dart @@ -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 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 ranks = [ + "d", + "d+", + "c-", + "c", + "c+", + "b-", + "b", + "b+", + "a-", + "a", + "a+", + "s-", + "s", + "s+", + "ss", + "u", + "x", + "x+" +]; +const List ranks2 = [ + "top1", + "x+", + "x", + "u", + "ss", + "s+", + "s", + "s-", + "a+", + "a", + "a-", + "b+", + "b", + "b-", + "c+", + "c", + "c-", + "d+", + "d" +]; +const Map 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 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 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 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 achievementColors = [ + Colors.grey, + Color(0xFFB38070), // bronze + Color(0xFF7E9EA7), // silver + Color(0xFFE2A042), // gold + Color(0xFF70D0A3), // platinum + Color(0xFFD590FF), // diamond + Colors.white, +]; + +const Map 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 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 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 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 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 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 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> 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] +}; \ No newline at end of file diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index e4dd836..e2c14bc 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -1,7 +1,12 @@ import 'dart:math'; import 'dart:typed_data'; -import 'tetrio.dart'; +import 'package:tetra_stats/data_objects/clears.dart'; +import 'package:tetra_stats/data_objects/end_context_multi.dart'; +import 'package:tetra_stats/data_objects/est_tr.dart'; +import 'package:tetra_stats/data_objects/finesse.dart'; +import 'package:tetra_stats/data_objects/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/playstyle.dart'; // I want to implement those fancy TWC stats // So, i'm going to read replay for things @@ -32,8 +37,11 @@ int biggestSpikeFromReplay(events){ class Garbage{ // charsys where??? late int sent; late int recived; - late int attack; - late int cleared; + int? attack; + int? cleared; + int? sent_normal; + int? maxspike; + int? maxspike_nomult; Garbage({ required this.sent, @@ -47,6 +55,9 @@ class Garbage{ // charsys where??? recived = json['received']; attack = json['attack']; cleared = json['cleared']; + sent_normal = json['sent_normal']; + maxspike = json['maxspike']; + maxspike_nomult = json['maxspike_nomult']; } Garbage.toJson(){ @@ -54,7 +65,7 @@ class Garbage{ // charsys where??? } Garbage operator + (Garbage other){ - return Garbage(sent: sent + other.sent, recived: recived + other.recived, attack: attack + other.attack, cleared: cleared + other.cleared); + return Garbage(sent: sent + other.sent, recived: recived + other.recived, attack: attack??0 + (other.attack??0), cleared: (cleared??0) + (other.cleared??0)); } } diff --git a/lib/data_objects/tetrio_player.dart b/lib/data_objects/tetrio_player.dart new file mode 100644 index 0000000..1746942 --- /dev/null +++ b/lib/data_objects/tetrio_player.dart @@ -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 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 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 toJson() { + final Map data = {}; + // 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); +} diff --git a/lib/data_objects/tetrio_player_from_leaderboard.dart b/lib/data_objects/tetrio_player_from_leaderboard.dart new file mode 100644 index 0000000..80ba9b8 --- /dev/null +++ b/lib/data_objects/tetrio_player_from_leaderboard.dart @@ -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 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 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; + } + } +} diff --git a/lib/data_objects/tetrio_players_leaderboard.dart b/lib/data_objects/tetrio_players_leaderboard.dart new file mode 100644 index 0000000..91b8aa4 --- /dev/null +++ b/lib/data_objects/tetrio_players_leaderboard.dart @@ -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 leaderboard; + + TetrioPlayersLeaderboard(this.type, this.leaderboard); + + @override + String toString(){ + return "$type leaderboard: ${leaderboard.length} players"; + } + + List getStatRanking(List leaderboard, Stats stat, {bool reversed = false, String country = ""}){ + List 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 getStatRankingFromLB(Stats stat, {bool reversed = false, String country = ""}){ + List 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 getRankData(String rank){ + if (rank.isNotEmpty && !rankCutoffs.keys.contains(rank)) throw Exception("Invalid rank"); + List 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(Mapleague) { + if (league.values.first.gamesPlayed == 0) return null; + bool fakePositions = false; + late List 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.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 results = []; + for (Stats stat in stats) { + List 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> 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 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 json, String t, DateTime ts) { + type = t; + timestamp = ts; + leaderboard = []; + for (Map entry in json) { + leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, ts)); + } + } + + addPlayers(List list){ + leaderboard.addAll(list); + } +} diff --git a/lib/data_objects/tetrio_prisecter.dart b/lib/data_objects/tetrio_prisecter.dart new file mode 100644 index 0000000..6595cb1 --- /dev/null +++ b/lib/data_objects/tetrio_prisecter.dart @@ -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 json){ + pri = json['pri']; + sec = json['sec']; + ter = json['ter']; + } +} \ No newline at end of file diff --git a/lib/data_objects/tetrio_zen.dart b/lib/data_objects/tetrio_zen.dart new file mode 100644 index 0000000..97e085c --- /dev/null +++ b/lib/data_objects/tetrio_zen.dart @@ -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 json) { + level = json['level']; + score = json['score']; + } + + Map toJson() { + final Map data = {}; + data['level'] = level; + data['score'] = score; + return data; + } +} diff --git a/lib/data_objects/user_records.dart b/lib/data_objects/user_records.dart new file mode 100644 index 0000000..cac93d4 --- /dev/null +++ b/lib/data_objects/user_records.dart @@ -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); +} diff --git a/lib/data_objects/zenith_results.dart b/lib/data_objects/zenith_results.dart new file mode 100644 index 0000000..d3efdb1 --- /dev/null +++ b/lib/data_objects/zenith_results.dart @@ -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 splits; + + ZenithResults.fromJson(Map 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)); + } + } +} diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index a8b8209..1565380 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -3,10 +3,10 @@ /// Original: res/i18n /// To regenerate, run: `dart run slang` /// -/// Locales: 2 -/// Strings: 1210 (605 per locale) +/// Locales: 3 +/// Strings: 2295 (765 per locale) /// -/// Built on 2024-09-04 at 20:41 UTC +/// Built on 2024-12-31 at 17:29 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -26,7 +26,8 @@ const AppLocale _baseLocale = AppLocale.en; /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { en(languageCode: 'en', build: Translations.build), - ru(languageCode: 'ru', build: _StringsRu.build); + ruRu(languageCode: 'ru', countryCode: 'RU', build: _StringsRuRu.build), + zhCn(languageCode: 'zh', countryCode: 'CN', build: _StringsZhCn.build); const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element @@ -150,236 +151,19 @@ class Translations implements BaseTranslations { // Translations Map get locales => { 'en': 'English', - 'ru': 'Russian (Русский)', + 'ru-RU': 'Russian (Русский)', + 'zh-CN': 'Simplified Chinese (简体中文)', }; - String get tetraLeague => 'Tetra League'; - String get tlRecords => 'TL Records'; - String get history => 'History'; - String get sprint => '40 Lines'; - String get blitz => 'Blitz'; - String get recent => 'Recent'; - String get recentRuns => 'Recent runs'; - String blitzScore({required Object p}) => '${p} points'; - String get openSPreplay => 'Open replay in TETR.IO'; - String get downloadSPreplay => 'Download replay'; - String get other => 'Other'; - String get distinguishment => 'Distinguishment'; - String get zen => 'Zen'; - String get bio => 'Bio'; - String get news => 'News'; - late final _StringsNewsPartsEn newsParts = _StringsNewsPartsEn._(_root); - String get openSearch => 'Search player'; - String get closeSearch => 'Close search'; - String get searchHint => 'Nickname, ID or Discord userID (with "ds:" prefix)'; - String get refresh => 'Refresh'; - String get fetchAndsaveTLHistory => 'Get player history'; - String get fetchAndSaveOldTLmatches => 'Get Tetra League matches history'; - String fetchAndsaveTLHistoryResult({required Object number}) => '${number} states was found'; - String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} matches was found'; - String get showStoredData => 'Show stored data'; - String get statsCalc => 'Stats Calculator'; - String get settings => 'Settings'; - String get track => 'Track'; - String get stopTracking => 'Stop\ntracking'; - String get becameTracked => 'Added to tracking list!'; - String get compare => 'Compare'; - String get stoppedBeingTracked => 'Removed from tracking list!'; - String get tlLeaderboard => 'Tetra League leaderboard'; - String get noRecords => 'No records'; - String noOldRecords({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: 'No records', - one: 'Only ${n} record', - two: 'Only ${n} records', - few: 'Only ${n} records', - many: 'Only ${n} records', - other: 'Only ${n} records', - ); - String get noRecord => 'No record'; - String get botRecord => 'Bots are not allowed to set records'; - String get anonRecord => 'Guests are not allowed to set records'; - String get notEnoughData => 'Not enough data'; - String get noHistorySaved => 'No history saved'; - String get pseudoTooltipHeaderInit => 'Hover over point'; - String get pseudoTooltipFooterInit => 'to see detailed data'; - String obtainDate({required Object date}) => 'Obtained ${date}'; - String fetchDate({required Object date}) => 'Fetched ${date}'; - String get exactGametime => 'Exact gametime'; - String get bigRedBanned => 'BANNED'; - String get normalBanned => 'Banned'; - String get bigRedBadStanding => 'BAD STANDING'; - String get copiedToClipboard => 'Copied to clipboard!'; - String get playerRoleAccount => ' account '; - String get wasFromBeginning => 'that was from very beginning'; - String get created => 'created'; - String get botCreatedBy => 'by'; - String get notSupporter => 'Not a supporter'; - String get assignedManualy => 'That badge was assigned manualy by TETR.IO admins'; - String supporter({required Object tier}) => 'Supporter tier ${tier}'; - String comparingWith({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; - String get top => 'Top'; - String get topRank => 'Top rank'; - String verdictGeneral({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; - String get verdictBetter => 'better'; - String get verdictWorse => 'worse'; - String get smooth => 'Smooth'; - String get postSeason => 'Off-season'; - String get seasonStarts => 'Season starts in:'; - String get nanow => 'Not avaliable for now...'; - String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}'; - String get seasonEnded => 'Season has ended'; - String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; - String numOfVictories({required Object wins}) => '~${wins} victories'; - String get promotionOnNextWin => 'Promotion on next win'; - String numOfdefeats({required Object losses}) => '~${losses} defeats'; - String get demotionOnNextLoss => 'Demotion on next loss'; - String get nerdStats => 'Nerd Stats'; - String get playersYouTrack => 'Players you track'; - String get formula => 'Formula'; - String get exactValue => 'Exact value'; - String get neverPlayedTL => 'That user never played Tetra League'; - String get botTL => 'Bots are not allowed to play Tetra League'; - String get anonTL => 'Guests are not allowed to play Tetra League'; - String get quickPlay => 'Quick Play'; - String get expert => 'Expert'; - String get withMods => 'With mods'; - String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: 'with ${n} mods', - one: 'with ${n} mod', - two: 'with ${n} mods', - few: 'with ${n} mods', - many: 'with ${n} mods', - other: 'with ${n} mods', - ); - String get exportDB => 'Export local database'; - String get exportDBDescription => 'It contains states and Tetra League records of the tracked players and list of tracked players.'; - String get desktopExportAlertTitle => 'Desktop export'; - String get desktopExportText => 'It seems like you using this app on desktop. Check your documents folder, you should find "TetraStats.db". Copy it somewhere'; - String get androidExportAlertTitle => 'Android export'; - String androidExportText({required Object exportedDB}) => 'Exported.\n${exportedDB}'; - String get importDB => 'Import local database'; - String get importDBDescription => 'Restore your backup. Notice that already stored database will be overwritten.'; - String get importWrongFileType => 'Wrong file type'; - String get importCancelled => 'Operation was cancelled'; - String get importSuccess => 'Import successful'; - String get yourID => 'Your TETR.IO account'; - String get yourIDAlertTitle => 'Your nickname in TETR.IO'; - String get yourIDText => 'When app loads, it will retrieve data for this account'; - String get language => 'Language'; - String get updateInBackground => 'Update stats in the background'; - String get updateInBackgroundDescription => 'While Tetra Stats is running, it can update stats of the current player when cache expires'; - String get customization => 'Customization'; - String get customizationDescription => 'Change appearance of different things in Tetra Stats UI'; - String get oskKagari => 'Osk Kagari gimmick'; - String get oskKagariDescription => 'If on, osk\'s rank on main view will be rendered as :kagari:'; - String get AccentColor => 'Accent color'; - String get AccentColorDescription => 'Almost all interactive UI elements highlighted with this color'; - String get timestamps => 'Timestamps'; - String get timestampsDescription => 'You can choose, in which way timestamps shows time'; - String get timestampsAbsoluteGMT => 'Absolute (GMT)'; - String get timestampsAbsoluteLocalTime => 'Absolute (Your timezone)'; - String get timestampsRelative => 'Relative'; - String get rating => 'Main representation of rating'; - String get ratingDescription => 'TR is not linear, while Glicko does not have boundaries and percentile is volatile'; - String get ratingLBposition => 'LB position'; - String get sheetbotGraphs => 'Sheetbot-like behavior for radar graphs'; - String get sheetbotGraphsDescription => 'If on, points on the graphs can appear on the opposite half of the graph if value is negative'; - String get lbStats => 'Show leaderboard based stats'; - String get lbStatsDescription => 'That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values'; - String get aboutApp => 'About app'; - String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; - String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}'; - String statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; - String matchesViewTitle({required Object nickname}) => '${nickname} TL matches'; - String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; - String stateRemoved({required Object date}) => '${date} state was removed from database!'; - String matchRemoved({required Object date}) => '${date} match was removed from database!'; - String get viewAllMatches => 'View all matches'; - String get trackedPlayersViewTitle => 'Stored data'; - String get trackedPlayersZeroEntrys => 'Empty list. Press "Track" button in previous view to add current player here'; - String get trackedPlayersOneEntry => 'There is only one player'; - String trackedPlayersManyEntrys({required Object numberOfPlayers}) => 'There are ${numberOfPlayers} players'; - String trackedPlayersEntry({required Object nickname, required Object numberOfStates}) => '${nickname}: ${numberOfStates} states'; - String trackedPlayersDescription({required Object firstStateDate, required Object lastStateDate}) => 'From ${firstStateDate} until ${lastStateDate}'; - String trackedPlayersStatesDeleted({required Object nickname}) => '${nickname} states was removed from database!'; - String get duplicatedFix => 'Remove duplicated TL mathces'; - String get compressDB => 'Compress DB'; - String SpaceSaved({required Object size}) => 'Space saved: ${size}'; - String averageXrank({required Object rankLetter}) => 'Average ${rankLetter} rank'; - String get vs => 'vs'; - String get inTLmatch => 'in TL match'; - String get downloadReplay => 'Download .ttrm replay'; - String get openReplay => 'Open replay in TETR.IO'; - String replaySaved({required Object path}) => 'Replay saved to ${path}'; - String get match => 'Match'; - String get timeWeightedmatch => 'Match (time-weighted)'; - String roundNumber({required Object n}) => 'Round ${n}'; - String get statsFor => 'Stats for'; - String get numberOfRounds => 'Number of rounds'; - String get matchLength => 'Match Length'; - String get roundLength => 'Round Length'; - String get matchStats => 'Match stats'; - String get timeWeightedmatchStats => 'Time-weighted match stats'; - String get replayIssue => 'Can\'t process replay'; - String get matchIsTooOld => 'Replay is not available'; - String get winner => 'Winner'; - String get registred => 'Registred'; - String get playedTL => 'Played Tetra League'; - String get winChance => 'Win Chance'; - String get byGlicko => 'By Glicko'; - String get byEstTR => 'By Est. TR'; - String compareViewNoValues({required Object avgR}) => 'Please, enter username, user ID, APM-PPS-VS values (divider doesn\'t matter, only order matter) or ${avgR} (where R is rank) to both fields'; - String compareViewWrongValue({required Object value}) => 'Falied to assign ${value}'; - String get mostRecentOne => 'Most recent one'; - String get yes => 'Yes'; - String get no => 'No'; - String get daysLater => 'days later'; - String get dayseBefore => 'days before'; - String get fromBeginning => 'From beginning'; - String get calc => 'Calc'; - String get calcViewNoValues => 'Enter values to calculate the stats'; - String get rankAveragesViewTitle => 'Ranks cutoffs'; - String get sprintAndBlitsViewTitle => '40 lines and Blitz averages'; - String sprintAndBlitsRelevance({required Object date}) => 'Relevance: ${date}'; - String get rank => 'Rank'; - String get averages => 'Averages'; - String get lbViewZeroEntrys => 'Empty list'; - String get lbViewOneEntry => 'There is only one player'; - String lbViewManyEntrys({required Object numberOfPlayers}) => 'There are ${numberOfPlayers}'; - String get everyoneAverages => 'Values for leaderboard'; - String get sortBy => 'Sort by'; - String get reversed => 'Reversed'; - String get country => 'Country'; - String rankAverages({required Object rank}) => 'Values for ${rank} rank'; - String players({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} players', - one: '${n} player', - two: '${n} players', - few: '${n} players', - many: '${n} players', - other: '${n} players', - ); - String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} games', - one: '${n} game', - two: '${n} games', - few: '${n} games', - many: '${n} games', - other: '${n} games', - ); - String gamesPlayed({required Object games}) => '${games} played'; - String get chart => 'Chart'; - String get entries => 'Entries'; - String get minimums => 'Minimums'; - String get maximums => 'Maximums'; - String get lowestValues => 'Lowest Values'; - String get averageValues => 'Average Values'; - String get highestValues => 'Highest Values'; - String forPlayer({required Object username}) => 'for player ${username}'; - String currentAxis({required Object axis}) => '${axis} axis:'; - String get p1nkl0bst3rAlert => 'That data was retrived from third party API maintained by p1nkl0bst3r'; - String get notForWeb => 'Function is not available for web version'; - late final _StringsGraphsEn graphs = _StringsGraphsEn._(_root); - late final _StringsStatCellNumEn statCellNum = _StringsStatCellNumEn._(_root); + Map get gamemodes => { + 'league': 'Tetra League', + 'zenith': 'Quick Play', + 'zenithex': 'Quick Play Expert', + '40l': '40 Lines', + 'blitz': 'Blitz', + '5mblast': '5,000,000 Blast', + 'zen': 'Zen', + }; + late final _StringsDestinationsEn destinations = _StringsDestinationsEn._(_root); Map get playerRole => { 'user': 'User', 'banned': 'Banned', @@ -390,11 +174,92 @@ class Translations implements BaseTranslations { 'halfmod': 'Community moderator', 'anon': 'Anonymous', }; - late final _StringsNumOfGameActionsEn numOfGameActions = _StringsNumOfGameActionsEn._(_root); - late final _StringsPopupActionsEn popupActions = _StringsPopupActionsEn._(_root); + String get goBackButton => 'Go Back'; + String get nanow => 'Not avaliable for now...'; + String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}'; + String get seasonEnded => 'Season has ended'; + String overallPB({required Object pb}) => 'Overall PB: ${pb} m'; + String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; + String numOfVictories({required Object wins}) => '~${wins} victories'; + String get promotionOnNextWin => 'Promotion on next win'; + String numOfdefeats({required Object losses}) => '~${losses} defeats'; + String get demotionOnNextLoss => 'Demotion on next loss'; + String get records => 'Records'; + String get nerdStats => 'Nerd Stats'; + String get playstyles => 'Playstyles'; + String get horoscopes => 'Horoscopes'; + String get relatedAchievements => 'Related Achievements'; + String get season => 'Season'; + String get smooth => 'Smooth'; + String get dateAndTime => 'Date & Time'; + String get TLfullLBnote => 'Heavy, but allows you to sort players by their stats and filter them by ranks'; + String get rank => 'Rank'; + String verdictGeneral({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} of ${rank} rank avg'; + String get verdictBetter => 'ahead'; + String get verdictWorse => 'behind'; + String get localStanding => 'local'; + late final _StringsXpEn xp = _StringsXpEn._(_root); + late final _StringsGametimeEn gametime = _StringsGametimeEn._(_root); + String get track => 'Track'; + String get stopTracking => 'Stop tracking'; + String supporter({required Object tier}) => 'Supporter tier ${tier}'; + String comparingWith({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; + String get compare => 'Compare'; + String get comparison => 'Comparison'; + String get enterUsername => 'Enter username or \$avgX (where X is rank)'; + String get general => 'General'; + String get badges => 'Badges'; + String obtainDate({required Object date}) => 'Obtained ${date}'; + String get assignedManualy => 'That badge was assigned manualy by TETR.IO admins'; + String get distinguishment => 'Distinguishment'; + String get banned => 'Banned'; + String get bannedSubtext => 'Bans are placed when TETR.IO rules or terms of service are broken'; + String get badStanding => 'Bad standing'; + String get badStandingSubtext => 'One or more recent bans on record'; + String get botAccount => 'Bot account'; + String botAccountSubtext({required Object botMaintainers}) => 'Operated by ${botMaintainers}'; + String get copiedToClipboard => 'Copied to clipboard!'; + String get bio => 'Bio'; + String get news => 'News'; + late final _StringsMatchResultEn matchResult = _StringsMatchResultEn._(_root); + late final _StringsDistinguishmentsEn distinguishments = _StringsDistinguishmentsEn._(_root); + late final _StringsNewsEntriesEn newsEntries = _StringsNewsEntriesEn._(_root); + String rankupMiddle({required Object r}) => '${r} rank'; + String get copyUserID => 'Click to copy user ID'; + String get searchHint => 'Username or ID'; + String get navMenu => 'Navigation menu'; + String get navMenuTooltip => 'Open navigation menu'; + String get refresh => 'Refresh data'; + String get searchButton => 'Search'; + String get trackedPlayers => 'Tracked Players'; + String get standing => 'Standing'; + String get previousSeasons => 'Previous Seasons'; + String get recent => 'Recent'; + String get top => 'Top'; + String get noRecord => 'No record'; + String sprintAndBlitsRelevance({required Object date}) => 'Relevance: ${date}'; + late final _StringsSnackBarMessagesEn snackBarMessages = _StringsSnackBarMessagesEn._(_root); late final _StringsErrorsEn errors = _StringsErrorsEn._(_root); + late final _StringsActionsEn actions = _StringsActionsEn._(_root); + late final _StringsGraphsDestinationEn graphsDestination = _StringsGraphsDestinationEn._(_root); + late final _StringsFilterModaleEn filterModale = _StringsFilterModaleEn._(_root); + late final _StringsCutoffsDestinationEn cutoffsDestination = _StringsCutoffsDestinationEn._(_root); + late final _StringsRankViewEn rankView = _StringsRankViewEn._(_root); + late final _StringsStateViewEn stateView = _StringsStateViewEn._(_root); + late final _StringsTlMatchViewEn tlMatchView = _StringsTlMatchViewEn._(_root); + late final _StringsCalcDestinationEn calcDestination = _StringsCalcDestinationEn._(_root); + late final _StringsInfoDestinationEn infoDestination = _StringsInfoDestinationEn._(_root); + late final _StringsLeaderboardsDestinationEn leaderboardsDestination = _StringsLeaderboardsDestinationEn._(_root); + late final _StringsSavedDataDestinationEn savedDataDestination = _StringsSavedDataDestinationEn._(_root); + late final _StringsSettingsDestinationEn settingsDestination = _StringsSettingsDestinationEn._(_root); + late final _StringsHomeNavigationEn homeNavigation = _StringsHomeNavigationEn._(_root); + late final _StringsGraphsNavigationEn graphsNavigation = _StringsGraphsNavigationEn._(_root); + late final _StringsCalcNavigationEn calcNavigation = _StringsCalcNavigationEn._(_root); + late final _StringsFirstTimeViewEn firstTimeView = _StringsFirstTimeViewEn._(_root); + late final _StringsAboutViewEn aboutView = _StringsAboutViewEn._(_root); + late final _StringsStatsEn stats = _StringsStatsEn._(_root); Map get countries => { - '': 'Not selected', + '': 'Worldwide', 'AF': 'Afghanistan', 'AX': 'Åland Islands', 'AL': 'Albania', @@ -656,148 +521,131 @@ class Translations implements BaseTranslations { }; } -// Path: newsParts -class _StringsNewsPartsEn { - _StringsNewsPartsEn._(this._root); +// Path: destinations +class _StringsDestinationsEn { + _StringsDestinationsEn._(this._root); final Translations _root; // ignore: unused_field // Translations - String get leaderboardStart => 'Got '; - String get leaderboardMiddle => 'on '; - String get personalbest => 'Got a new PB in '; - String get personalbestMiddle => 'of '; - String get badgeStart => 'Obtained a '; - String get badgeEnd => 'badge'; - String get rankupStart => 'Obtained '; - String rankupMiddle({required Object r}) => '${r} rank '; - String get rankupEnd => 'in Tetra League'; - String get tetoSupporter => 'TETR.IO supporter'; - String get supporterStart => 'Become a '; - String get supporterGiftStart => 'Received the gift of '; - String unknownNews({required Object type}) => 'Unknown news of type ${type}'; + String get home => 'Home'; + String get graphs => 'Graphs'; + String get leaderboards => 'Leaderboards'; + String get cutoffs => 'Cutoffs'; + String get calc => 'Calculator'; + String get info => 'Info Center'; + String get data => 'Saved Data'; + String get settings => 'Settings'; } -// Path: graphs -class _StringsGraphsEn { - _StringsGraphsEn._(this._root); +// Path: xp +class _StringsXpEn { + _StringsXpEn._(this._root); final Translations _root; // ignore: unused_field // Translations - String get attack => 'Attack'; - String get speed => 'Speed'; - String get defense => 'Defense'; - String get cheese => 'Cheese'; + String get title => 'XP Level'; + String progressToNextLevel({required Object percentage}) => 'Progress to next level: ${percentage}'; + String progressTowardsGoal({required Object goal, required Object percentage, required Object left}) => 'Progress from 0 XP to level ${goal}: ${percentage} (${left} XP left)'; } -// Path: statCellNum -class _StringsStatCellNumEn { - _StringsStatCellNumEn._(this._root); +// Path: gametime +class _StringsGametimeEn { + _StringsGametimeEn._(this._root); final Translations _root; // ignore: unused_field // Translations - String get xpLevel => 'XP Level'; - String get xpProgress => 'Progress to next level'; - String xpFrom0ToLevel({required Object n}) => 'Progress from 0 XP to level ${n}'; - String get xpLeft => 'XP left'; - String get hoursPlayed => 'Hours\nPlayed'; - String get onlineGames => 'Online\nGames'; - String get gamesWon => 'Games\nWon'; - String get totalGames => 'Total Games Played'; - String get totalWon => 'Total Games Won'; - String get friends => 'Friends'; - String get apm => 'Attack\nPer Minute'; - String get vs => 'Versus\nScore'; - String get recordLB => 'Leaderboard placement'; - String get lbp => 'Leaderboard\nplacement'; - String get lbpShort => '№ in LB'; - String get lbpc => 'Country LB\nplacement'; - String get lbpcShort => '№ in local LB'; - String get gamesPlayed => 'Games\nplayed'; - String get gamesWonTL => 'Games\nWon'; - String get winrate => 'Winrate'; - String get level => 'Level'; - String get score => 'Score'; - String get spp => 'Score\nPer Piece'; - String get pieces => 'Pieces\nPlaced'; - String get pps => 'Pieces\nPer Second'; - String get finesseFaults => 'Finesse\nFaults'; - String get finessePercentage => 'Finesse\nPercentage'; - String get keys => 'Key\nPresses'; - String get kpp => 'KP Per\nPiece'; - String get kps => 'KP Per\nSecond'; - String get tr => 'Tetra Rating'; - String get rd => 'Rating Deviation'; - String get app => 'Attack Per Piece'; - String get appDescription => '(Abbreviated as APP) Main efficiency metric. Tells how many attack you producing per piece'; - String get vsapmDescription => 'Basically, tells how much and how efficient you using garbage in your attacks'; - String get dss => 'Downstack\nPer Second'; - String get dssDescription => '(Abbreviated as DS/S) Downstack per Second measures how many garbage lines you clear in a second.'; - String get dsp => 'Downstack\nPer Piece'; - String get dspDescription => '(Abbreviated as DS/P) Downstack per Piece measures how many garbage lines you clear per piece.'; - String get appdsp => 'APP + DS/P'; - String get appdspDescription => 'Just a sum of Attack per Piece and Downstack per Piece.'; - String get cheese => 'Cheese\nIndex'; - String get cheeseDescription => '(Abbreviated as Cheese) Cheese Index is an approximation how much clean / cheese garbage player sends. Lower = more clean. Higher = more cheese.\nInvented by kerrmunism'; - String get gbe => 'Garbage\nEfficiency'; - String get gbeDescription => '(Abbreviated as Gb Eff.) Garbage Efficiency measures how well player uses their garbage. Higher = better or they use their garbage more. Lower = they mostly send their garbage back at cheese or rarely clear garbage.\nInvented by Zepheniah and Dragonboy.'; - String get nyaapp => 'Weighted\nAPP'; - String get nyaappDescription => '(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.'; - String get area => 'Area'; - String get areaDescription => 'How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections'; - String get estOfTR => 'Estimated TR'; - String get estOfTRShort => 'Est. TR'; - String get accOfEst => 'Accuracy'; - String get accOfEstShort => 'Acc.'; + String get title => 'Exact gametime'; + String gametimeAday({required Object gametime}) => '${gametime} a day in average'; + String breakdown({required Object years, required Object months, required Object days, required Object minutes, required Object seconds}) => 'It\'s ${years} years,\nor ${months} months,\nor ${days} days,\nor ${minutes} minutes\nor ${seconds} seconds'; } -// Path: numOfGameActions -class _StringsNumOfGameActionsEn { - _StringsNumOfGameActionsEn._(this._root); +// Path: matchResult +class _StringsMatchResultEn { + _StringsMatchResultEn._(this._root); final Translations _root; // ignore: unused_field // Translations - String get pc => 'All Clears'; - String get hold => 'Holds'; - String inputs({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} key presses', - one: '${n} key press', - two: '${n} key presses', - few: '${n} key presses', - many: '${n} key presses', - other: '${n} key presses', - ); - String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} T-spins total', - one: '${n} T-spin total', - two: '${n} T-spins total', - few: '${n} T-spins total', - many: '${n} T-spins total', - other: '${n} T-spins total', - ); - String lineClears({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} lines cleared', - one: '${n} line cleared', - two: '${n} lines cleared', - few: '${n} lines cleared', - many: '${n} lines cleared', - other: '${n} lines cleared', - ); + String get victory => 'Victory'; + String get defeat => 'Defeat'; + String get tie => 'Tie'; + String get dqvictory => 'Opponent was DQ\'ed'; + String get dqdefeat => 'Disqualified'; + String get nocontest => 'No Contest'; + String get nullified => 'Nullified'; } -// Path: popupActions -class _StringsPopupActionsEn { - _StringsPopupActionsEn._(this._root); +// Path: distinguishments +class _StringsDistinguishmentsEn { + _StringsDistinguishmentsEn._(this._root); final Translations _root; // ignore: unused_field // Translations - String get cancel => 'Cancel'; - String get submit => 'Submit'; - String get ok => 'OK'; + String get noHeader => 'Header is missing'; + String get noFooter => 'Footer is missing'; + String get twc => 'TETR.IO World Champion'; + String twcYear({required Object year}) => '${year} TETR.IO World Championship'; +} + +// Path: newsEntries +class _StringsNewsEntriesEn { + _StringsNewsEntriesEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + TextSpan leaderboard({required InlineSpan rank, required InlineSpan gametype}) => TextSpan(children: [ + const TextSpan(text: 'Got № '), + rank, + const TextSpan(text: ' in '), + gametype, + ]); + TextSpan personalbest({required InlineSpan gametype, required InlineSpan pb}) => TextSpan(children: [ + const TextSpan(text: 'Got a new PB in '), + gametype, + const TextSpan(text: ' of '), + pb, + ]); + TextSpan badge({required InlineSpan badge}) => TextSpan(children: [ + const TextSpan(text: 'Obtained a '), + badge, + const TextSpan(text: ' badge'), + ]); + TextSpan rankup({required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: 'Obtained '), + rank, + const TextSpan(text: ' in Tetra League'), + ]); + TextSpan supporter({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Became a '), + s('TETR.IO supporter'), + ]); + TextSpan supporter_gift({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Received the gift of '), + s('TETR.IO supporter'), + ]); + TextSpan unknown({required InlineSpan type}) => TextSpan(children: [ + const TextSpan(text: 'Unknown news of type '), + type, + ]); +} + +// Path: snackBarMessages +class _StringsSnackBarMessagesEn { + _StringsSnackBarMessagesEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String stateRemoved({required Object date}) => '${date} state was removed from database!'; + String matchRemoved({required Object date}) => '${date} match was removed from database!'; + String get notForWeb => 'Function is not available for web version'; + String get importSuccess => 'Import successful'; + String get importCancelled => 'Import was cancelled'; } // Path: errors @@ -807,6 +655,9 @@ class _StringsErrorsEn { final Translations _root; // ignore: unused_field // Translations + String get noRecords => 'No records'; + String get notEnoughData => 'Not enough data'; + String get noHistorySaved => 'No history saved'; String connection({required Object code, required Object message}) => 'Some issue with connection: ${code} ${message}'; String get noSuchUser => 'No such user'; String get noSuchUserSub => 'Either you mistyped something, or the account no longer exists'; @@ -835,14 +686,993 @@ class _StringsErrorsEn { String get replayRejected => 'Third party API blocked your IP address'; } +// Path: actions +class _StringsActionsEn { + _StringsActionsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get cancel => 'Cancel'; + String get submit => 'Submit'; + String get ok => 'OK'; + String get apply => 'Apply'; + String get refresh => 'Refresh'; +} + +// Path: graphsDestination +class _StringsGraphsDestinationEn { + _StringsGraphsDestinationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get fetchAndsaveTLHistory => 'Get player history'; + String get fetchAndSaveOldTLmatches => 'Get Tetra League matches history'; + String fetchAndsaveTLHistoryResult({required Object number}) => '${number} states was found'; + String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} matches was found'; + String gamesPlayed({required Object games}) => '${games} played'; + String get dateAndTime => 'Date & Time'; + String get filterModaleTitle => 'Filter ranks on graph'; +} + +// Path: filterModale +class _StringsFilterModaleEn { + _StringsFilterModaleEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get all => 'All'; +} + +// Path: cutoffsDestination +class _StringsCutoffsDestinationEn { + _StringsCutoffsDestinationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Tetra League State'; + String relevance({required Object timestamp}) => 'as of ${timestamp}'; + String get actual => 'Actual'; + String get target => 'Target'; + String get cutoffTR => 'Cutoff TR'; + String get targetTR => 'Target TR'; + String get state => 'State'; + String get advanced => 'Advanced'; + String players({required Object n}) => 'Players (${n})'; + String get moreInfo => 'More Info'; + String NumberOne({required Object tr}) => '№ 1 is ${tr} TR'; + String inflated({required Object tr}) => 'Inflated on ${tr} TR'; + String get notInflated => 'Not inflated'; + String deflated({required Object tr}) => 'Deflated on ${tr} TR'; + String get notDeflated => 'Not deflated'; + String get wellDotDotDot => 'Well...'; + String fromPlace({required Object n}) => 'from № ${n}'; + String get viewButton => 'View'; +} + +// Path: rankView +class _StringsRankViewEn { + _StringsRankViewEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String rankTitle({required Object rank}) => '${rank} rank data'; + String get everyoneTitle => 'Entire leaderboard'; + String get trRange => 'TR Range'; + String get supposedToBe => 'Supposed to be'; + String gap({required Object value}) => '${value} gap'; + String trGap({required Object value}) => '${value} TR gap'; + String get deflationGap => 'Deflation gap'; + String get inflationGap => 'Inflation gap'; + String get LBposRange => 'LB pos range'; + String overpopulated({required Object players}) => 'Overpopulated by a ${players}'; + String underpopulated({required Object players}) => 'Underpopulated by a ${players}'; + String get PlayersEqualSupposedToBe => 'cute'; + String get avgStats => 'Average Stats'; + String avgForRank({required Object rank}) => 'Average for ${rank} rank'; + String get avgNerdStats => 'Average Nerd Stats'; + String get minimums => 'Minimums'; + String get maximums => 'Maximums'; +} + +// Path: stateView +class _StringsStateViewEn { + _StringsStateViewEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String title({required Object date}) => 'State from ${date}'; +} + +// Path: tlMatchView +class _StringsTlMatchViewEn { + _StringsTlMatchViewEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get match => 'Match'; + String get vs => 'vs'; + String get winner => 'Winner'; + String roundNumber({required Object n}) => 'Round ${n}'; + String get statsFor => 'Stats for'; + String get numberOfRounds => 'Number of rounds'; + String get matchLength => 'Match Length'; + String get roundLength => 'Round Length'; + String get matchStats => 'Match stats'; + String get downloadReplay => 'Download .ttrm replay'; + String get openReplay => 'Open replay in TETR.IO'; +} + +// Path: calcDestination +class _StringsCalcDestinationEn { + _StringsCalcDestinationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String placeholders({required Object stat}) => 'Enter your ${stat}'; + String get tip => 'Enter values and press "Calc" to see Nerd Stats for them'; + String get statsCalcButton => 'Calc'; + String get damageCalcTip => 'Click on the actions on the left to add them here'; + String get actions => 'Actions'; + String get results => 'Results'; + String get rules => 'Rules'; + String get noSpinClears => 'No Spin Clears'; + String get spins => 'Spins'; + String get miniSpins => 'Mini spins'; + String get noLineclear => 'No lineclear (Break Combo)'; + String get custom => 'Custom'; + String get multiplier => 'Multiplier'; + String get pcDamage => 'Perfect Clear Damage'; + String get comboTable => 'Combo Table'; + String get b2bChaining => 'Back-To-Back Chaining'; + String get surgeStartAtB2B => 'Starts at B2B'; + String get surgeStartAmount => 'Start amount'; + String get totalDamage => 'Total damage'; + String get lineclears => 'Lineclears'; + String get combo => 'Combo'; + String get surge => 'Surge'; + String get pcs => 'PCs'; +} + +// Path: infoDestination +class _StringsInfoDestinationEn { + _StringsInfoDestinationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Information Center'; + String get sprintAndBlitzAverages => '40 Lines & Blitz Averages'; + String get sprintAndBlitzAveragesDescription => 'Since calculating 40 Lines & Blitz averages is tedious process, it gets updated only once in a while. Click on the title of this card to see the full 40 Lines & Blitz averages table'; + String get tetraStatsWiki => 'Tetra Stats Wiki'; + String get tetraStatsWikiDescription => 'Find more information about Tetra Stats functions and statictic, that it provides'; + String get about => 'About Tetra Stats'; + String get aboutDescription => 'Developed by dan63\n'; +} + +// Path: leaderboardsDestination +class _StringsLeaderboardsDestinationEn { + _StringsLeaderboardsDestinationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Leaderboards'; + String get tl => 'Tetra League (Current Season)'; + String get fullTL => 'Tetra League (Current Season, full one)'; + String get ar => 'Acievement Points'; +} + +// Path: savedDataDestination +class _StringsSavedDataDestinationEn { + _StringsSavedDataDestinationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Saved Data'; + String get tip => 'Select nickname on the left to see data assosiated with it'; + String seasonTLstates({required Object s}) => 'S${s} TL States'; + String get TLrecords => 'TL Records'; +} + +// Path: settingsDestination +class _StringsSettingsDestinationEn { + _StringsSettingsDestinationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Settings'; + String get general => 'General'; + String get customization => 'Customization'; + String get database => 'Local database'; + String get checking => 'Checking...'; + String get enterToSubmit => 'Press Enter to submit'; + String get account => 'Your account in TETR.IO'; + String get accountDescription => 'Stats of that player will be loaded initially right after launching this app. By default it loads my (dan63) stats. To change that, enter your nickname here.'; + String get done => 'Done!'; + String get noSuchAccount => 'No such account'; + String get language => 'Language'; + String languageDescription({required Object languages}) => 'Tetra Stats was translated on ${languages}. By default, app will pick your system one or English, if locale of your system isn\'t avaliable.'; + String languages({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'zero languages', + one: '${n} language', + two: '${n} languages', + few: '${n} languages', + many: '${n} languages', + other: '${n} languages', + ); + String get updateInTheBackground => 'Update data in the background'; + String get updateInTheBackgroundDescription => 'If on, Tetra Stats will attempt to retrieve new info once cache expires. Usually that happen every 5 minutes'; + String get compareStats => 'Compare TL stats with rank averages'; + String get compareStatsDescription => 'If on, Tetra Stats will provide additional metrics, which allow you to compare yourself with average player on your rank. The way you\'ll see it — stats will be highlited with corresponding color, hover over them with cursor for more info.'; + String get showPosition => 'Show position on leaderboard by stats'; + String get showPositionDescription => 'This can take some time (and traffic) to load, but will allow you to see your position on the leaderboard, sorted by a stat'; + String get accentColor => 'Accent color'; + String get accentColorDescription => 'That color is seen across this app and usually highlites interactive UI elements.'; + String get accentColorModale => 'Pick an accent color'; + String get timestamps => 'Timestamps format'; + String timestampsDescriptionPart1({required Object d}) => 'You can choose, in which way timestamps shows time. By default, they show time in GMT timezone, formatted according to chosen locale, example: ${d}.'; + String timestampsDescriptionPart2({required Object y, required Object r}) => 'There is also:\n• Locale formatted in your timezone: ${y}\n• Relative timestamp: ${r}'; + String get timestampsAbsoluteGMT => 'Absolute (GMT)'; + String get timestampsAbsoluteLocalTime => 'Absolute (Your timezone)'; + String get timestampsRelative => 'Relative'; + String get sheetbotLikeGraphs => 'Sheetbot-like behavior for radar graphs'; + String get sheetbotLikeGraphsDescription => 'Altough it was considered by me, that the way graphs work in SheetBot is not very correct, some people were confused to see, that -0.5 stride dosen\'t look the way it looks on SheetBot graph. Hence, he we are: if this toggle is on, points on the graphs can appear on the opposite half of the graph if value is negative.'; + String get oskKagariGimmick => 'Osk-Kagari gimmick'; + String get oskKagariGimmickDescription => 'If on, instead of osk\'s rank, :kagari: will be rendered.'; + String get bytesOfDataStored => 'of data stored'; + String get TLrecordsSaved => 'Tetra League records saved'; + String get TLplayerstatesSaved => 'Tetra League playerstates saved'; + String get fixButton => 'Fix'; + String get compressButton => 'Compress'; + String get exportDB => 'Export local database'; + String get desktopExportAlertTitle => 'Desktop export'; + String get desktopExportText => 'It seems like you using this app on desktop. Check your documents folder, you should find "TetraStats.db". Copy it somewhere'; + String get androidExportAlertTitle => 'Android export'; + String androidExportText({required Object exportedDB}) => 'Exported.\n${exportedDB}'; + String get importDB => 'Import local database'; + String get importDBDescription => 'Restore your backup. Notice that already stored database will be overwritten.'; + String get importWrongFileType => 'Wrong file type'; +} + +// Path: homeNavigation +class _StringsHomeNavigationEn { + _StringsHomeNavigationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get overview => 'Overview'; + String get standing => 'Standing'; + String get seasons => 'Seasons'; + String get mathces => 'Matches'; + String get pb => 'PB'; + String get normal => 'Normal'; + String get expert => 'Expert'; + String get expertRecords => 'Ex Records'; +} + +// Path: graphsNavigation +class _StringsGraphsNavigationEn { + _StringsGraphsNavigationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get history => 'Player History'; + String get league => 'League State'; + String get cutoffs => 'Cutoffs History'; +} + +// Path: calcNavigation +class _StringsCalcNavigationEn { + _StringsCalcNavigationEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get stats => 'Stats Calculator'; + String get damage => 'Damage Calculator'; +} + +// Path: firstTimeView +class _StringsFirstTimeViewEn { + _StringsFirstTimeViewEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get welcome => 'Welcome to Tetra Stats'; + String get description => 'Service, that allows you to keep track of various statistics for TETR.IO'; + String get nicknameQuestion => 'What\'s your nickname?'; + String get inpuntHint => 'Type it here... (3-16 symbols)'; + String get emptyInputError => 'Can\'t submit an empty string'; + String niceToSeeYou({required Object n}) => 'Nice to see you, ${n}'; + String get letsTakeALook => 'Let\'s take a look at your stats...'; + String get skip => 'Skip'; +} + +// Path: aboutView +class _StringsAboutViewEn { + _StringsAboutViewEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'About Tetra Stats'; + String get about => 'Tetra Stats is a service, that works with TETR.IO Tetra Channel API, providing data from it and calculating some addtitional metrics, based on this data. Service allows user to track their progress in Tetra League with "Track" function, which records every Tetra League change into local database (not automatically, you have to visit service from time to time), so these changes could be looked through graphs.\n\nBeanserver blaster is a part of a Tetra Stats, that decoupled into a serverside script. It provides full Tetra League leaderboard, allowing Tetra Stats to sort leaderboard by any metric and build scatter chart, that allows user to analyse Tetra League trends. It also provides history of Tetra League ranks cutoffs, which can be viewed by user via graph as well.\n\nThere is a plans to add replay analysis and tournaments history, so stay tuned!\n\nService is not associated with TETR.IO or osk in any capacity.'; + String get appVersion => 'App Version'; + String build({required Object build}) => 'Build ${build}'; + String get GHrepo => 'GitHub Repository'; + String get submitAnIssue => 'Submit an issue'; + String get credits => 'Credits'; + String get authorAndDeveloper => 'Autor & developer'; + String get providedFormulas => 'Provided formulas'; + String get providedS1history => 'Provided S1 history'; + String get inoue => 'Inoue (replay grabber)'; + String get zhCNlocale => 'Simplfied Chinese locale'; + String get supportHim => 'Support him!'; +} + +// Path: stats +class _StringsStatsEn { + _StringsStatsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get registrationDate => 'Registration Date'; + String get gametime => 'Time Played'; + String get ogp => 'Online Games Played'; + String get ogw => 'Online Games Won'; + String get followers => 'Followers'; + late final _StringsStatsXpEn xp = _StringsStatsXpEn._(_root); + late final _StringsStatsTrEn tr = _StringsStatsTrEn._(_root); + late final _StringsStatsGlickoEn glicko = _StringsStatsGlickoEn._(_root); + late final _StringsStatsRdEn rd = _StringsStatsRdEn._(_root); + late final _StringsStatsGlixareEn glixare = _StringsStatsGlixareEn._(_root); + late final _StringsStatsS1trEn s1tr = _StringsStatsS1trEn._(_root); + late final _StringsStatsGpEn gp = _StringsStatsGpEn._(_root); + late final _StringsStatsGwEn gw = _StringsStatsGwEn._(_root); + late final _StringsStatsWinrateEn winrate = _StringsStatsWinrateEn._(_root); + late final _StringsStatsApmEn apm = _StringsStatsApmEn._(_root); + late final _StringsStatsPpsEn pps = _StringsStatsPpsEn._(_root); + late final _StringsStatsVsEn vs = _StringsStatsVsEn._(_root); + late final _StringsStatsAppEn app = _StringsStatsAppEn._(_root); + late final _StringsStatsVsapmEn vsapm = _StringsStatsVsapmEn._(_root); + late final _StringsStatsDssEn dss = _StringsStatsDssEn._(_root); + late final _StringsStatsDspEn dsp = _StringsStatsDspEn._(_root); + late final _StringsStatsAppdspEn appdsp = _StringsStatsAppdspEn._(_root); + late final _StringsStatsCheeseEn cheese = _StringsStatsCheeseEn._(_root); + late final _StringsStatsGbeEn gbe = _StringsStatsGbeEn._(_root); + late final _StringsStatsNyaappEn nyaapp = _StringsStatsNyaappEn._(_root); + late final _StringsStatsAreaEn area = _StringsStatsAreaEn._(_root); + late final _StringsStatsEtrEn etr = _StringsStatsEtrEn._(_root); + late final _StringsStatsEtraccEn etracc = _StringsStatsEtraccEn._(_root); + late final _StringsStatsOpenerEn opener = _StringsStatsOpenerEn._(_root); + late final _StringsStatsPlonkEn plonk = _StringsStatsPlonkEn._(_root); + late final _StringsStatsStrideEn stride = _StringsStatsStrideEn._(_root); + late final _StringsStatsInfdsEn infds = _StringsStatsInfdsEn._(_root); + late final _StringsStatsAltitudeEn altitude = _StringsStatsAltitudeEn._(_root); + late final _StringsStatsClimbSpeedEn climbSpeed = _StringsStatsClimbSpeedEn._(_root); + late final _StringsStatsPeakClimbSpeedEn peakClimbSpeed = _StringsStatsPeakClimbSpeedEn._(_root); + late final _StringsStatsKosEn kos = _StringsStatsKosEn._(_root); + late final _StringsStatsB2bEn b2b = _StringsStatsB2bEn._(_root); + late final _StringsStatsFinesseEn finesse = _StringsStatsFinesseEn._(_root); + late final _StringsStatsFinesseFaultsEn finesseFaults = _StringsStatsFinesseFaultsEn._(_root); + late final _StringsStatsTotalTimeEn totalTime = _StringsStatsTotalTimeEn._(_root); + late final _StringsStatsLevelEn level = _StringsStatsLevelEn._(_root); + late final _StringsStatsPiecesEn pieces = _StringsStatsPiecesEn._(_root); + late final _StringsStatsSppEn spp = _StringsStatsSppEn._(_root); + late final _StringsStatsKpEn kp = _StringsStatsKpEn._(_root); + late final _StringsStatsKppEn kpp = _StringsStatsKppEn._(_root); + late final _StringsStatsKpsEn kps = _StringsStatsKpsEn._(_root); + String blitzScore({required Object p}) => '${p} points'; + String levelUpRequirement({required Object p}) => 'Level up requirement: ${p}'; + String get piecesTotal => 'Total pieces placed'; + String get piecesWithPerfectFinesse => 'Placed with perfect finesse'; + String get score => 'Score'; + String get lines => 'Lines'; + String get linesShort => 'L'; + String get pcs => 'Perfect Clears'; + String get holds => 'Holds'; + String get spike => 'Top Spike'; + String top({required Object percentage}) => 'Top ${percentage}'; + String topRank({required Object rank}) => 'Top rank: ${rank}'; + String get floor => 'Floor'; + String get split => 'Split'; + String get total => 'Total'; + String get sent => 'Sent'; + String get received => 'Received'; + String get placement => 'Placement'; + String get peak => 'Peak'; + String qpWithMods({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: 'With 1 mod', + two: 'With ${n} mods', + few: 'With ${n} mods', + many: 'With ${n} mods', + other: 'With ${n} mods', + ); + String inputs({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} key presses', + one: '${n} key press', + two: '${n} key presses', + few: '${n} key presses', + many: '${n} key presses', + other: '${n} key presses', + ); + String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} T-spins total', + one: '${n} T-spin total', + two: '${n} T-spins total', + few: '${n} T-spins total', + many: '${n} T-spins total', + other: '${n} T-spins total', + ); + String linesCleared({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} lines cleared', + one: '${n} line cleared', + two: '${n} lines cleared', + few: '${n} lines cleared', + many: '${n} lines cleared', + other: '${n} lines cleared', + ); + late final _StringsStatsGraphsEn graphs = _StringsStatsGraphsEn._(_root); + String players({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} players', + one: '${n} player', + two: '${n} players', + few: '${n} players', + many: '${n} players', + other: '${n} players', + ); + String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} games', + one: '${n} game', + two: '${n} games', + few: '${n} games', + many: '${n} games', + other: '${n} games', + ); + late final _StringsStatsLineClearEn lineClear = _StringsStatsLineClearEn._(_root); + late final _StringsStatsLineClearsEn lineClears = _StringsStatsLineClearsEn._(_root); + String get mini => 'Mini'; + String get tSpin => 'T-spin'; + String get tSpins => 'T-spins'; + String get spin => 'Spin'; + String get spins => 'Spins'; +} + +// Path: stats.xp +class _StringsStatsXpEn { + _StringsStatsXpEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'XP'; + String get full => 'Experience Points'; +} + +// Path: stats.tr +class _StringsStatsTrEn { + _StringsStatsTrEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'TR'; + String get full => 'Tetra Rating'; +} + +// Path: stats.glicko +class _StringsStatsGlickoEn { + _StringsStatsGlickoEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Glicko'; + String get full => 'Glicko'; +} + +// Path: stats.rd +class _StringsStatsRdEn { + _StringsStatsRdEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'RD'; + String get full => 'Rating Deviation'; +} + +// Path: stats.glixare +class _StringsStatsGlixareEn { + _StringsStatsGlixareEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'GXE'; + String get full => 'GLIXARE'; +} + +// Path: stats.s1tr +class _StringsStatsS1trEn { + _StringsStatsS1trEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'S1 TR'; + String get full => 'Season 1 like TR'; +} + +// Path: stats.gp +class _StringsStatsGpEn { + _StringsStatsGpEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'GP'; + String get full => 'Games Played'; +} + +// Path: stats.gw +class _StringsStatsGwEn { + _StringsStatsGwEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'GW'; + String get full => 'Games Won'; +} + +// Path: stats.winrate +class _StringsStatsWinrateEn { + _StringsStatsWinrateEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'WR%'; + String get full => 'Win Rate'; +} + +// Path: stats.apm +class _StringsStatsApmEn { + _StringsStatsApmEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'APM'; + String get full => 'Attack Per Minute'; +} + +// Path: stats.pps +class _StringsStatsPpsEn { + _StringsStatsPpsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'PPS'; + String get full => 'Pieces Per Second'; +} + +// Path: stats.vs +class _StringsStatsVsEn { + _StringsStatsVsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'VS'; + String get full => 'Versus Score'; +} + +// Path: stats.app +class _StringsStatsAppEn { + _StringsStatsAppEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'APP'; + String get full => 'Attack Per Piece'; +} + +// Path: stats.vsapm +class _StringsStatsVsapmEn { + _StringsStatsVsapmEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'VS/APM'; + String get full => 'VS / APM'; +} + +// Path: stats.dss +class _StringsStatsDssEn { + _StringsStatsDssEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'DS/S'; + String get full => 'Downstack Per Second'; +} + +// Path: stats.dsp +class _StringsStatsDspEn { + _StringsStatsDspEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'DS/P'; + String get full => 'Downstack Per Piece'; +} + +// Path: stats.appdsp +class _StringsStatsAppdspEn { + _StringsStatsAppdspEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'APP+DSP'; + String get full => 'APP + DSP'; +} + +// Path: stats.cheese +class _StringsStatsCheeseEn { + _StringsStatsCheeseEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Cheese'; + String get full => 'Cheese Index'; +} + +// Path: stats.gbe +class _StringsStatsGbeEn { + _StringsStatsGbeEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'GbE'; + String get full => 'Garbage Efficiency'; +} + +// Path: stats.nyaapp +class _StringsStatsNyaappEn { + _StringsStatsNyaappEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'wAPP'; + String get full => 'Weighted APP'; +} + +// Path: stats.area +class _StringsStatsAreaEn { + _StringsStatsAreaEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Area'; + String get full => 'Area'; +} + +// Path: stats.etr +class _StringsStatsEtrEn { + _StringsStatsEtrEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'eTR'; + String get full => 'Estimated TR'; +} + +// Path: stats.etracc +class _StringsStatsEtraccEn { + _StringsStatsEtraccEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => '±eTR'; + String get full => 'Accuracy of Estimated TR'; +} + +// Path: stats.opener +class _StringsStatsOpenerEn { + _StringsStatsOpenerEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Opener'; + String get full => 'Opener'; +} + +// Path: stats.plonk +class _StringsStatsPlonkEn { + _StringsStatsPlonkEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Plonk'; + String get full => 'Plonk'; +} + +// Path: stats.stride +class _StringsStatsStrideEn { + _StringsStatsStrideEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Stride'; + String get full => 'Stride'; +} + +// Path: stats.infds +class _StringsStatsInfdsEn { + _StringsStatsInfdsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Inf. DS'; + String get full => 'Infinite Downstack'; +} + +// Path: stats.altitude +class _StringsStatsAltitudeEn { + _StringsStatsAltitudeEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'm'; + String get full => 'Altitude'; +} + +// Path: stats.climbSpeed +class _StringsStatsClimbSpeedEn { + _StringsStatsClimbSpeedEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'CSP'; + String get full => 'Climb Speed'; + String get gaugetTitle => 'Climb\nSpeed'; +} + +// Path: stats.peakClimbSpeed +class _StringsStatsPeakClimbSpeedEn { + _StringsStatsPeakClimbSpeedEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Peak CSP'; + String get full => 'Peak Climb Speed'; + String get gaugetTitle => 'Peak'; +} + +// Path: stats.kos +class _StringsStatsKosEn { + _StringsStatsKosEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'KO\'s'; + String get full => 'Knockouts'; +} + +// Path: stats.b2b +class _StringsStatsB2bEn { + _StringsStatsB2bEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'B2B'; + String get full => 'Back-To-Back'; +} + +// Path: stats.finesse +class _StringsStatsFinesseEn { + _StringsStatsFinesseEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'F'; + String get full => 'Finesse'; + String get widgetTitle => 'inesse'; +} + +// Path: stats.finesseFaults +class _StringsStatsFinesseFaultsEn { + _StringsStatsFinesseFaultsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'FF'; + String get full => 'Finesse Faults'; +} + +// Path: stats.totalTime +class _StringsStatsTotalTimeEn { + _StringsStatsTotalTimeEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Time'; + String get full => 'Total Time'; + String get widgetTitle => 'otal Time'; +} + +// Path: stats.level +class _StringsStatsLevelEn { + _StringsStatsLevelEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'Lvl'; + String get full => 'Level'; +} + +// Path: stats.pieces +class _StringsStatsPiecesEn { + _StringsStatsPiecesEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'P'; + String get full => 'Pieces'; +} + +// Path: stats.spp +class _StringsStatsSppEn { + _StringsStatsSppEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'SPP'; + String get full => 'Score Per Piece'; +} + +// Path: stats.kp +class _StringsStatsKpEn { + _StringsStatsKpEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'KP'; + String get full => 'Key presses'; +} + +// Path: stats.kpp +class _StringsStatsKppEn { + _StringsStatsKppEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'KPP'; + String get full => 'Key presses Per Piece'; +} + +// Path: stats.kps +class _StringsStatsKpsEn { + _StringsStatsKpsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get short => 'KPS'; + String get full => 'Key presses Per Second'; +} + +// Path: stats.graphs +class _StringsStatsGraphsEn { + _StringsStatsGraphsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get attack => 'Attack'; + String get speed => 'Speed'; + String get defense => 'Defense'; + String get cheese => 'Cheese'; +} + +// Path: stats.lineClear +class _StringsStatsLineClearEn { + _StringsStatsLineClearEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get single => 'Single'; + String get double => 'Double'; + String get triple => 'Triple'; + String get quad => 'Quad'; + String get penta => 'Penta'; + String get hexa => 'Hexa'; + String get hepta => 'Hepta'; + String get octa => 'Octa'; + String get ennea => 'Ennea'; + String get deca => 'Deca'; + String get hendeca => 'Hendeca'; + String get dodeca => 'Dodeca'; + String get triadeca => 'Triadeca'; + String get tessaradeca => 'Tessaradeca'; + String get pentedeca => 'Pentedeca'; + String get hexadeca => 'Hexadeca'; + String get heptadeca => 'Heptadeca'; + String get octadeca => 'Octadeca'; + String get enneadeca => 'Enneadeca'; + String get eicosa => 'Eicosa'; + String get kagaris => 'Kagaris'; +} + +// Path: stats.lineClears +class _StringsStatsLineClearsEn { + _StringsStatsLineClearsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get zero => 'Zeros'; + String get single => 'Singles'; + String get double => 'Doubles'; + String get triple => 'Triples'; + String get quad => 'Quads'; + String get penta => 'Pentas'; +} + // Path: -class _StringsRu implements Translations { +class _StringsRuRu implements Translations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - _StringsRu.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + _StringsRuRu.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), $meta = TranslationMetadata( - locale: AppLocale.ru, + locale: AppLocale.ruRu, overrides: overrides ?? {}, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -850,247 +1680,30 @@ class _StringsRu implements Translations { $meta.setFlatMapFunction(_flatMapFunction); } - /// Metadata for the translations of . + /// Metadata for the translations of . @override final TranslationMetadata $meta; /// Access flat map @override dynamic operator[](String key) => $meta.getTranslation(key); - @override late final _StringsRu _root = this; // ignore: unused_field + @override late final _StringsRuRu _root = this; // ignore: unused_field // Translations @override Map get locales => { 'en': 'Английский (English)', - 'ru': 'Русский', + 'ru-RU': 'Русский', + 'zh-CN': 'Упрощенный Китайский (简体中文)', }; - @override String get tetraLeague => 'Тетра Лига'; - @override String get tlRecords => 'Матчи ТЛ'; - @override String get history => 'История'; - @override String get sprint => '40 линий'; - @override String get blitz => 'Блиц'; - @override String get recent => 'Недавно'; - @override String get recentRuns => 'Недавние'; - @override String blitzScore({required Object p}) => '${p} очков'; - @override String get openSPreplay => 'Открыть повтор в TETR.IO'; - @override String get downloadSPreplay => 'Скачать повтор'; - @override String get other => 'Другое'; - @override String get distinguishment => 'Заслуга'; - @override String get zen => 'Дзен'; - @override String get bio => 'Биография'; - @override String get news => 'Новости'; - @override late final _StringsNewsPartsRu newsParts = _StringsNewsPartsRu._(_root); - @override String get openSearch => 'Искать игрока'; - @override String get closeSearch => 'Закрыть поиск'; - @override String get searchHint => 'Ник, ID или ID в Discord (с префиксом "ds:")'; - @override String get refresh => 'Обновить'; - @override String get fetchAndsaveTLHistory => 'Получить историю игрока'; - @override String get fetchAndSaveOldTLmatches => 'Получить старые матчи Тетра Лиги'; - @override String fetchAndsaveTLHistoryResult({required Object number}) => '${number} состояний было найдено'; - @override String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} старых матчей было найдено'; - @override String get showStoredData => 'Показать сохранённые данные'; - @override String get statsCalc => 'Калькулятор статистики'; - @override String get settings => 'Настройки'; - @override String get track => 'Отслеживать'; - @override String get stopTracking => 'Перестать\nотслеживать'; - @override String get becameTracked => 'Добавлен в список отслеживания!'; - @override String get stoppedBeingTracked => 'Удалён из списка отслеживания!'; - @override String get compare => 'Сравнить'; - @override String get tlLeaderboard => 'Рейтинговая таблица'; - @override String get noRecords => 'Нет записей'; - @override String noOldRecords({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: 'Нет записей', - one: 'Всего один матч', - two: 'Всего ${n} матча', - few: 'Всего ${n} матча', - many: 'Всего ${n} матчей', - other: '${n} матчей', - ); - @override String get noRecord => 'Нет рекорда'; - @override String get botRecord => 'Ботам нельзя ставить рекорды'; - @override String get anonRecord => 'Гостям нельзя ставить рекорды'; - @override String get notEnoughData => 'Недостаточно данных'; - @override String get noHistorySaved => 'Нет сохранённой истории'; - @override String get pseudoTooltipHeaderInit => 'Наведите курсор на точку'; - @override String get pseudoTooltipFooterInit => 'чтобы узнать подробности'; - @override String obtainDate({required Object date}) => 'Получено ${date}'; - @override String fetchDate({required Object date}) => 'На момент ${date}'; - @override String get exactGametime => 'Время, проведённое в игре'; - @override String get bigRedBanned => 'ЗАБАНЕН'; - @override String get normalBanned => 'Забанен'; - @override String get bigRedBadStanding => 'ПЛОХАЯ РЕПУТАЦИЯ'; - @override String get copiedToClipboard => 'Скопировано в буфер обмена!'; - @override String get playerRoleAccount => ', аккаунт которого '; - @override String get wasFromBeginning => 'существовал с самого начала'; - @override String get created => 'создан'; - @override String get botCreatedBy => 'игроком'; - @override String get notSupporter => 'Нет саппортерки'; - @override String supporter({required Object tier}) => 'Саппортерка ${tier} уровня'; - @override String get assignedManualy => 'Этот значок был присвоен вручную администрацией TETR.IO'; - @override String comparingWith({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; - @override String get top => 'Топ'; - @override String get topRank => 'Топ ранг'; - @override String verdictGeneral({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; - @override String get verdictBetter => 'Лучше'; - @override String get verdictWorse => 'Хуже'; - @override String get smooth => 'Гладкий'; - @override String get postSeason => 'Внесезонье'; - @override String get seasonStarts => 'Сезон начнётся через:'; - @override String get nanow => 'Пока недоступно...'; - @override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}'; - @override String get seasonEnded => 'Сезон закончился'; - @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; - @override String numOfVictories({required Object wins}) => '~${wins} побед'; - @override String get promotionOnNextWin => 'Повышение после следующей победы'; - @override String numOfdefeats({required Object losses}) => '~${losses} поражений'; - @override String get demotionOnNextLoss => 'Понижение после следующего поражения'; - @override String get nerdStats => 'Для задротов'; - @override String get playersYouTrack => 'Отслеживаемые игроки'; - @override String get formula => 'Формула'; - @override String get exactValue => 'Точное значение'; - @override String get neverPlayedTL => 'Этот игрок никогда не играл в Тетра Лигу'; - @override String get botTL => 'Ботам нельзя играть в Тетра Лигу'; - @override String get anonTL => 'Гостям нельзя играть в Тетра Лигу'; - @override String get quickPlay => 'Быстрая Игра'; - @override String get expert => 'Эксперт'; - @override String get withMods => 'С модами'; - @override String withModsPlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: 'с ${n} модами', - one: 'с ${n} модом', - two: 'с ${n} модами', - few: 'с ${n} модами', - many: 'с ${n} модами', - other: 'с ${n} модами', - ); - @override String get exportDB => 'Экспортировать локальную базу данных'; - @override String get exportDBDescription => 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; - @override String get desktopExportAlertTitle => 'Экспорт на десктопе'; - @override String get desktopExportText => 'Похоже, вы используете десктопную версию. Проверьте папку "Документы", там вы должны найти файл "TetraStats.db". Скопируйте его куда-нибудь'; - @override String get androidExportAlertTitle => 'Экспорт на Android'; - @override String androidExportText({required Object exportedDB}) => 'Экспортировано.\n${exportedDB}'; - @override String get importDB => 'Импортировать локальную базу данных'; - @override String get importDBDescription => 'Восстановите свою резеврную копию. Обратите внимание, что текущая база данных будет перезаписана.'; - @override String get importWrongFileType => 'Неверный тип файла'; - @override String get importCancelled => 'Операция была отменена'; - @override String get importSuccess => 'Успешно импортировано'; - @override String get yourID => 'Ваш аккаунт в TETR.IO'; - @override String get yourIDAlertTitle => 'Ваш ник в TETR.IO'; - @override String get yourIDText => 'При запуске приложения оно будет получать статистику этого игрока.'; - @override String get language => 'Язык (Language)'; - @override String get updateInBackground => 'Обновлять статистику в фоне'; - @override String get updateInBackgroundDescription => 'Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кеш истекает'; - @override String get customization => 'Кастомизация'; - @override String get customizationDescription => 'Измените внешний вид пользовательского интерфейса Tetra Stats'; - @override String get oskKagari => '"Оск Кагари" прикол'; - @override String get oskKagariDescription => 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:'; - @override String get AccentColor => 'Цветовой акцент'; - @override String get AccentColorDescription => 'Почти все интерактивные элементы пользовательского интерфейса окрашены в этот цвет'; - @override String get timestamps => 'Метки времени'; - @override String get timestampsDescription => 'Вы можете выбрать, каким образом метки времени показывают время'; - @override String get timestampsAbsoluteGMT => 'Абсолютные (GMT)'; - @override String get timestampsAbsoluteLocalTime => 'Абсолютные (Ваш часовой пояс)'; - @override String get timestampsRelative => 'Относительные'; - @override String get rating => 'Основное представление рейтинга'; - @override String get ratingDescription => 'TR нелинеен, тогда как Glicko не имеет границ, а положение в таблице лидеров волатильно'; - @override String get ratingLBposition => 'Позиция в рейтинге'; - @override String get sheetbotGraphs => 'Графики-радары как у sheetBot'; - @override String get sheetbotGraphsDescription => 'Если включено, точки на графике могут появляться на противоположной стороне графика если значение отрицательное'; - @override String get lbStats => 'Показывать статистику, основанную на рейтинговой таблице'; - @override String get lbStatsDescription => 'Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате'; - @override String get aboutApp => 'О приложении'; - @override String aboutAppText({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; - @override String stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; - @override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; - @override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${nickname}'; - @override String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; - @override String stateRemoved({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!'; - @override String matchRemoved({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; - @override String get viewAllMatches => 'Все матчи'; - @override String get trackedPlayersViewTitle => 'Сохранённые данные'; - @override String get trackedPlayersZeroEntrys => 'Пустой список. Вернитесь на предыдущий экран и нажмите кнопку "Отслеживать", чтобы текущий игрок появился здесь'; - @override String get trackedPlayersOneEntry => 'В списке только один игрок'; - @override String trackedPlayersManyEntrys({required Object numberOfPlayers}) => 'В списке ${numberOfPlayers} игроков'; - @override String trackedPlayersEntry({required Object nickname, required Object numberOfStates}) => '${nickname}: ${numberOfStates} состояний'; - @override String trackedPlayersDescription({required Object firstStateDate, required Object lastStateDate}) => 'Начиная с ${firstStateDate} и заканчивая ${lastStateDate}'; - @override String trackedPlayersStatesDeleted({required Object nickname}) => 'Состояния аккаунта ${nickname} были удалены из локальной базы данных!'; - @override String get duplicatedFix => 'Удалить дубликаты матчей в Тетра Лиге'; - @override String get compressDB => 'Сжать базу данных'; - @override String SpaceSaved({required Object size}) => 'Места освобождено: ${size}'; - @override String averageXrank({required Object rankLetter}) => 'Средний ${rankLetter} ранг'; - @override String get vs => 'против'; - @override String get inTLmatch => 'в матче ТЛ'; - @override String get downloadReplay => 'Скачать .ttrm повтор'; - @override String get openReplay => 'Открыть повтор в TETR.IO'; - @override String replaySaved({required Object path}) => 'Повтор сохранён по пути ${path}'; - @override String get match => 'Матч'; - @override String get timeWeightedmatch => 'Матч (взвешенная по времени)'; - @override String roundNumber({required Object n}) => 'Раунд ${n}'; - @override String get statsFor => 'Статистика за'; - @override String get numberOfRounds => 'Количество раундов'; - @override String get matchLength => 'Продолжительность матча'; - @override String get roundLength => 'Продолжительность раунда'; - @override String get matchStats => 'Статистика матча'; - @override String get timeWeightedmatchStats => 'Взвешенная по времени cтатистика матча'; - @override String get replayIssue => 'Ошибка обработки повтора'; - @override String get matchIsTooOld => 'Информация о повторе недоступна'; - @override String get winner => 'Победитель'; - @override String get registred => 'Зарегистрирован'; - @override String get playedTL => 'Играл в Тетра Лигу'; - @override String get winChance => 'Шансы на победу'; - @override String get byGlicko => 'По Glicko'; - @override String get byEstTR => 'По расч. TR'; - @override String compareViewNoValues({required Object avgR}) => 'Пожалуйста, введите никнейм, ID, APM-PPS-VS (неважно, какой разделитель, важен порядок) или ${avgR} (где R это ранг), в оба поля'; - @override String compareViewWrongValue({required Object value}) => 'Не удалось получить ${value}'; - @override String get mostRecentOne => 'Самый последний'; - @override String get yes => 'Да'; - @override String get no => 'Нет'; - @override String get daysLater => 'дней позже'; - @override String get dayseBefore => 'дней раньше'; - @override String get fromBeginning => 'С начала'; - @override String get calc => 'Считать'; - @override String get calcViewNoValues => 'Введите значения, чтобы посчитать статистику'; - @override String get rankAveragesViewTitle => 'Требования рангов'; - @override String get sprintAndBlitsViewTitle => 'Средние результаты 40 линий и блица'; - @override String sprintAndBlitsRelevance({required Object date}) => 'Актуальность: ${date}'; - @override String get rank => 'Ранг'; - @override String get averages => 'Средние значения'; - @override String get lbViewZeroEntrys => 'Рейтинговая таблица пуста'; - @override String get lbViewOneEntry => 'В рейтинговой таблице всего один игрок'; - @override String lbViewManyEntrys({required Object numberOfPlayers}) => 'В рейтинговой таблице находится ${numberOfPlayers}'; - @override String get everyoneAverages => 'Значения таблицы'; - @override String get sortBy => 'Cортировать по'; - @override String get reversed => 'Наоборот'; - @override String get country => 'Страна'; - @override String rankAverages({required Object rank}) => 'Значения для ${rank} ранга'; - @override String players({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: '${n} игроков', - one: '${n} игрок', - two: '${n} игрока', - few: '${n} игрока', - many: '${n} игроков', - other: '${n} игроков', - ); - @override String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: '${n} игр', - one: '${n} игра', - two: '${n} игры', - few: '${n} игры', - many: '${n} игр', - other: '${n} игр', - ); - @override String gamesPlayed({required Object games}) => '${games} сыграно'; - @override String get chart => 'График'; - @override String get entries => 'Список'; - @override String get minimums => 'Минимумы'; - @override String get maximums => 'Максимумы'; - @override String get lowestValues => 'Самые низкие показатели'; - @override String get averageValues => 'Средние значения показателей'; - @override String get highestValues => 'Самые высокие показатели'; - @override String forPlayer({required Object username}) => 'для игрока ${username}'; - @override String currentAxis({required Object axis}) => 'Ось ${axis}:'; - @override String get p1nkl0bst3rAlert => 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; - @override String get notForWeb => 'Функция недоступна для веб версии'; - @override late final _StringsGraphsRu graphs = _StringsGraphsRu._(_root); - @override late final _StringsStatCellNumRu statCellNum = _StringsStatCellNumRu._(_root); + @override Map get gamemodes => { + 'league': 'Тетра Лига', + 'zenith': 'Quick Play', + 'zenithex': 'Quick Play Expert', + '40l': '40 линий', + 'blitz': 'Блиц', + '5mblast': '5 000 000 бласт', + 'zen': 'Дзен', + }; + @override late final _StringsDestinationsRuRu destinations = _StringsDestinationsRuRu._(_root); @override Map get playerRole => { 'user': 'Пользователь', 'banned': 'Заблокированный пользователь', @@ -1101,11 +1714,92 @@ class _StringsRu implements Translations { 'halfmod': 'Модератор сообщества', 'anon': 'Аноним', }; - @override late final _StringsNumOfGameActionsRu numOfGameActions = _StringsNumOfGameActionsRu._(_root); - @override late final _StringsPopupActionsRu popupActions = _StringsPopupActionsRu._(_root); - @override late final _StringsErrorsRu errors = _StringsErrorsRu._(_root); + @override String get goBackButton => 'Назад'; + @override String get nanow => 'Сейчас недоступно...'; + @override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}'; + @override String get seasonEnded => 'Сезон завершён'; + @override String overallPB({required Object pb}) => 'Абсолютный рекорд: ${pb} м'; + @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; + @override String numOfVictories({required Object wins}) => '~${wins} побед'; + @override String get promotionOnNextWin => 'Повышение после следующей победы'; + @override String numOfdefeats({required Object losses}) => '~${losses} поражений'; + @override String get demotionOnNextLoss => 'Понижение после следующего поражения'; + @override String get records => 'Записи'; + @override String get nerdStats => 'Для Задротов'; + @override String get playstyles => 'Стили игры'; + @override String get horoscopes => 'Гороскопы'; + @override String get relatedAchievements => 'Достижения режима'; + @override String get season => 'Сезон'; + @override String get smooth => 'Сглаживание'; + @override String get dateAndTime => 'Дата и время'; + @override String get TLfullLBnote => 'Большая, но позволяет сортировать игроков по их статам и фильтровать их по рангам'; + @override String get rank => 'Ранг'; + @override String verdictGeneral({required Object n, required Object verdict, required Object rank}) => 'На ${n} ${verdict} среднего ${rank}'; + @override String get verdictBetter => 'впереди'; + @override String get verdictWorse => 'позади'; + @override String get localStanding => 'по стране'; + @override late final _StringsXpRuRu xp = _StringsXpRuRu._(_root); + @override late final _StringsGametimeRuRu gametime = _StringsGametimeRuRu._(_root); + @override String get track => 'Отслеживать'; + @override String get stopTracking => 'Не отслеживать'; + @override String supporter({required Object tier}) => 'Спонсор ${tier}-го уровня'; + @override String comparingWith({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; + @override String get compare => 'Сравнить'; + @override String get comparison => 'Сравнение'; + @override String get enterUsername => 'Введите ник или \$avgX (где X это ранг)'; + @override String get general => 'Основное'; + @override String get badges => 'Значки'; + @override String obtainDate({required Object date}) => 'Получен ${date}'; + @override String get assignedManualy => 'Этот значок был присвоен вручную администрацией TETR.IO'; + @override String get distinguishment => 'Заслуга'; + @override String get banned => 'Забанен'; + @override String get bannedSubtext => 'Баны выдаются в случаях нарушений правил TETR.IO'; + @override String get badStanding => 'Плохая репутация'; + @override String get badStandingSubtext => 'Один или более банов на счету'; + @override String get botAccount => 'Бот аккаунт'; + @override String botAccountSubtext({required Object botMaintainers}) => 'Операторы: ${botMaintainers}'; + @override String get copiedToClipboard => 'Скопировано в буфер обмена!'; + @override String get bio => 'Биография'; + @override String get news => 'Новости'; + @override late final _StringsMatchResultRuRu matchResult = _StringsMatchResultRuRu._(_root); + @override late final _StringsDistinguishmentsRuRu distinguishments = _StringsDistinguishmentsRuRu._(_root); + @override late final _StringsNewsEntriesRuRu newsEntries = _StringsNewsEntriesRuRu._(_root); + @override String rankupMiddle({required Object r}) => '${r} ранг'; + @override String get copyUserID => 'Нажмите, чтобы скопировать ID'; + @override String get searchHint => 'Никнейм или ID'; + @override String get navMenu => 'Меню навигации'; + @override String get navMenuTooltip => 'Открыть меню навигации'; + @override String get refresh => 'Обновить данные'; + @override String get searchButton => 'Искать'; + @override String get trackedPlayers => 'Отслеживаемые игроки'; + @override String get standing => 'Положение'; + @override String get previousSeasons => 'Предыдущие сезоны'; + @override String get recent => 'Недавние'; + @override String get top => 'Топ'; + @override String get noRecord => 'Нет записи'; + @override String sprintAndBlitsRelevance({required Object date}) => 'Актуальность: ${date}'; + @override late final _StringsSnackBarMessagesRuRu snackBarMessages = _StringsSnackBarMessagesRuRu._(_root); + @override late final _StringsErrorsRuRu errors = _StringsErrorsRuRu._(_root); + @override late final _StringsActionsRuRu actions = _StringsActionsRuRu._(_root); + @override late final _StringsGraphsDestinationRuRu graphsDestination = _StringsGraphsDestinationRuRu._(_root); + @override late final _StringsFilterModaleRuRu filterModale = _StringsFilterModaleRuRu._(_root); + @override late final _StringsCutoffsDestinationRuRu cutoffsDestination = _StringsCutoffsDestinationRuRu._(_root); + @override late final _StringsRankViewRuRu rankView = _StringsRankViewRuRu._(_root); + @override late final _StringsStateViewRuRu stateView = _StringsStateViewRuRu._(_root); + @override late final _StringsTlMatchViewRuRu tlMatchView = _StringsTlMatchViewRuRu._(_root); + @override late final _StringsCalcDestinationRuRu calcDestination = _StringsCalcDestinationRuRu._(_root); + @override late final _StringsInfoDestinationRuRu infoDestination = _StringsInfoDestinationRuRu._(_root); + @override late final _StringsLeaderboardsDestinationRuRu leaderboardsDestination = _StringsLeaderboardsDestinationRuRu._(_root); + @override late final _StringsSavedDataDestinationRuRu savedDataDestination = _StringsSavedDataDestinationRuRu._(_root); + @override late final _StringsSettingsDestinationRuRu settingsDestination = _StringsSettingsDestinationRuRu._(_root); + @override late final _StringsHomeNavigationRuRu homeNavigation = _StringsHomeNavigationRuRu._(_root); + @override late final _StringsGraphsNavigationRuRu graphsNavigation = _StringsGraphsNavigationRuRu._(_root); + @override late final _StringsCalcNavigationRuRu calcNavigation = _StringsCalcNavigationRuRu._(_root); + @override late final _StringsFirstTimeViewRuRu firstTimeView = _StringsFirstTimeViewRuRu._(_root); + @override late final _StringsAboutViewRuRu aboutView = _StringsAboutViewRuRu._(_root); + @override late final _StringsStatsRuRu stats = _StringsStatsRuRu._(_root); @override Map get countries => { - '': 'Не выбрана', + '': 'Во всем мире', 'AF': 'Афганистан', 'AX': 'Аландские острова', 'AL': 'Албания', @@ -1336,13 +2030,13 @@ class _StringsRu implements Translations { 'TL': 'Тимор-Лешти', 'TG': 'Того', 'TK': 'Токелау', - 'TO': 'Tonga', + 'TO': 'Тонга', 'TT': 'Тринидад и Тобаго', 'TN': 'Тунис', 'TR': 'Турция', 'TM': 'Туркменистан', 'TC': 'Острова Теркс и Кайкос', - 'ТВ': 'Тувалу', + 'TV': 'Тувалу', 'UG': 'Уганда', 'UA': 'Украина', 'AE': 'Объединенные Арабские Эмираты', @@ -1367,119 +2061,594 @@ class _StringsRu implements Translations { }; } -// Path: newsParts -class _StringsNewsPartsRu implements _StringsNewsPartsEn { - _StringsNewsPartsRu._(this._root); +// Path: destinations +class _StringsDestinationsRuRu implements _StringsDestinationsEn { + _StringsDestinationsRuRu._(this._root); - @override final _StringsRu _root; // ignore: unused_field + @override final _StringsRuRu _root; // ignore: unused_field // Translations - @override String get leaderboardStart => 'Взял '; - @override String get leaderboardMiddle => 'в таблице '; - @override String get personalbest => 'Поставил новый ЛР в '; - @override String get personalbestMiddle => 'с результатом в '; - @override String get badgeStart => 'Заработал значок '; - @override String get badgeEnd => ''; - @override String get rankupStart => 'Заработал '; - @override String rankupMiddle({required Object r}) => '${r} ранг '; - @override String get rankupEnd => 'в Тетра Лиге'; - @override String get tetoSupporter => 'TETR.IO supporter'; - @override String get supporterStart => 'Стал обладателем '; - @override String get supporterGiftStart => 'Получил подарок в виде '; - @override String unknownNews({required Object type}) => 'Неизвестная новость типа ${type}'; + @override String get home => 'Дом'; + @override String get graphs => 'Графики'; + @override String get leaderboards => 'Таблицы лидеров'; + @override String get cutoffs => 'Требования рангов'; + @override String get calc => 'Калькулятор'; + @override String get info => 'Инфо-центр'; + @override String get data => 'Сохранённые данные'; + @override String get settings => 'Настройки'; } -// Path: graphs -class _StringsGraphsRu implements _StringsGraphsEn { - _StringsGraphsRu._(this._root); +// Path: xp +class _StringsXpRuRu implements _StringsXpEn { + _StringsXpRuRu._(this._root); - @override final _StringsRu _root; // ignore: unused_field + @override final _StringsRuRu _root; // ignore: unused_field // Translations - @override String get attack => 'Атака'; - @override String get speed => 'Скорость'; - @override String get defense => 'Защита'; - @override String get cheese => 'Сыр'; + @override String get title => 'Уровень Опыта'; + @override String progressToNextLevel({required Object percentage}) => 'Прогресс до следующего уровня: ${percentage}'; + @override String progressTowardsGoal({required Object goal, required Object percentage, required Object left}) => 'Прогресс с 0 XP до уровня ${goal}: ${percentage} (${left} XP осталось)'; } -// Path: statCellNum -class _StringsStatCellNumRu implements _StringsStatCellNumEn { - _StringsStatCellNumRu._(this._root); +// Path: gametime +class _StringsGametimeRuRu implements _StringsGametimeEn { + _StringsGametimeRuRu._(this._root); - @override final _StringsRu _root; // ignore: unused_field + @override final _StringsRuRu _root; // ignore: unused_field // Translations - @override String get xpLevel => 'Уровень\nопыта'; - @override String get xpProgress => 'Прогресс до следующего уровня'; - @override String xpFrom0ToLevel({required Object n}) => 'Прогресс от 0 XP до ${n} уровня'; - @override String get xpLeft => 'XP осталось'; - @override String get hoursPlayed => 'Часов\nСыграно'; - @override String get onlineGames => 'Онлайн\nИгр'; - @override String get gamesWon => 'Онлайн\nПобед'; - @override String get totalGames => 'Всего матчей'; - @override String get totalWon => 'Всего побед'; - @override String get friends => 'Друзей'; - @override String get apm => 'Атака в\nМинуту'; - @override String get vs => 'Показатель\nVersus'; - @override String get recordLB => 'Место в таблице'; - @override String get lbp => 'Положение\nв рейтинге'; - @override String get lbpShort => '№ в рейтинге'; - @override String get lbpc => 'Положение\nв рейтинге страны'; - @override String get lbpcShort => '№ по стране'; - @override String get gamesPlayed => 'Игр\nСыграно'; - @override String get gamesWonTL => 'Побед'; - @override String get winrate => 'Процент\nпобед'; - @override String get level => 'Уровень'; + @override String get title => 'Времени проведено в игре'; + @override String gametimeAday({required Object gametime}) => '${gametime} в день в среднем'; + @override String breakdown({required Object years, required Object months, required Object days, required Object minutes, required Object seconds}) => 'Это ${years} лет,\nили ${months} месяцев,\nили ${days} дней,\nили ${minutes} минут\nили ${seconds} секунд'; +} + +// Path: matchResult +class _StringsMatchResultRuRu implements _StringsMatchResultEn { + _StringsMatchResultRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get victory => 'Победа'; + @override String get defeat => 'Поражение'; + @override String get tie => 'Ничья'; + @override String get dqvictory => 'Оппонент дисквалифицирован'; + @override String get dqdefeat => 'Дисквалифицирован'; + @override String get nocontest => 'Без согласия'; + @override String get nullified => 'Отменен'; +} + +// Path: distinguishments +class _StringsDistinguishmentsRuRu implements _StringsDistinguishmentsEn { + _StringsDistinguishmentsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get noHeader => 'Заголовок отсутствует'; + @override String get noFooter => 'Подзаголовок отсуствует'; + @override String get twc => 'Чемпион мира TETR.IO'; + @override String twcYear({required Object year}) => 'Чемпионат мира по TETR.IO ${year} года'; +} + +// Path: newsEntries +class _StringsNewsEntriesRuRu implements _StringsNewsEntriesEn { + _StringsNewsEntriesRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override TextSpan leaderboard({required InlineSpan rank, required InlineSpan gametype}) => TextSpan(children: [ + const TextSpan(text: 'Заработал №'), + rank, + const TextSpan(text: ' в режиме '), + gametype, + ]); + @override TextSpan personalbest({required InlineSpan gametype, required InlineSpan pb}) => TextSpan(children: [ + const TextSpan(text: 'Новый ЛР в '), + gametype, + const TextSpan(text: ': '), + pb, + ]); + @override TextSpan badge({required InlineSpan badge}) => TextSpan(children: [ + const TextSpan(text: 'Заработал значок '), + badge, + ]); + @override TextSpan rankup({required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: 'Заработал '), + rank, + const TextSpan(text: ' в Тетра Лиге'), + ]); + @override TextSpan supporter({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Стал '), + s('спонсором TETR.IO'), + ]); + @override TextSpan supporter_gift({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Получил '), + s('спонсорку TETR.IO'), + const TextSpan(text: ' в качестве подарка'), + ]); + @override TextSpan unknown({required InlineSpan type}) => TextSpan(children: [ + const TextSpan(text: 'Неизвестная новость типа '), + type, + ]); +} + +// Path: snackBarMessages +class _StringsSnackBarMessagesRuRu implements _StringsSnackBarMessagesEn { + _StringsSnackBarMessagesRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String stateRemoved({required Object date}) => 'Состояние от ${date} удалено из базы данных!'; + @override String matchRemoved({required Object date}) => 'Матч от ${date} удален из базы данных!'; + @override String get notForWeb => 'Функция недоступна для веб-версии'; + @override String get importSuccess => 'Импорт выполнен успешно'; + @override String get importCancelled => 'Импорт был отменен'; +} + +// Path: errors +class _StringsErrorsRuRu implements _StringsErrorsEn { + _StringsErrorsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get noRecords => 'Нет записей'; + @override String get notEnoughData => 'Недостаточно данных'; + @override String get noHistorySaved => 'Нет сохраненной истории'; + @override String connection({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; + @override String get noSuchUser => 'Нет такого пользователя'; + @override String get noSuchUserSub => 'Либо вы опечатались, либо аккаунт больше не существует'; + @override String get discordNotAssigned => 'К данному Discord ID не привязан аккаунт'; + @override String get discordNotAssignedSub => 'Убедитесь, что указан правильный ID'; + @override String get history => 'История этого игрока отсутствует'; + @override String get actionSuggestion => 'Возможно, вы хотите'; + @override String get p1nkl0bst3rTLmatches => 'Матчей Тетра Лиги не найдено'; + @override String get clientException => 'Нет подключения к Интернету'; + @override String get forbidden => 'Ваш IP-адрес заблокирован'; + @override String forbiddenSub({required Object nickname}) => 'Если вы используете VPN или Proxy, выключите его. Если это не помогает, свяжитесь с ${nickname}'; + @override String get tooManyRequests => 'Слишком много запросов'; + @override String get tooManyRequestsSub => 'Повторите попытку позже'; + @override String get internal => 'Что-то случилось на стороне tetr.io'; + @override String get internalSub => 'Скорее всего, osk уже в курсе'; + @override String get internalWebVersion => 'Что-то случилось на стороне TETR.IO (или у oskware_bridge, я хз)'; + @override String get internalWebVersionSub => 'Если на osk status page нет сообщений о проблемах, дайте знать dan63047'; + @override String get oskwareBridge => 'Что-то случилось с oskware_bridge'; + @override String get oskwareBridgeSub => 'Дайте знать dan63047'; + @override String get p1nkl0bst3rForbidden => 'Сторонний API заблокировал ваш IP-адрес'; + @override String get p1nkl0bst3rTooManyRequests => 'Слишком много запросов к стороннему API. Попробуйте позже'; + @override String get p1nkl0bst3rinternal => 'Что-то случилось на стороне p1nkl0bst3r'; + @override String get p1nkl0bst3rinternalWebVersion => 'Что-то случилось на стороне p1nkl0bst3r (или на oskware_bridge, я хз)'; + @override String get replayAlreadySaved => 'Повтор уже был сохранен'; + @override String get replayExpired => 'Повтор истек и больше не доступен'; + @override String get replayRejected => 'Сторонний API заблокировал ваш IP-адрес'; +} + +// Path: actions +class _StringsActionsRuRu implements _StringsActionsEn { + _StringsActionsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get cancel => 'Отменить'; + @override String get submit => 'Подтвердить'; + @override String get ok => 'ОК'; + @override String get apply => 'Применить'; + @override String get refresh => 'Обновить'; +} + +// Path: graphsDestination +class _StringsGraphsDestinationRuRu implements _StringsGraphsDestinationEn { + _StringsGraphsDestinationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get fetchAndsaveTLHistory => 'Получить историю игрока'; + @override String get fetchAndSaveOldTLmatches => 'Получить историю матчей Тетра Лиги'; + @override String fetchAndsaveTLHistoryResult({required Object number}) => '${number} состояний было найдено'; + @override String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} матчей было найдено'; + @override String gamesPlayed({required Object games}) => '${games} сыграно'; + @override String get dateAndTime => 'Дата и время'; + @override String get filterModaleTitle => 'Фильтровать график по рангам'; +} + +// Path: filterModale +class _StringsFilterModaleRuRu implements _StringsFilterModaleEn { + _StringsFilterModaleRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get all => 'Все'; +} + +// Path: cutoffsDestination +class _StringsCutoffsDestinationRuRu implements _StringsCutoffsDestinationEn { + _StringsCutoffsDestinationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get title => 'Состояние Тетра Лиги'; + @override String relevance({required Object timestamp}) => 'на момент ${timestamp}'; + @override String get actual => 'Требование'; + @override String get target => 'Цель'; + @override String get cutoffTR => 'Треб. TR'; + @override String get targetTR => 'Целевой TR'; + @override String get state => 'Состояние'; + @override String get advanced => 'Продвинутая'; + @override String players({required Object n}) => 'Игроков (${n})'; + @override String get moreInfo => 'Подробнее'; + @override String NumberOne({required Object tr}) => '№ 1 - ${tr} TR'; + @override String inflated({required Object tr}) => 'Инфляция - ${tr} TR'; + @override String get notInflated => 'Нет инфляции'; + @override String deflated({required Object tr}) => 'Дефляция - ${tr} TR'; + @override String get notDeflated => 'Нет дефляции'; + @override String get wellDotDotDot => 'Ну-у...'; + @override String fromPlace({required Object n}) => 'от № ${n}'; + @override String get viewButton => 'Посмотреть'; +} + +// Path: rankView +class _StringsRankViewRuRu implements _StringsRankViewEn { + _StringsRankViewRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String rankTitle({required Object rank}) => 'Данные ${rank} ранга'; + @override String get everyoneTitle => 'Вся таблица'; + @override String get trRange => 'Диапазон TR'; + @override String get supposedToBe => 'Должен быть'; + @override String gap({required Object value}) => 'промежуток в ${value}'; + @override String trGap({required Object value}) => 'промежуток в ${value} TR'; + @override String get deflationGap => 'Зона дефляции'; + @override String get inflationGap => 'Зона инфляции'; + @override String get LBposRange => 'Диапазон по позициям'; + @override String overpopulated({required Object players}) => 'Переполнен ${players}'; + @override String underpopulated({required Object players}) => 'Не хватает ${players}'; + @override String get PlayersEqualSupposedToBe => 'лол'; + @override String get avgStats => 'Средние значения'; + @override String avgForRank({required Object rank}) => 'Среднее для ${rank} ранга'; + @override String get avgNerdStats => 'Средние задротские значения'; + @override String get minimums => 'Минимумы'; + @override String get maximums => 'Максимумы'; +} + +// Path: stateView +class _StringsStateViewRuRu implements _StringsStateViewEn { + _StringsStateViewRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String title({required Object date}) => 'Состояние от ${date}'; +} + +// Path: tlMatchView +class _StringsTlMatchViewRuRu implements _StringsTlMatchViewEn { + _StringsTlMatchViewRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get match => 'Матч'; + @override String get vs => 'против'; + @override String get winner => 'Победитель'; + @override String roundNumber({required Object n}) => 'Раунд ${n}'; + @override String get statsFor => 'Статистика для'; + @override String get numberOfRounds => 'Количество раундов'; + @override String get matchLength => 'Продолжительность матча'; + @override String get roundLength => 'Продолжительность раунда'; + @override String get matchStats => 'Статистика матча'; + @override String get downloadReplay => 'Скачать .ttrm повтор'; + @override String get openReplay => 'Открыть повтор в TETR.IO'; +} + +// Path: calcDestination +class _StringsCalcDestinationRuRu implements _StringsCalcDestinationEn { + _StringsCalcDestinationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String placeholders({required Object stat}) => 'Введите ваш ${stat}'; + @override String get tip => 'Введите значения и нажмите "Считать", чтобы увидеть статистику для задротов'; + @override String get statsCalcButton => 'Считать'; + @override String get damageCalcTip => 'Нажмите на действия слева, чтобы добавить их сюда'; + @override String get actions => 'Действия'; + @override String get results => 'Результаты'; + @override String get rules => 'Правила'; + @override String get noSpinClears => 'Без спинов'; + @override String get spins => 'Спины'; + @override String get miniSpins => 'Мини спины'; + @override String get noLineclear => '0 линий (сброс комбо)'; + @override String get custom => 'Custom'; + @override String get multiplier => 'Множитель'; + @override String get pcDamage => 'PC урон'; + @override String get comboTable => 'Таблица комбо'; + @override String get b2bChaining => 'Таблица комбо'; + @override String get surgeStartAtB2B => 'Начинается с B2B'; + @override String get surgeStartAmount => 'Начинается с'; + @override String get totalDamage => 'Всего урона'; + @override String get lineclears => 'Lineclears'; + @override String get combo => 'Комбо'; + @override String get surge => 'Surge'; + @override String get pcs => 'PCs'; +} + +// Path: infoDestination +class _StringsInfoDestinationRuRu implements _StringsInfoDestinationEn { + _StringsInfoDestinationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get title => 'Информационный Центр'; + @override String get sprintAndBlitzAverages => 'Средние значения для 40 линий и блиц'; + @override String get sprintAndBlitzAveragesDescription => 'Поскольку считать средние значения 40 линий и Блиц неудобно, они обновляется довольно редко. Кликните по названию этой карточки, чтобы увидеть таблицу средних значений 40 линий и Блиц'; + @override String get tetraStatsWiki => 'Tetra Stats Вики'; + @override String get tetraStatsWikiDescription => 'Узнайте больше о функциях Tetra Stats и статистике, что он предоставляет'; + @override String get about => 'О Tetra Stats'; + @override String get aboutDescription => 'Разработано dan63\n'; +} + +// Path: leaderboardsDestination +class _StringsLeaderboardsDestinationRuRu implements _StringsLeaderboardsDestinationEn { + _StringsLeaderboardsDestinationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get title => 'Таблицы лидеров'; + @override String get tl => 'Тетра Лига (Текущий сезон)'; + @override String get fullTL => 'Тетра Лига (Текущий сезон, вся за раз)'; + @override String get ar => 'Очки достижений'; +} + +// Path: savedDataDestination +class _StringsSavedDataDestinationRuRu implements _StringsSavedDataDestinationEn { + _StringsSavedDataDestinationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get title => 'Сохранённые данные'; + @override String get tip => 'Выберите никнейм слева, чтобы увидеть данные ассоциированные с ним'; + @override String seasonTLstates({required Object s}) => 'TL ${s} сезона'; + @override String get TLrecords => 'Записи TL'; +} + +// Path: settingsDestination +class _StringsSettingsDestinationRuRu implements _StringsSettingsDestinationEn { + _StringsSettingsDestinationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get title => 'Настройки'; + @override String get general => 'Общие'; + @override String get customization => 'Кастомизация'; + @override String get database => 'Локальная база данных'; + @override String get checking => 'Проверяем...'; + @override String get enterToSubmit => 'Enter, чтобы подтвердить'; + @override String get account => 'Ваш аккаунт в TETR.IO'; + @override String get accountDescription => 'Статистика этого игрока будет загружена сразу после запуска приложения. По умолчанию программа загружает мою (dan63) статистику. Чтобы изменить это, введите свой ник.'; + @override String get done => 'Готово!'; + @override String get noSuchAccount => 'Нет такого аккаунта'; + @override String get language => 'Язык'; + @override String languageDescription({required Object languages}) => 'Tetra Stats был переведен на ${languages}. По умолчанию приложение выберет язык системы или Английский, если перевода на язык системы нету.'; + @override String languages({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: 'ноль языков', + one: '${n} язык', + two: '${n} языка', + few: '${n} языка', + many: '${n} языков', + other: '${n} языков', + ); + @override String get updateInTheBackground => 'Обновлять данные в фоновом режиме'; + @override String get updateInTheBackgroundDescription => 'Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кэш истекает. Обычно это происходит каждые 5 минут'; + @override String get compareStats => 'Сравнивать статистику со средними значениями ранга'; + @override String get compareStatsDescription => 'Если включено, Tetra Stats загрузит средние значения и будет сравнивать вас со средними значениями вашего ранга. В результате этого почти каждый пункт статистики обретёт цвет, наводите курсор, что-бы узнать больше.'; + @override String get showPosition => 'Показывать позиции по статам'; + @override String get showPositionDescription => 'На загрузку потребуется немного времени (и трафика), но зато вы сможете видеть своё положение в таблице Тетра Лиги, отсортированной по статам'; + @override String get accentColor => 'Цветовой акцент'; + @override String get accentColorDescription => 'Этот цвет подчёркивает интерактивные элементы интерфейса.'; + @override String get accentColorModale => 'Выберите цвет акцента'; + @override String get timestamps => 'Формат отметок времени'; + @override String timestampsDescriptionPart1({required Object d}) => 'Вы можете выбрать вид отметок времени. По умолчанию показывается дата и время по Гринвичу, форматированная в соответствии с выбранной локалью. Пример: ${d}.'; + @override String timestampsDescriptionPart2({required Object y, required Object r}) => 'Также можно выбрать:\n• Дата и время в вашем часовом поясе: ${y}\n• Относительные отметки времени: ${r}'; + @override String get timestampsAbsoluteGMT => 'Абсолютные (по Гринвичу)'; + @override String get timestampsAbsoluteLocalTime => 'Абсолютные (ваша временная зона)'; + @override String get timestampsRelative => 'Относительные'; + @override String get sheetbotLikeGraphs => 'Графики-радары как у sheetBot'; + @override String get sheetbotLikeGraphsDescription => 'Хоть и несмотря на то, что я считаю поведение графиков sheetBot-а не совсем корректным, некоторые пользователи были в замешательстве от того, что -0,5 страйд не выглядит так, как на графике sheetBot-а. Поэтому вот моё решение: если тумблер включен, точки графика могут появляться на противоположенной стороне графика если значение со знаком минус.'; + @override String get oskKagariGimmick => '"Оск Кагари" прикол'; + @override String get oskKagariGimmickDescription => 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:.'; + @override String get bytesOfDataStored => 'данных сохранено'; + @override String get TLrecordsSaved => 'записей о матчах Тетра Лиги сохранено'; + @override String get TLplayerstatesSaved => 'состояний Тетра Лиги сохранено'; + @override String get fixButton => 'Исправить'; + @override String get compressButton => 'Сжать'; + @override String get exportDB => 'Экспортировать локальную базу данных'; + @override String get desktopExportAlertTitle => 'Экспорт на десктопе'; + @override String get desktopExportText => 'Похоже, вы используете десктопную версию. Проверьте папку "Документы", там вы должны найти файл "TetraStats.db". Скопируйте его куда-нибудь'; + @override String get androidExportAlertTitle => 'Экспорт на Android'; + @override String androidExportText({required Object exportedDB}) => 'Экспортировано.\n${exportedDB}'; + @override String get importDB => 'Импортировать локальную базу данных'; + @override String get importDBDescription => 'Восстановите свою резеврную копию. Обратите внимание, что текущая база данных будет перезаписана.'; + @override String get importWrongFileType => 'Неверный тип файла'; +} + +// Path: homeNavigation +class _StringsHomeNavigationRuRu implements _StringsHomeNavigationEn { + _StringsHomeNavigationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get overview => 'Обзор'; + @override String get standing => 'Положение'; + @override String get seasons => 'Сезоны'; + @override String get mathces => 'Матчи'; + @override String get pb => 'Рекорд'; + @override String get normal => 'Обычный'; + @override String get expert => 'Эксперт'; + @override String get expertRecords => 'Записи EX'; +} + +// Path: graphsNavigation +class _StringsGraphsNavigationRuRu implements _StringsGraphsNavigationEn { + _StringsGraphsNavigationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get history => 'История игрока'; + @override String get league => 'Состояние Лиги'; + @override String get cutoffs => 'История рангов'; +} + +// Path: calcNavigation +class _StringsCalcNavigationRuRu implements _StringsCalcNavigationEn { + _StringsCalcNavigationRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get stats => 'Калькулятор статистики'; + @override String get damage => 'Калькулятор урона'; +} + +// Path: firstTimeView +class _StringsFirstTimeViewRuRu implements _StringsFirstTimeViewEn { + _StringsFirstTimeViewRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get welcome => 'Добро пожаловать в Tetra Stats'; + @override String get description => 'Сервис, который позволяет просматривать статистику в TETR.IO'; + @override String get nicknameQuestion => 'Введите свой ник'; + @override String get inpuntHint => '(3-16 символов)'; + @override String get emptyInputError => 'Строка пуста'; + @override String niceToSeeYou({required Object n}) => 'Приятно познакомиться, ${n}'; + @override String get letsTakeALook => 'Давайте же посмотрим на ваши статы...'; + @override String get skip => 'Пропустить'; +} + +// Path: aboutView +class _StringsAboutViewRuRu implements _StringsAboutViewEn { + _StringsAboutViewRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get title => 'О Tetra Stats'; + @override String get about => 'Tetra Stats — это сервис, который работает с TETR.IO Tetra Channel API, показывает данные оттуда и считает дополнительную статистику, основанную на этих данных. Сервис позволяет отслеживать прогресс в Тетра Лиге с помощью функции "Отслеживать", которая записывает каждое изменение в Лиге в локальную базу данных (не автоматически, вы должны вручную посещать свой профиль), что позволяет потом просматривать изменения с помощью графиков.\n\nBeanserver blaster — серверная часть Tetra Stats. Она собирает полную таблицу игроков Тетра Лиги, благодаря чему сортировать эту таблицу по любой метрике и строить точечную диаграмму, что позволяет анализировать тренды Лиги. Также она предоставляет историю требований рангов, которую тоже можно посмотреть на графике.\n\nВ будущем планируется добавить анализ повторов и историю турниров, так что оставайтесь на связи.\n\nСервис ни коим образом не ассоциируется с TETR.IO или osk.'; + @override String get appVersion => 'Версия приложения'; + @override String build({required Object build}) => 'Сборка ${build}'; + @override String get GHrepo => 'Репозиторий на GitHub'; + @override String get submitAnIssue => 'Сообщить об ошибке'; + @override String get credits => 'Благодарности'; + @override String get authorAndDeveloper => 'Автор и разработчик'; + @override String get providedFormulas => 'Предоставил формулы'; + @override String get providedS1history => 'Предоставляет историю первого сезона лиги'; + @override String get inoue => 'Inoue (достаёт повторы)'; + @override String get zhCNlocale => 'Перевёл на упрощённый китайский'; + @override String get supportHim => 'Поддержите его!'; +} + +// Path: stats +class _StringsStatsRuRu implements _StringsStatsEn { + _StringsStatsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get registrationDate => 'Дата регистрации'; + @override String get gametime => 'Время в игре'; + @override String get ogp => 'Онлайн игр'; + @override String get ogw => 'Онлайн побед'; + @override String get followers => 'Подписчиков'; + @override late final _StringsStatsXpRuRu xp = _StringsStatsXpRuRu._(_root); + @override late final _StringsStatsTrRuRu tr = _StringsStatsTrRuRu._(_root); + @override late final _StringsStatsGlickoRuRu glicko = _StringsStatsGlickoRuRu._(_root); + @override late final _StringsStatsRdRuRu rd = _StringsStatsRdRuRu._(_root); + @override late final _StringsStatsGlixareRuRu glixare = _StringsStatsGlixareRuRu._(_root); + @override late final _StringsStatsS1trRuRu s1tr = _StringsStatsS1trRuRu._(_root); + @override late final _StringsStatsGpRuRu gp = _StringsStatsGpRuRu._(_root); + @override late final _StringsStatsGwRuRu gw = _StringsStatsGwRuRu._(_root); + @override late final _StringsStatsWinrateRuRu winrate = _StringsStatsWinrateRuRu._(_root); + @override late final _StringsStatsApmRuRu apm = _StringsStatsApmRuRu._(_root); + @override late final _StringsStatsPpsRuRu pps = _StringsStatsPpsRuRu._(_root); + @override late final _StringsStatsVsRuRu vs = _StringsStatsVsRuRu._(_root); + @override late final _StringsStatsAppRuRu app = _StringsStatsAppRuRu._(_root); + @override late final _StringsStatsVsapmRuRu vsapm = _StringsStatsVsapmRuRu._(_root); + @override late final _StringsStatsDssRuRu dss = _StringsStatsDssRuRu._(_root); + @override late final _StringsStatsDspRuRu dsp = _StringsStatsDspRuRu._(_root); + @override late final _StringsStatsAppdspRuRu appdsp = _StringsStatsAppdspRuRu._(_root); + @override late final _StringsStatsCheeseRuRu cheese = _StringsStatsCheeseRuRu._(_root); + @override late final _StringsStatsGbeRuRu gbe = _StringsStatsGbeRuRu._(_root); + @override late final _StringsStatsNyaappRuRu nyaapp = _StringsStatsNyaappRuRu._(_root); + @override late final _StringsStatsAreaRuRu area = _StringsStatsAreaRuRu._(_root); + @override late final _StringsStatsEtrRuRu etr = _StringsStatsEtrRuRu._(_root); + @override late final _StringsStatsEtraccRuRu etracc = _StringsStatsEtraccRuRu._(_root); + @override late final _StringsStatsOpenerRuRu opener = _StringsStatsOpenerRuRu._(_root); + @override late final _StringsStatsPlonkRuRu plonk = _StringsStatsPlonkRuRu._(_root); + @override late final _StringsStatsStrideRuRu stride = _StringsStatsStrideRuRu._(_root); + @override late final _StringsStatsInfdsRuRu infds = _StringsStatsInfdsRuRu._(_root); + @override late final _StringsStatsAltitudeRuRu altitude = _StringsStatsAltitudeRuRu._(_root); + @override late final _StringsStatsClimbSpeedRuRu climbSpeed = _StringsStatsClimbSpeedRuRu._(_root); + @override late final _StringsStatsPeakClimbSpeedRuRu peakClimbSpeed = _StringsStatsPeakClimbSpeedRuRu._(_root); + @override late final _StringsStatsKosRuRu kos = _StringsStatsKosRuRu._(_root); + @override late final _StringsStatsB2bRuRu b2b = _StringsStatsB2bRuRu._(_root); + @override late final _StringsStatsFinesseRuRu finesse = _StringsStatsFinesseRuRu._(_root); + @override late final _StringsStatsFinesseFaultsRuRu finesseFaults = _StringsStatsFinesseFaultsRuRu._(_root); + @override late final _StringsStatsTotalTimeRuRu totalTime = _StringsStatsTotalTimeRuRu._(_root); + @override late final _StringsStatsLevelRuRu level = _StringsStatsLevelRuRu._(_root); + @override late final _StringsStatsPiecesRuRu pieces = _StringsStatsPiecesRuRu._(_root); + @override late final _StringsStatsSppRuRu spp = _StringsStatsSppRuRu._(_root); + @override late final _StringsStatsKpRuRu kp = _StringsStatsKpRuRu._(_root); + @override late final _StringsStatsKppRuRu kpp = _StringsStatsKppRuRu._(_root); + @override late final _StringsStatsKpsRuRu kps = _StringsStatsKpsRuRu._(_root); + @override String blitzScore({required Object p}) => '${p} очков'; + @override String levelUpRequirement({required Object p}) => 'Очков для повышения уровня: ${p}'; + @override String get piecesTotal => 'Всего фигур установлено'; + @override String get piecesWithPerfectFinesse => 'Установлено с идеальной техникой'; @override String get score => 'Счёт'; - @override String get spp => 'Очков\nна Фигуру'; - @override String get pieces => 'Фигур\nУстановлено'; - @override String get pps => 'Фигур в\nСекунду'; - @override String get finesseFaults => 'Ошибок\nТехники'; - @override String get finessePercentage => '% Качества\nТехники'; - @override String get keys => 'Нажатий\nКлавиш'; - @override String get kpp => 'Нажатий\nна Фигуру'; - @override String get kps => 'Нажатий\nв Секунду'; - @override String get tr => 'Тетра Рейтинг'; - @override String get rd => 'Отклонение рейтинга'; - @override String get app => 'Атака на Фигуру'; - @override String get appDescription => '(Сокращенно APP) Главный показатель эффективности. Показывает, сколько атаки приходится на одну фигуру'; - @override String get vsapmDescription => 'В основном, показывает как много мусора игрок использует в своих атаках и насколько эффективно.'; - @override String get dss => 'Downstack\nв Секунду'; - @override String get dssDescription => '(Сокращенно DS/S) Downstack (спуск вниз) в Секунду показывает как много мусорных линий в среднем игрок убирает за одну секунду.'; - @override String get dsp => 'Downstack\nна Фигуру'; - @override String get dspDescription => '(Сокращенно DS/P) Downstack (спуск вниз) на Фигуру показывает как много мусорных линий в среднем игрок убирает одну фигуру.'; - @override String get appdsp => 'APP + DS/P'; - @override String get appdspDescription => 'Просто сумма Атаки на Фигуру и Downstack на Фигуру.'; - @override String get cheese => 'Индекс сыра'; - @override String get cheeseDescription => '(Сокращенно Cheese) Индекс сыра является аппроксимацией того, насколько чистый / дырявый мусор игрок отправляет. Меньше = более чистый. Больше = более дырявый.\nПридумал kerrmunism'; - @override String get gbe => 'Garbage\nEfficiency'; - @override String get gbeDescription => '(Сокращенно Gb Eff.) Garbage Efficiency показывает насколько хорошо игрок использует свой мусор. Больше = лучше (или он использует больше мусора). Меньше = в основном отправляют сыр (или он редко чистит мусор).\nПридумали Zepheniah и Dragonboy.'; - @override String get nyaapp => 'Взвешенный\nAPP'; - @override String get nyaappDescription => '(Сокращенно wAPP) По сути, показывает способность отправлять сыр, сохраняя при этом высокую эффективность.\nПридумал Wertj.'; - @override String get area => 'Area'; - @override String get areaDescription => 'Какую площадь занимает диаграмма, если не брать в расчёт индекс сыра и VS/APM'; - @override String get estOfTR => 'Расчётный TR'; - @override String get estOfTRShort => 'Расч. TR'; - @override String get accOfEst => 'Точность расчёта'; - @override String get accOfEstShort => 'Точность'; -} - -// Path: numOfGameActions -class _StringsNumOfGameActionsRu implements _StringsNumOfGameActionsEn { - _StringsNumOfGameActionsRu._(this._root); - - @override final _StringsRu _root; // ignore: unused_field - - // Translations - @override String get pc => 'Все чисто'; - @override String get hold => 'В запас'; + @override String get lines => 'Линий'; + @override String get linesShort => 'L'; + @override String get pcs => 'Perfect Clears'; + @override String get holds => 'Holds'; + @override String get spike => 'Top Spike'; + @override String top({required Object percentage}) => 'Топ ${percentage}'; + @override String topRank({required Object rank}) => 'Топ ранг: ${rank}'; + @override String get floor => 'Этаж'; + @override String get split => 'Сектор'; + @override String get total => 'Всего'; + @override String get sent => 'Отправлено'; + @override String get received => 'Получено'; + @override String get placement => 'Положение'; + @override String get peak => 'Пик'; + @override String qpWithMods({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + one: 'С 1 модом', + two: 'С ${n} модами', + few: 'С ${n} модами', + many: 'С ${n} модами', + other: 'С ${n} модами', + ); @override String inputs({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, zero: '${n} нажатий клавиш', - one: '${n} нажатие на клавишу', - two: '${n} нажатия на клавишы', - few: '${n} нажатия на клавишы', - many: '${n} нажатий на клавиш', - other: '${n} нажатий на клавиш', + one: '${n} нажатие клавиш', + two: '${n} нажатия клавиш', + few: '${n} нажатия клавиш', + many: '${n} нажатий клавиш', + other: '${n} нажатий клавиш', ); @override String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, zero: '${n} T-спинов всего', @@ -1489,7 +2658,7 @@ class _StringsNumOfGameActionsRu implements _StringsNumOfGameActionsEn { many: '${n} T-спинов всего', other: '${n} T-спинов всего', ); - @override String lineClears({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + @override String linesCleared({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, zero: '${n} линий очищено', one: '${n} линия очищена', two: '${n} линии очищено', @@ -1497,53 +2666,2082 @@ class _StringsNumOfGameActionsRu implements _StringsNumOfGameActionsEn { many: '${n} линий очищено', other: '${n} линий очищено', ); + @override late final _StringsStatsGraphsRuRu graphs = _StringsStatsGraphsRuRu._(_root); + @override String players({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} игроков', + one: '${n} игрок', + two: '${n} игрока', + few: '${n} игрока', + many: '${n} игроков', + other: '${n} игроков', + ); + @override String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} игр', + one: '${n} игра', + two: '${n} игры', + few: '${n} игры', + many: '${n} игр', + other: '${n} игр', + ); + @override late final _StringsStatsLineClearRuRu lineClear = _StringsStatsLineClearRuRu._(_root); + @override late final _StringsStatsLineClearsRuRu lineClears = _StringsStatsLineClearsRuRu._(_root); + @override String get mini => 'Mini'; + @override String get tSpin => 'T-spin'; + @override String get tSpins => 'T-spins'; + @override String get spin => 'Spin'; + @override String get spins => 'Spins'; } -// Path: popupActions -class _StringsPopupActionsRu implements _StringsPopupActionsEn { - _StringsPopupActionsRu._(this._root); +// Path: stats.xp +class _StringsStatsXpRuRu implements _StringsStatsXpEn { + _StringsStatsXpRuRu._(this._root); - @override final _StringsRu _root; // ignore: unused_field + @override final _StringsRuRu _root; // ignore: unused_field // Translations - @override String get cancel => 'Отменить'; - @override String get submit => 'Подтвердить'; - @override String get ok => 'OK'; + @override String get short => 'Опыт'; + @override String get full => 'Очки опыта'; +} + +// Path: stats.tr +class _StringsStatsTrRuRu implements _StringsStatsTrEn { + _StringsStatsTrRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'TR'; + @override String get full => 'Тетра Рейтинг'; +} + +// Path: stats.glicko +class _StringsStatsGlickoRuRu implements _StringsStatsGlickoEn { + _StringsStatsGlickoRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Glicko'; + @override String get full => 'Glicko'; +} + +// Path: stats.rd +class _StringsStatsRdRuRu implements _StringsStatsRdEn { + _StringsStatsRdRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'RD'; + @override String get full => 'Отклонение Рейтинга'; +} + +// Path: stats.glixare +class _StringsStatsGlixareRuRu implements _StringsStatsGlixareEn { + _StringsStatsGlixareRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'GXE'; + @override String get full => 'GLIXARE'; +} + +// Path: stats.s1tr +class _StringsStatsS1trRuRu implements _StringsStatsS1trEn { + _StringsStatsS1trRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'S1 TR'; + @override String get full => 'TR как в первом сезоне'; +} + +// Path: stats.gp +class _StringsStatsGpRuRu implements _StringsStatsGpEn { + _StringsStatsGpRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'GP'; + @override String get full => 'Матчей'; +} + +// Path: stats.gw +class _StringsStatsGwRuRu implements _StringsStatsGwEn { + _StringsStatsGwRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'GW'; + @override String get full => 'Побед'; +} + +// Path: stats.winrate +class _StringsStatsWinrateRuRu implements _StringsStatsWinrateEn { + _StringsStatsWinrateRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'WR%'; + @override String get full => 'Процент побед'; +} + +// Path: stats.apm +class _StringsStatsApmRuRu implements _StringsStatsApmEn { + _StringsStatsApmRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'APM'; + @override String get full => 'Атаки в Минуту'; +} + +// Path: stats.pps +class _StringsStatsPpsRuRu implements _StringsStatsPpsEn { + _StringsStatsPpsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'PPS'; + @override String get full => 'Фигур в Секунду'; +} + +// Path: stats.vs +class _StringsStatsVsRuRu implements _StringsStatsVsEn { + _StringsStatsVsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'VS'; + @override String get full => 'Показатель Versus'; +} + +// Path: stats.app +class _StringsStatsAppRuRu implements _StringsStatsAppEn { + _StringsStatsAppRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'APP'; + @override String get full => 'Атаки на Фигуру'; +} + +// Path: stats.vsapm +class _StringsStatsVsapmRuRu implements _StringsStatsVsapmEn { + _StringsStatsVsapmRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'VS/APM'; + @override String get full => 'VS / APM'; +} + +// Path: stats.dss +class _StringsStatsDssRuRu implements _StringsStatsDssEn { + _StringsStatsDssRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'DS/S'; + @override String get full => 'Спуск в секунду'; +} + +// Path: stats.dsp +class _StringsStatsDspRuRu implements _StringsStatsDspEn { + _StringsStatsDspRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'DS/P'; + @override String get full => 'Спуск на фигуру'; +} + +// Path: stats.appdsp +class _StringsStatsAppdspRuRu implements _StringsStatsAppdspEn { + _StringsStatsAppdspRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'APP+DSP'; + @override String get full => 'APP + DSP'; +} + +// Path: stats.cheese +class _StringsStatsCheeseRuRu implements _StringsStatsCheeseEn { + _StringsStatsCheeseRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Cheese'; + @override String get full => 'Индекс Сыра'; +} + +// Path: stats.gbe +class _StringsStatsGbeRuRu implements _StringsStatsGbeEn { + _StringsStatsGbeRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'GbE'; + @override String get full => 'Эффективность Мусора'; +} + +// Path: stats.nyaapp +class _StringsStatsNyaappRuRu implements _StringsStatsNyaappEn { + _StringsStatsNyaappRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'wAPP'; + @override String get full => 'Weighted APP'; +} + +// Path: stats.area +class _StringsStatsAreaRuRu implements _StringsStatsAreaEn { + _StringsStatsAreaRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Area'; + @override String get full => 'Area'; +} + +// Path: stats.etr +class _StringsStatsEtrRuRu implements _StringsStatsEtrEn { + _StringsStatsEtrRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'eTR'; + @override String get full => 'Расчётный TR'; +} + +// Path: stats.etracc +class _StringsStatsEtraccRuRu implements _StringsStatsEtraccEn { + _StringsStatsEtraccRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => '±eTR'; + @override String get full => 'Точность расчёта'; +} + +// Path: stats.opener +class _StringsStatsOpenerRuRu implements _StringsStatsOpenerEn { + _StringsStatsOpenerRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Opener'; + @override String get full => 'Опенер'; +} + +// Path: stats.plonk +class _StringsStatsPlonkRuRu implements _StringsStatsPlonkEn { + _StringsStatsPlonkRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Plonk'; + @override String get full => 'Плонк'; +} + +// Path: stats.stride +class _StringsStatsStrideRuRu implements _StringsStatsStrideEn { + _StringsStatsStrideRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Stride'; + @override String get full => 'Страйд'; +} + +// Path: stats.infds +class _StringsStatsInfdsRuRu implements _StringsStatsInfdsEn { + _StringsStatsInfdsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Inf. DS'; + @override String get full => 'Бесконечный спуск'; +} + +// Path: stats.altitude +class _StringsStatsAltitudeRuRu implements _StringsStatsAltitudeEn { + _StringsStatsAltitudeRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'м'; + @override String get full => 'Высота'; +} + +// Path: stats.climbSpeed +class _StringsStatsClimbSpeedRuRu implements _StringsStatsClimbSpeedEn { + _StringsStatsClimbSpeedRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'CSP'; + @override String get full => 'Скорость подъёма'; + @override String get gaugetTitle => 'Скорость\nПодъёма'; +} + +// Path: stats.peakClimbSpeed +class _StringsStatsPeakClimbSpeedRuRu implements _StringsStatsPeakClimbSpeedEn { + _StringsStatsPeakClimbSpeedRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Пик CSP'; + @override String get full => 'Пиковая скорость подъёма'; + @override String get gaugetTitle => 'Пик'; +} + +// Path: stats.kos +class _StringsStatsKosRuRu implements _StringsStatsKosEn { + _StringsStatsKosRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'KO\'s'; + @override String get full => 'Выбил'; +} + +// Path: stats.b2b +class _StringsStatsB2bRuRu implements _StringsStatsB2bEn { + _StringsStatsB2bRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'B2B'; + @override String get full => 'Back-To-Back'; +} + +// Path: stats.finesse +class _StringsStatsFinesseRuRu implements _StringsStatsFinesseEn { + _StringsStatsFinesseRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'F'; + @override String get full => 'Техника'; + @override String get widgetTitle => 'Техника'; +} + +// Path: stats.finesseFaults +class _StringsStatsFinesseFaultsRuRu implements _StringsStatsFinesseFaultsEn { + _StringsStatsFinesseFaultsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'FF'; + @override String get full => 'Ошибок техники'; +} + +// Path: stats.totalTime +class _StringsStatsTotalTimeRuRu implements _StringsStatsTotalTimeEn { + _StringsStatsTotalTimeRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Время'; + @override String get full => 'Общее время'; + @override String get widgetTitle => 'Общее время'; +} + +// Path: stats.level +class _StringsStatsLevelRuRu implements _StringsStatsLevelEn { + _StringsStatsLevelRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'Лвл'; + @override String get full => 'Уровень'; +} + +// Path: stats.pieces +class _StringsStatsPiecesRuRu implements _StringsStatsPiecesEn { + _StringsStatsPiecesRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'P'; + @override String get full => 'Фигур'; +} + +// Path: stats.spp +class _StringsStatsSppRuRu implements _StringsStatsSppEn { + _StringsStatsSppRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'SPP'; + @override String get full => 'Очков на Фигуру'; +} + +// Path: stats.kp +class _StringsStatsKpRuRu implements _StringsStatsKpEn { + _StringsStatsKpRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'KP'; + @override String get full => 'Нажатий клавиш'; +} + +// Path: stats.kpp +class _StringsStatsKppRuRu implements _StringsStatsKppEn { + _StringsStatsKppRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'KPP'; + @override String get full => 'Нажатий клавиш на Фигуру'; +} + +// Path: stats.kps +class _StringsStatsKpsRuRu implements _StringsStatsKpsEn { + _StringsStatsKpsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get short => 'KPS'; + @override String get full => 'Нажатий клавиш в Секунду'; +} + +// Path: stats.graphs +class _StringsStatsGraphsRuRu implements _StringsStatsGraphsEn { + _StringsStatsGraphsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get attack => 'Атака'; + @override String get speed => 'Скорость'; + @override String get defense => 'Оборона'; + @override String get cheese => 'Сыр'; +} + +// Path: stats.lineClear +class _StringsStatsLineClearRuRu implements _StringsStatsLineClearEn { + _StringsStatsLineClearRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get single => 'Single'; + @override String get double => 'Double'; + @override String get triple => 'Triple'; + @override String get quad => 'Quad'; + @override String get penta => 'Penta'; + @override String get hexa => 'Hexa'; + @override String get hepta => 'Hepta'; + @override String get octa => 'Octa'; + @override String get ennea => 'Ennea'; + @override String get deca => 'Deca'; + @override String get hendeca => 'Hendeca'; + @override String get dodeca => 'Dodeca'; + @override String get triadeca => 'Triadeca'; + @override String get tessaradeca => 'Tessaradeca'; + @override String get pentedeca => 'Pentedeca'; + @override String get hexadeca => 'Hexadeca'; + @override String get heptadeca => 'Heptadeca'; + @override String get octadeca => 'Octadeca'; + @override String get enneadeca => 'Enneadeca'; + @override String get eicosa => 'Eicosa'; + @override String get kagaris => 'Kagaris'; +} + +// Path: stats.lineClears +class _StringsStatsLineClearsRuRu implements _StringsStatsLineClearsEn { + _StringsStatsLineClearsRuRu._(this._root); + + @override final _StringsRuRu _root; // ignore: unused_field + + // Translations + @override String get zero => 'Zeros'; + @override String get single => 'Singles'; + @override String get double => 'Doubles'; + @override String get triple => 'Triples'; + @override String get quad => 'Quads'; + @override String get penta => 'Pentas'; +} + +// Path: +class _StringsZhCn implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsZhCn.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.zhCn, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + @override late final _StringsZhCn _root = this; // ignore: unused_field + + // Translations + @override Map get locales => { + 'en': '英语 (English)', + 'ru-RU': '俄语 (Русский)', + 'zh-CN': '简体中文', + }; + @override Map get gamemodes => { + 'league': 'Tetra 联赛', + 'zenith': '快速游戏', + 'zenithex': '快速游戏 · 专家模式', + '40l': '40行竞速', + 'blitz': '闪电战', + '5mblast': '5,000,000 Blast', + 'zen': '禅意模式', + }; + @override late final _StringsDestinationsZhCn destinations = _StringsDestinationsZhCn._(_root); + @override Map get playerRole => { + 'user': '用户', + 'banned': '已封禁', + 'bot': '机器人', + 'sysop': '系统操作员', + 'admin': '管理员', + 'mod': '管理员', + 'halfmod': '社区管理员', + 'anon': '匿名用户', + }; + @override String get goBackButton => '返回'; + @override String get nanow => '目前不可用...'; + @override String seasonEnds({required Object countdown}) => '当前赛季还有${countdown}结束'; + @override String get seasonEnded => '赛季已结束'; + @override String overallPB({required Object pb}) => '生涯最佳:${pb} m'; + @override String gamesUntilRanked({required Object left}) => '还有${left}局才可获得段位'; + @override String numOfVictories({required Object wins}) => '~${wins}次胜局'; + @override String get promotionOnNextWin => '下一场胜局即可升段'; + @override String numOfdefeats({required Object losses}) => '~${losses}次负局'; + @override String get demotionOnNextLoss => '下一场负局即可掉段'; + @override String get records => '记录'; + @override String get nerdStats => '详细信息'; + @override String get playstyles => '游戏方式'; + @override String get horoscopes => '散点图'; + @override String get relatedAchievements => '相关成就'; + @override String get season => '赛季'; + @override String get smooth => '平滑'; + @override String get dateAndTime => '日期和时间:'; + @override String get TLfullLBnote => '很大,但允许你通过玩家的数据对玩家进行排序,还可以按段位筛选玩家'; + @override String get rank => '段位'; + @override String verdictGeneral({required Object rank, required Object n, required Object verdict}) => '比 ${rank} 段平均数据${n} ${verdict}'; + @override String get verdictBetter => '好'; + @override String get verdictWorse => '差'; + @override String get localStanding => '本地'; + @override late final _StringsXpZhCn xp = _StringsXpZhCn._(_root); + @override late final _StringsGametimeZhCn gametime = _StringsGametimeZhCn._(_root); + @override String get track => '跟踪'; + @override String get stopTracking => '停止跟踪'; + @override String supporter({required Object tier}) => '${tier}级会员'; + @override String comparingWith({required Object newDate, required Object oldDate}) => '${newDate}的数据与${oldDate}相比'; + @override String get compare => '比较'; + @override String get comparison => '比较'; + @override String get enterUsername => '输入用户名或者\$avgX (X是一个段位)'; + @override String get general => '常规'; + @override String get badges => '勋章'; + @override String obtainDate({required Object date}) => '于${date}获得'; + @override String get assignedManualy => '此徽章由TETR.IO管理员手动颁发'; + @override String get distinguishment => '区别'; + @override String get banned => '已封禁'; + @override String get bannedSubtext => '由于 TETR.IO 规则或服务条款被违反 而被封禁'; + @override String get badStanding => '信誉不佳'; + @override String get badStandingSubtext => '近期有一次或多次违禁行为'; + @override String get botAccount => '机器人账号'; + @override String botAccountSubtext({required Object botMaintainers}) => '由${botMaintainers}管理'; + @override String get copiedToClipboard => '已复制到剪贴板!'; + @override String get bio => '个性签名'; + @override String get news => '新闻'; + @override late final _StringsMatchResultZhCn matchResult = _StringsMatchResultZhCn._(_root); + @override late final _StringsDistinguishmentsZhCn distinguishments = _StringsDistinguishmentsZhCn._(_root); + @override late final _StringsNewsEntriesZhCn newsEntries = _StringsNewsEntriesZhCn._(_root); + @override String rankupMiddle({required Object r}) => '${r} 段'; + @override String get copyUserID => '点击以复制用户 ID'; + @override String get searchHint => '用户名或 ID'; + @override String get navMenu => '导航菜单'; + @override String get navMenuTooltip => '打开导航菜单'; + @override String get refresh => '刷新数据'; + @override String get searchButton => '搜索'; + @override String get trackedPlayers => '跟踪的玩家'; + @override String get standing => '名次'; + @override String get previousSeasons => '上赛季'; + @override String get recent => '最近'; + @override String get top => '前'; + @override String get noRecord => '暂无记录'; + @override String sprintAndBlitsRelevance({required Object date}) => '${date}'; + @override late final _StringsSnackBarMessagesZhCn snackBarMessages = _StringsSnackBarMessagesZhCn._(_root); + @override late final _StringsErrorsZhCn errors = _StringsErrorsZhCn._(_root); + @override late final _StringsActionsZhCn actions = _StringsActionsZhCn._(_root); + @override late final _StringsGraphsDestinationZhCn graphsDestination = _StringsGraphsDestinationZhCn._(_root); + @override late final _StringsFilterModaleZhCn filterModale = _StringsFilterModaleZhCn._(_root); + @override late final _StringsCutoffsDestinationZhCn cutoffsDestination = _StringsCutoffsDestinationZhCn._(_root); + @override late final _StringsRankViewZhCn rankView = _StringsRankViewZhCn._(_root); + @override late final _StringsStateViewZhCn stateView = _StringsStateViewZhCn._(_root); + @override late final _StringsTlMatchViewZhCn tlMatchView = _StringsTlMatchViewZhCn._(_root); + @override late final _StringsCalcDestinationZhCn calcDestination = _StringsCalcDestinationZhCn._(_root); + @override late final _StringsInfoDestinationZhCn infoDestination = _StringsInfoDestinationZhCn._(_root); + @override late final _StringsLeaderboardsDestinationZhCn leaderboardsDestination = _StringsLeaderboardsDestinationZhCn._(_root); + @override late final _StringsSavedDataDestinationZhCn savedDataDestination = _StringsSavedDataDestinationZhCn._(_root); + @override late final _StringsSettingsDestinationZhCn settingsDestination = _StringsSettingsDestinationZhCn._(_root); + @override late final _StringsHomeNavigationZhCn homeNavigation = _StringsHomeNavigationZhCn._(_root); + @override late final _StringsGraphsNavigationZhCn graphsNavigation = _StringsGraphsNavigationZhCn._(_root); + @override late final _StringsCalcNavigationZhCn calcNavigation = _StringsCalcNavigationZhCn._(_root); + @override late final _StringsFirstTimeViewZhCn firstTimeView = _StringsFirstTimeViewZhCn._(_root); + @override late final _StringsAboutViewZhCn aboutView = _StringsAboutViewZhCn._(_root); + @override late final _StringsStatsZhCn stats = _StringsStatsZhCn._(_root); + @override Map get countries => { + '': '全球', + 'AF': '阿富汗', + 'AX': '奥兰群岛', + 'AL': '阿尔巴尼亚', + 'DZ': '阿尔及利亚', + 'AS': '美属萨摩亚', + 'AD': '安道尔', + 'AO': '安哥拉', + 'AI': '安圭拉', + 'AQ': '南极洲', + 'AG': '安提瓜和巴布达', + 'AR': '阿根廷', + 'AM': '亚美尼亚', + 'AW': '阿鲁巴', + 'AU': '澳大利亚', + 'AT': '奥地利', + 'AZ': '阿塞拜疆', + 'BS': '巴哈马', + 'BH': '巴林', + 'BD': '孟加拉国', + 'BB': '巴巴多斯', + 'BY': '白俄罗斯', + 'BE': '比利时', + 'BZ': '伯利兹', + 'BJ': '贝宁', + 'BM': '百慕大', + 'BT': '不丹', + 'BO': '玻利维亚', + 'BA': '波黑', + 'BW': '博茨瓦纳', + 'BV': '布韦岛', + 'BR': '巴西', + 'IO': '英属印度洋领地', + 'BN': '文莱', + 'BG': '保加利亚', + 'BF': '布基纳法索', + 'BI': '布隆迪', + 'KH': '柬埔寨', + 'CM': '喀麦隆', + 'CA': '加拿大', + 'CV': '佛得角', + 'BQ': '荷兰加勒比区', + 'KY': '开曼群岛', + 'CF': '中非', + 'TD': '乍得', + 'CL': '智利', + 'CN': '中国', + 'CX': '圣诞岛', + 'CC': '科科斯群岛', + 'CO': '哥伦比亚', + 'KM': '科摩罗', + 'CG': '刚果(布)', + 'CD': '刚果(金)', + 'CK': '库克群岛', + 'CR': '哥斯达黎加', + 'CI': '科特迪瓦', + 'HR': '克罗地亚', + 'CU': '古巴', + 'CW': '库拉索', + 'CY': '塞浦路斯', + 'CZ': '捷克', + 'DK': '丹麦', + 'DJ': '吉布提', + 'DM': '多米尼克', + 'DO': '多米尼加共和国', + 'EC': '厄瓜多尔', + 'EG': '埃及', + 'SV': '萨尔瓦多', + 'GB-ENG': '英格兰', + 'GQ': '赤道几内亚', + 'ER': '厄立特里亚', + 'EE': '爱沙尼亚', + 'ET': '埃塞俄比亚', + 'EU': '欧洲', + 'FK': '福克兰群岛 (马尔维纳斯)', + 'FO': '法罗群岛', + 'FJ': '斐济', + 'FI': '芬兰', + 'FR': '法国', + 'GF': '法属圭亚那', + 'PF': '法属波利尼西亚', + 'TF': '法属南部领地', + 'GA': '加蓬', + 'GM': '冈比亚', + 'GE': '格鲁吉亚', + 'DE': '德国', + 'GH': '加纳', + 'GI': '直布罗陀', + 'GR': '希腊', + 'GL': '格陵兰', + 'GD': '格林纳达', + 'GP': '瓜德罗普', + 'GU': '关岛', + 'GT': '危地马拉', + 'GG': '根西', + 'GN': '几内亚', + 'GW': '几内亚比绍', + 'GY': '圭亚那', + 'HT': '海地', + 'HM': '赫德岛和麦克唐纳群岛', + 'VA': '梵蒂冈', + 'HN': '洪都拉斯', + 'HK': '中国香港', + 'HU': '匈牙利', + 'IS': '冰岛', + 'IN': '印度', + 'ID': '印尼', + 'IR': '伊朗', + 'IQ': '伊拉克', + 'IE': '爱尔兰', + 'IM': '马恩岛', + 'IL': '以色列', + 'IT': '意大利', + 'JM': '牙买加', + 'JP': '日本', + 'JE': '泽西', + 'JO': '约旦', + 'KZ': '哈萨克斯坦', + 'KE': '肯尼亚', + 'KI': '基里巴斯', + 'KP': '朝鲜', + 'KR': '韩国', + 'XK': '科索沃', + 'KW': '科威特', + 'KG': '吉尔吉斯斯坦', + 'LA': '老挝', + 'LV': '拉托维亚', + 'LB': '黎巴嫩', + 'LS': '莱索托', + 'LR': '利比里亚', + 'LY': '利比亚', + 'LI': '列支敦士登', + 'LT': '立陶宛', + 'LU': '卢森堡', + 'MO': '中国澳门', + 'MK': '马其顿', + 'MG': '马达加斯加', + 'MW': '马拉维', + 'MY': '马来西亚', + 'MV': '马尔代夫', + 'ML': '马里', + 'MT': '马耳他', + 'MH': '马绍尔群岛', + 'MQ': '马提尼克', + 'MR': '毛里塔尼亚', + 'MU': '毛里求斯', + 'YT': '马约特岛', + 'MX': '墨西哥', + 'FM': '密克罗尼西亚', + 'MD': '摩尔多瓦', + 'MC': '摩纳哥', + 'ME': '黑山', + 'MA': '摩洛哥', + 'MN': '蒙古', + 'MS': '蒙特塞拉特', + 'MZ': '莫桑比克', + 'MM': '缅甸', + 'NA': '纳米比亚', + 'NR': '瑙鲁', + 'NP': '尼泊尔', + 'NL': '荷兰', + 'AN': '荷属安地列斯', + 'NC': '新喀里多尼亚', + 'NZ': '新西兰', + 'NI': '尼加拉瓜', + 'NE': '尼日尔', + 'NG': '尼日利亚', + 'NU': '纽埃', + 'NF': '诺福克岛', + 'GB-NIR': '北爱尔兰', + 'MP': '北马里亚纳群岛', + 'NO': '挪威', + 'OM': '阿曼', + 'PK': '巴基斯坦', + 'PW': '帕劳', + 'PS': '巴勒斯坦', + 'PA': '巴拿马', + 'PG': '巴布亚新几内亚', + 'PY': '巴拉圭', + 'PE': '秘鲁', + 'PH': '菲律宾', + 'PN': '皮特凯恩', + 'PL': '波兰', + 'PT': '葡萄牙', + 'PR': '波多黎哥', + 'QA': '卡塔尔', + 'RE': '留尼汪', + 'RO': '罗马尼亚', + 'RU': '俄罗斯', + 'RW': '卢旺达', + 'BL': '圣巴泰勒米', + 'SH': '圣赫勒拿-阿森松-特里斯坦达库尼亚', + 'KN': '圣基茨和尼维斯', + 'LC': '圣卢西亚', + 'MF': '圣马丁', + 'PM': '圣皮埃尔和密克隆', + 'VC': '圣文森特和格林纳丁斯', + 'WS': '萨摩亚', + 'SM': '圣马利诺', + 'ST': '圣多美和普林西比', + 'SA': '沙特阿拉伯', + 'GB-SCT': '苏格兰', + 'SN': '塞内加尔', + 'RS': '塞尔维亚', + 'SC': '塞舌尔', + 'SL': '塞拉利昂', + 'SG': '新加坡', + 'SX': '荷属圣马丁', + 'SK': '斯洛伐克', + 'SI': '斯洛文尼亚', + 'SB': '所罗门', + 'SO': '索马里', + 'ZA': '南非', + 'GS': '南乔治亚和南桑威奇', + 'SS': '南苏丹', + 'ES': '西班牙', + 'LK': '斯里兰卡', + 'SD': '苏丹', + 'SR': '苏里南', + 'SJ': '斯瓦尔巴和扬马延', + 'SZ': '斯威士兰', + 'SE': '瑞典', + 'CH': '瑞士', + 'SY': '叙利亚', + 'TW': '中国台湾', + 'TJ': '塔吉克斯坦', + 'TZ': '坦桑尼亚', + 'TH': '泰国', + 'TL': '东帝汶', + 'TG': '多哥', + 'TK': '托克劳', + 'TO': '汤加', + 'TT': '特立尼达和多巴哥', + 'TN': '突尼斯', + 'TR': '土耳其', + 'TM': '土库曼斯坦', + 'TC': '特克斯河凯科斯群岛', + 'TV': '图瓦卢', + 'UG': '乌干达', + 'UA': '乌克兰', + 'AE': '阿联酋', + 'GB': '英国', + 'US': '美国', + 'UY': '乌拉圭', + 'UM': '美国本土外小岛屿', + 'UZ': '乌兹别克斯坦', + 'VU': '瓦努阿图', + 'VE': '委内瑞拉', + 'VN': '越南', + 'VG': '英属维京群岛', + 'VI': '美属维京群岛', + 'GB-WLS': '威尔士', + 'WF': '瓦利斯群岛和富图纳群岛', + 'EH': '西撒哈拉', + 'YE': '也门', + 'ZM': '赞比亚', + 'ZW': '津巴布韦', + 'XX': '未知', + 'XM': '月球', + }; +} + +// Path: destinations +class _StringsDestinationsZhCn implements _StringsDestinationsEn { + _StringsDestinationsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get home => '主页'; + @override String get graphs => '图表'; + @override String get leaderboards => '排行榜'; + @override String get cutoffs => '段位分界线'; + @override String get calc => '计算器'; + @override String get info => '信息中心'; + @override String get data => '已保存的数据'; + @override String get settings => '设置'; +} + +// Path: xp +class _StringsXpZhCn implements _StringsXpEn { + _StringsXpZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => '经验等级'; + @override String progressToNextLevel({required Object percentage}) => '到下一等级的进度:${percentage}'; + @override String progressTowardsGoal({required Object goal, required Object percentage, required Object left}) => '从0级到${goal}级的进度:${percentage} (还差 ${left} 点经验值)'; +} + +// Path: gametime +class _StringsGametimeZhCn implements _StringsGametimeEn { + _StringsGametimeZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => '精确游戏时长'; + @override String gametimeAday({required Object gametime}) => '平均每天${gametime}'; + @override String breakdown({required Object years, required Object months, required Object days, required Object minutes, required Object seconds}) => '相当于 ${years} 年,\n${months} 月,\n${days} 天,\n${minutes} 分钟,\n${seconds} 秒'; +} + +// Path: matchResult +class _StringsMatchResultZhCn implements _StringsMatchResultEn { + _StringsMatchResultZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get victory => '胜利'; + @override String get defeat => '失败'; + @override String get tie => '平局'; + @override String get dqvictory => '对手被取消资格'; + @override String get dqdefeat => '被取消资格'; + @override String get nocontest => '无竞赛记录'; + @override String get nullified => '竞赛记录已取消'; +} + +// Path: distinguishments +class _StringsDistinguishmentsZhCn implements _StringsDistinguishmentsEn { + _StringsDistinguishmentsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get noHeader => '缺少标题'; + @override String get noFooter => '缺少标题'; + @override String get twc => 'TETR.IO 世界冠军'; + @override String twcYear({required Object year}) => '${year} TETR.IO 世界杯'; +} + +// Path: newsEntries +class _StringsNewsEntriesZhCn implements _StringsNewsEntriesEn { + _StringsNewsEntriesZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override TextSpan leaderboard({required InlineSpan gametype, required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: '在'), + gametype, + const TextSpan(text: '中荣获第'), + rank, + const TextSpan(text: '名'), + ]); + @override TextSpan personalbest({required InlineSpan gametype, required InlineSpan pb}) => TextSpan(children: [ + const TextSpan(text: '在'), + gametype, + const TextSpan(text: '中取得新纪录:'), + pb, + ]); + @override TextSpan badge({required InlineSpan badge}) => TextSpan(children: [ + const TextSpan(text: '获得勋章 '), + badge, + ]); + @override TextSpan rankup({required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: '升 '), + rank, + ]); + @override TextSpan supporter({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: '成为'), + s('TETR.IO supporter'), + ]); + @override TextSpan supporter_gift({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: '被赠送'), + s('TETR.IO supporter'), + ]); + @override TextSpan unknown({required InlineSpan type}) => TextSpan(children: [ + const TextSpan(text: '未知新闻类型 '), + type, + ]); +} + +// Path: snackBarMessages +class _StringsSnackBarMessagesZhCn implements _StringsSnackBarMessagesEn { + _StringsSnackBarMessagesZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String stateRemoved({required Object date}) => '成功移除${date}时的状态!'; + @override String matchRemoved({required Object date}) => '成功移除${date}时的一局!'; + @override String get notForWeb => '此功能在网络版本中不可用'; + @override String get importSuccess => '导入成功'; + @override String get importCancelled => '导入已取消'; } // Path: errors -class _StringsErrorsRu implements _StringsErrorsEn { - _StringsErrorsRu._(this._root); +class _StringsErrorsZhCn implements _StringsErrorsEn { + _StringsErrorsZhCn._(this._root); - @override final _StringsRu _root; // ignore: unused_field + @override final _StringsZhCn _root; // ignore: unused_field // Translations - @override String connection({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; - @override String get noSuchUser => 'Нет такого пользователя'; - @override String get noSuchUserSub => 'Либо вы ошиблись при вводе, либо аккаунта больше не существует'; - @override String get discordNotAssigned => 'К данному Discord ID не привязан аккаунт'; - @override String get discordNotAssignedSub => 'Убедитесь в том, что вы вставили правильный ID'; - @override String get history => 'История данного игрока отсутствует'; - @override String get actionSuggestion => 'Возможно, вы хотите'; - @override String get p1nkl0bst3rTLmatches => 'Старых матчей Тетра Лиги не было найдено'; - @override String get clientException => 'Нет соединения с интернетом'; - @override String get forbidden => 'Ваш IP адрес заблокирован'; - @override String forbiddenSub({required Object nickname}) => 'Если у вас работает VPN или прокси, выключите его. Если это не помогло, свяжитесь с ${nickname}'; - @override String get tooManyRequests => 'Слишком много запросов'; - @override String get tooManyRequestsSub => 'Подождите немного и попробуйте снова'; - @override String get internal => 'Что-то случилось на стороне tetr.io'; - @override String get internalSub => 'Скорее всего, osk уже в курсе об этом'; - @override String get internalWebVersion => 'Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)'; - @override String get internalWebVersionSub => 'Если статус страница osk-а говорит, что всё ок - свяжитесь с dan63047'; - @override String get oskwareBridge => 'Что-то случилось с oskware_bridge'; - @override String get oskwareBridgeSub => 'Дайте dan63047 знать'; - @override String get p1nkl0bst3rForbidden => 'Стороннее API заблокировало ваш IP адрес'; - @override String get p1nkl0bst3rTooManyRequests => 'Слишком много запросов к стороннему API. Попробуйте позже'; - @override String get p1nkl0bst3rinternal => 'Что-то случилось на стороне p1nkl0bst3r-а'; - @override String get p1nkl0bst3rinternalWebVersion => 'Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)'; - @override String get replayAlreadySaved => 'Повтор уже сохранён'; - @override String get replayExpired => 'Повтор истёк и больше недоступен'; - @override String get replayRejected => 'Стороннее API заблокировало ваш IP адрес'; + @override String get noRecords => '暂无记录'; + @override String get notEnoughData => '数据不足'; + @override String get noHistorySaved => '没有保存历史记录'; + @override String connection({required Object code, required Object message}) => '连接错误:${code} ${message}'; + @override String get noSuchUser => '用户不存在'; + @override String get noSuchUserSub => '您输入的内容有误,或者用户不存在'; + @override String get discordNotAssigned => '没有指定Discord ID的用户'; + @override String get discordNotAssignedSub => '请确保您提供了有效的 ID'; + @override String get history => '缺少该玩家的历史'; + @override String get actionSuggestion => '也许,您想要'; + @override String get p1nkl0bst3rTLmatches => '没有找到Tetra联赛比赛'; + @override String get clientException => '你尚未连接'; + @override String get forbidden => '您的 IP 地址已被封禁'; + @override String forbiddenSub({required Object nickname}) => '如果你在使用VPN,请关闭。如果仍然不可以,请联系${nickname}'; + @override String get tooManyRequests => '您的评分已经被限制'; + @override String get tooManyRequestsSub => '请稍后重试'; + @override String get internal => 'TETR.IO 出现了问题!'; + @override String get internalSub => 'osk,或许,已经知道了'; + @override String get internalWebVersion => 'TETR.IO (也许是oskware_bridge,我不知道到底是哪儿) 出现了问题!'; + @override String get internalWebVersionSub => '如果 osk status 页面显示一切都很正常,请联系dan63047'; + @override String get oskwareBridge => 'oskware_bridge 出现了问题!'; + @override String get oskwareBridgeSub => '请联系dan63047'; + @override String get p1nkl0bst3rForbidden => '第三方API屏蔽了您的 IP 地址'; + @override String get p1nkl0bst3rTooManyRequests => '第三方API请求过多,请稍后再试'; + @override String get p1nkl0bst3rinternal => 'p1nkl0bst3r 那边出现了问题!'; + @override String get p1nkl0bst3rinternalWebVersion => 'p1nkl0bst3r 那边(也许是oskware_bridge,我不知道到底是哪儿)出现了问题!'; + @override String get replayAlreadySaved => '回放已保存'; + @override String get replayExpired => '回放已过期或不再可用'; + @override String get replayRejected => '第三方API屏蔽了您的 IP 地址'; +} + +// Path: actions +class _StringsActionsZhCn implements _StringsActionsEn { + _StringsActionsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get cancel => '取消'; + @override String get submit => '确定'; + @override String get ok => '确定'; + @override String get apply => '应用'; + @override String get refresh => '刷新'; +} + +// Path: graphsDestination +class _StringsGraphsDestinationZhCn implements _StringsGraphsDestinationEn { + _StringsGraphsDestinationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get fetchAndsaveTLHistory => '获取玩家历史'; + @override String get fetchAndSaveOldTLmatches => '获取 Tetra 联赛历史记录'; + @override String fetchAndsaveTLHistoryResult({required Object number}) => '找到 ${number} 个状态'; + @override String fetchAndSaveOldTLmatchesResult({required Object number}) => '找到 ${number} 场比赛'; + @override String gamesPlayed({required Object games}) => '游玩次数:${games}'; + @override String get dateAndTime => '日期和时间'; + @override String get filterModaleTitle => '在图表上筛选等级'; +} + +// Path: filterModale +class _StringsFilterModaleZhCn implements _StringsFilterModaleEn { + _StringsFilterModaleZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get all => '全部'; +} + +// Path: cutoffsDestination +class _StringsCutoffsDestinationZhCn implements _StringsCutoffsDestinationEn { + _StringsCutoffsDestinationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => 'Tetra 联赛 状态'; + @override String relevance({required Object timestamp}) => '${timestamp}'; + @override String get actual => '实际'; + @override String get target => '目标'; + @override String get cutoffTR => '分段 TR'; + @override String get targetTR => '目标 TR'; + @override String get state => '状态'; + @override String get advanced => '高级选项'; + @override String players({required Object n}) => '玩家(${n})'; + @override String get moreInfo => '更多信息'; + @override String NumberOne({required Object tr}) => '№ 1 is ${tr} TR'; + @override String inflated({required Object tr}) => '高于目标 ${tr}'; + @override String get notInflated => '不偏高'; + @override String deflated({required Object tr}) => '低于目标 ${tr}'; + @override String get notDeflated => '不偏低'; + @override String get wellDotDotDot => '嗯…'; + @override String fromPlace({required Object n}) => '自 № ${n}'; + @override String get viewButton => '查看'; +} + +// Path: rankView +class _StringsRankViewZhCn implements _StringsRankViewEn { + _StringsRankViewZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String rankTitle({required Object rank}) => '${rank} 段数据'; + @override String get everyoneTitle => '全部排行榜'; + @override String get trRange => 'TR 范围'; + @override String get supposedToBe => '应为'; + @override String gap({required Object value}) => '相差 ${value}'; + @override String trGap({required Object value}) => '相差 ${value} TR'; + @override String get deflationGap => '偏低量'; + @override String get inflationGap => '偏高量'; + @override String get LBposRange => '排行榜位置范围'; + @override String overpopulated({required Object players}) => '比期望的多 ${players}'; + @override String underpopulated({required Object players}) => '比期望的少 ${players}'; + @override String get PlayersEqualSupposedToBe => '符合'; + @override String get avgStats => '平均数据'; + @override String avgForRank({required Object rank}) => '${rank} 段平均数据'; + @override String get avgNerdStats => '平均详细信息'; + @override String get minimums => '最小值'; + @override String get maximums => '最大值'; +} + +// Path: stateView +class _StringsStateViewZhCn implements _StringsStateViewEn { + _StringsStateViewZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String title({required Object date}) => '${date}的状态'; +} + +// Path: tlMatchView +class _StringsTlMatchViewZhCn implements _StringsTlMatchViewEn { + _StringsTlMatchViewZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get match => '匹配'; + @override String get vs => 'vs'; + @override String get winner => '获胜者'; + @override String roundNumber({required Object n}) => '第${n}回合'; + @override String get statsFor => '状态'; + @override String get numberOfRounds => '回合数'; + @override String get matchLength => '比赛时长'; + @override String get roundLength => '回合时长'; + @override String get matchStats => '比赛数据'; + @override String get downloadReplay => '下载 .ttrm 回放'; + @override String get openReplay => '在 TETR.IO 中打开回放'; +} + +// Path: calcDestination +class _StringsCalcDestinationZhCn implements _StringsCalcDestinationEn { + _StringsCalcDestinationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String placeholders({required Object stat}) => '输入你的${stat}'; + @override String get tip => '输入值并按 "计算" 来查看TA的详细信息'; + @override String get statsCalcButton => '计算'; + @override String get damageCalcTip => '点击左侧的操作在此添加'; + @override String get actions => '操作'; + @override String get results => '结果'; + @override String get rules => '规则'; + @override String get noSpinClears => '非 Spin 清除'; + @override String get spins => 'Spin'; + @override String get miniSpins => 'Mini spin'; + @override String get noLineclear => '无清除(连消结束)'; + @override String get custom => '自定义'; + @override String get multiplier => '倍增'; + @override String get pcDamage => '全消伤害'; + @override String get comboTable => '连击'; + @override String get b2bChaining => 'B2B增伤'; + @override String get surgeStartAtB2B => '开始于B2B'; + @override String get surgeStartAmount => '初始值'; + @override String get totalDamage => '累计伤害'; + @override String get lineclears => '清除行数'; + @override String get combo => '连击'; + @override String get surge => 'B2B充能'; + @override String get pcs => '全消'; +} + +// Path: infoDestination +class _StringsInfoDestinationZhCn implements _StringsInfoDestinationEn { + _StringsInfoDestinationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => '信息中心'; + @override String get sprintAndBlitzAverages => '40 行 & 闪电战平均数据'; + @override String get sprintAndBlitzAveragesDescription => '计算40 行 & 闪电战平均数据是个很繁琐的过程,所以很久才会更新一次。 点击标题查看完整的 40 行 & 闪电战平均数据表'; + @override String get tetraStatsWiki => 'Tetra Stats Wiki'; + @override String get tetraStatsWikiDescription => '查看更多关于Tetra Stats提供的功能和数据'; + @override String get about => '关于 Tetra Stats'; + @override String get aboutDescription => '由 dan63 开发'; +} + +// Path: leaderboardsDestination +class _StringsLeaderboardsDestinationZhCn implements _StringsLeaderboardsDestinationEn { + _StringsLeaderboardsDestinationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => '排行榜'; + @override String get tl => 'Tetra 联赛(当前赛季)'; + @override String get fullTL => 'Tetra 联赛(当前赛季,完整)'; + @override String get ar => '成就点'; +} + +// Path: savedDataDestination +class _StringsSavedDataDestinationZhCn implements _StringsSavedDataDestinationEn { + _StringsSavedDataDestinationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => '已保存的数据'; + @override String get tip => '选择左边的昵称以查看与之相关的数据'; + @override String seasonTLstates({required Object s}) => '第${s}赛季状态'; + @override String get TLrecords => '联赛记录'; +} + +// Path: settingsDestination +class _StringsSettingsDestinationZhCn implements _StringsSettingsDestinationEn { + _StringsSettingsDestinationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => '设置'; + @override String get general => '常规'; + @override String get customization => '自定义设置'; + @override String get database => '本地数据库'; + @override String get checking => '正在检查...'; + @override String get enterToSubmit => '按回车键提交'; + @override String get account => '您的 TETR.IO 账号'; + @override String get accountDescription => '该玩家的状态将在启动此应用后立即加载。 默认情况下,它会加载我的数据。如要更改,请在此输入您的昵称。'; + @override String get done => '完成!'; + @override String get noSuchAccount => '账号不存在'; + @override String get language => '语言'; + @override String languageDescription({required Object languages}) => 'Tetra Stats 有${languages}。默认情况下,应用程序将选择您的系统语言,如果您的系统区域设置不可用,则选择英语。'; + @override String languages({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '0种语言', + one: '${n}种语言', + two: '${n}种语言', + few: '${n}种语言', + many: '${n}种语言', + other: '${n}种语言', + ); + @override String get updateInTheBackground => '后台更新数据'; + @override String get updateInTheBackgroundDescription => '如果开启,Tetra Stats将尝试在缓存过期后查询新信息。通常一次/5分钟。'; + @override String get compareStats => '将TL数据与段位平均水平作比较'; + @override String get compareStatsDescription => '如果开启,Tetra Stats将提供额外的量度,使您能够将自己与普通玩家的等级相比较。 你看到它的方式——统计信息将以相应的颜色高亮,用光标悬停在它们上面以获取更多信息。'; + @override String get showPosition => '显示排行榜中的位置'; + @override String get showPositionDescription => '这可能需要一些时间(和流量),但您可以看到您在排行榜上的位置,按数据排序'; + @override String get accentColor => '主题色'; + @override String get accentColorDescription => '这种颜色会在这个应用上可见,而且通常会高亮显示交互界面元素。'; + @override String get accentColorModale => '选取主题色'; + @override String get timestamps => '时间戳格式'; + @override String timestampsDescriptionPart1({required Object d}) => '您可以选择时间戳显示时间的方式。默认情况下,它们以 GMT 时区显示时间,并根据所选区域设置进行格式设置,例如:${d}。'; + @override String timestampsDescriptionPart2({required Object y, required Object r}) => '这里还有:\n• 以您的时区设置的区域设置:${y}\n• 相对时间戳:${r}'; + @override String get timestampsAbsoluteGMT => 'GMT'; + @override String get timestampsAbsoluteLocalTime => '您的时区'; + @override String get timestampsRelative => '相对'; + @override String get sheetbotLikeGraphs => 'Sheetbot 型雷达图'; + @override String get sheetbotLikeGraphsDescription => '尽管我认为,图表在 SheetBot 中的工作方式不是很正确,有些人感到困惑,那 -0.5 Stride 看起来不像它在 SheetBot 图表上那样。因此,我们这里有:如果开启,则如果数值为负,则图形上的点可以出现在图形的另一半。'; + @override String get oskKagariGimmick => 'Osk-Kagari'; + @override String get oskKagariGimmickDescription => '如果开启,osk的段位会显示为:kagari:'; + @override String get bytesOfDataStored => '存储数据'; + @override String get TLrecordsSaved => '已保存 Tetra 联赛记录'; + @override String get TLplayerstatesSaved => '已保存 Tetra 联赛玩家状态'; + @override String get fixButton => '修复'; + @override String get compressButton => '压缩'; + @override String get exportDB => '导出本地数据库'; + @override String get desktopExportAlertTitle => '桌面导出'; + @override String get desktopExportText => '看起来您在桌面上使用了这个应用程序。请检查您的文档文件夹,您应该找到"TetraStats.db"。请将其复制到某处'; + @override String get androidExportAlertTitle => 'Android 导出'; + @override String androidExportText({required Object exportedDB}) => '已导出。\n${exportedDB}'; + @override String get importDB => '导入本地数据库'; + @override String get importDBDescription => '还原您的备份。请注意已存储的数据库将被覆盖。'; + @override String get importWrongFileType => '文件类型错误!'; +} + +// Path: homeNavigation +class _StringsHomeNavigationZhCn implements _StringsHomeNavigationEn { + _StringsHomeNavigationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get overview => '概览'; + @override String get standing => '名次'; + @override String get seasons => '赛季'; + @override String get mathces => '比赛场次'; + @override String get pb => '个人最佳'; + @override String get normal => '普通模式'; + @override String get expert => '专家模式'; + @override String get expertRecords => '专家模式记录'; +} + +// Path: graphsNavigation +class _StringsGraphsNavigationZhCn implements _StringsGraphsNavigationEn { + _StringsGraphsNavigationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get history => '玩家历史记录'; + @override String get league => '联赛状态'; + @override String get cutoffs => '分段线历史'; +} + +// Path: calcNavigation +class _StringsCalcNavigationZhCn implements _StringsCalcNavigationEn { + _StringsCalcNavigationZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get stats => '数据计算器'; + @override String get damage => '伤害计算器'; +} + +// Path: firstTimeView +class _StringsFirstTimeViewZhCn implements _StringsFirstTimeViewEn { + _StringsFirstTimeViewZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get welcome => '欢迎使用 Tetra Stats'; + @override String get description => '服务,允许您跟踪TETR.IO的各种数据'; + @override String get nicknameQuestion => '您的昵称是?'; + @override String get inpuntHint => '在此处输入... (3-16个符号)'; + @override String get emptyInputError => '不能提交空字符串'; + @override String niceToSeeYou({required Object n}) => '很高兴见到你,${n}'; + @override String get letsTakeALook => '让我们看看您的统计资料...'; + @override String get skip => '跳过'; +} + +// Path: aboutView +class _StringsAboutViewZhCn implements _StringsAboutViewEn { + _StringsAboutViewZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get title => '关于 Tetra Stats'; + @override String get about => 'Tetra Stats是一种服务,与TETR.IO Tetra Channel API共用,提供数据并根据这种数据计算一些附加度量。 服务允许用户用"Track"功能跟踪他们在Tetra League中的进度,这个功能记录每个Tetra Leage更改到本地数据库(非自动) 您必须不时地访问服务。这样,这些更改可以通过图表来查看。\n\nBeanserver blaster 是Tetra Stats的一部分,它被拆解成服务器侧脚本。 它提供完整的Tetra League排行榜,允许Tetra Stats通过任何公式对排行榜进行排序并生成散点图,这允许用户分析Tetra联赛趋势。 它还提供了Tetra League 的评分历史,用户也可以通过图表看到。\n\n我们有一个添加回放分析和锦标赛历史记录的计划,所以随时关注!\n\n服务没有与TETR.IO与osk以任何身份关联。'; + @override String get appVersion => '版本'; + @override String build({required Object build}) => '${build}'; + @override String get GHrepo => 'GitHub Repository'; + @override String get submitAnIssue => '提交问题'; + @override String get credits => '鸣谢'; + @override String get authorAndDeveloper => '作者 & 开发者'; + @override String get providedFormulas => '提供的公式'; + @override String get providedS1history => '提供的 S1 历史'; + @override String get inoue => 'Inoue (回放抓取器)'; + @override String get zhCNlocale => '简中翻译员'; + @override String get supportHim => '为他提供支持!'; +} + +// Path: stats +class _StringsStatsZhCn implements _StringsStatsEn { + _StringsStatsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get registrationDate => '注册时间'; + @override String get gametime => '游玩时长'; + @override String get ogp => '在线游戏次数'; + @override String get ogw => '在线游戏胜利次数'; + @override String get followers => '粉丝'; + @override late final _StringsStatsXpZhCn xp = _StringsStatsXpZhCn._(_root); + @override late final _StringsStatsTrZhCn tr = _StringsStatsTrZhCn._(_root); + @override late final _StringsStatsGlickoZhCn glicko = _StringsStatsGlickoZhCn._(_root); + @override late final _StringsStatsRdZhCn rd = _StringsStatsRdZhCn._(_root); + @override late final _StringsStatsGlixareZhCn glixare = _StringsStatsGlixareZhCn._(_root); + @override late final _StringsStatsS1trZhCn s1tr = _StringsStatsS1trZhCn._(_root); + @override late final _StringsStatsGpZhCn gp = _StringsStatsGpZhCn._(_root); + @override late final _StringsStatsGwZhCn gw = _StringsStatsGwZhCn._(_root); + @override late final _StringsStatsWinrateZhCn winrate = _StringsStatsWinrateZhCn._(_root); + @override late final _StringsStatsApmZhCn apm = _StringsStatsApmZhCn._(_root); + @override late final _StringsStatsPpsZhCn pps = _StringsStatsPpsZhCn._(_root); + @override late final _StringsStatsVsZhCn vs = _StringsStatsVsZhCn._(_root); + @override late final _StringsStatsAppZhCn app = _StringsStatsAppZhCn._(_root); + @override late final _StringsStatsVsapmZhCn vsapm = _StringsStatsVsapmZhCn._(_root); + @override late final _StringsStatsDssZhCn dss = _StringsStatsDssZhCn._(_root); + @override late final _StringsStatsDspZhCn dsp = _StringsStatsDspZhCn._(_root); + @override late final _StringsStatsAppdspZhCn appdsp = _StringsStatsAppdspZhCn._(_root); + @override late final _StringsStatsCheeseZhCn cheese = _StringsStatsCheeseZhCn._(_root); + @override late final _StringsStatsGbeZhCn gbe = _StringsStatsGbeZhCn._(_root); + @override late final _StringsStatsNyaappZhCn nyaapp = _StringsStatsNyaappZhCn._(_root); + @override late final _StringsStatsAreaZhCn area = _StringsStatsAreaZhCn._(_root); + @override late final _StringsStatsEtrZhCn etr = _StringsStatsEtrZhCn._(_root); + @override late final _StringsStatsEtraccZhCn etracc = _StringsStatsEtraccZhCn._(_root); + @override late final _StringsStatsOpenerZhCn opener = _StringsStatsOpenerZhCn._(_root); + @override late final _StringsStatsPlonkZhCn plonk = _StringsStatsPlonkZhCn._(_root); + @override late final _StringsStatsStrideZhCn stride = _StringsStatsStrideZhCn._(_root); + @override late final _StringsStatsInfdsZhCn infds = _StringsStatsInfdsZhCn._(_root); + @override late final _StringsStatsAltitudeZhCn altitude = _StringsStatsAltitudeZhCn._(_root); + @override late final _StringsStatsClimbSpeedZhCn climbSpeed = _StringsStatsClimbSpeedZhCn._(_root); + @override late final _StringsStatsPeakClimbSpeedZhCn peakClimbSpeed = _StringsStatsPeakClimbSpeedZhCn._(_root); + @override late final _StringsStatsKosZhCn kos = _StringsStatsKosZhCn._(_root); + @override late final _StringsStatsB2bZhCn b2b = _StringsStatsB2bZhCn._(_root); + @override late final _StringsStatsFinesseZhCn finesse = _StringsStatsFinesseZhCn._(_root); + @override late final _StringsStatsFinesseFaultsZhCn finesseFaults = _StringsStatsFinesseFaultsZhCn._(_root); + @override late final _StringsStatsTotalTimeZhCn totalTime = _StringsStatsTotalTimeZhCn._(_root); + @override late final _StringsStatsLevelZhCn level = _StringsStatsLevelZhCn._(_root); + @override late final _StringsStatsPiecesZhCn pieces = _StringsStatsPiecesZhCn._(_root); + @override late final _StringsStatsSppZhCn spp = _StringsStatsSppZhCn._(_root); + @override late final _StringsStatsKpZhCn kp = _StringsStatsKpZhCn._(_root); + @override late final _StringsStatsKppZhCn kpp = _StringsStatsKppZhCn._(_root); + @override late final _StringsStatsKpsZhCn kps = _StringsStatsKpsZhCn._(_root); + @override String blitzScore({required Object p}) => '${p} 分'; + @override String levelUpRequirement({required Object p}) => '还需 ${p} 升到下一级'; + @override String get piecesTotal => '放块总数'; + @override String get piecesWithPerfectFinesse => '极简块数'; + @override String get score => '分数'; + @override String get lines => '行数'; + @override String get linesShort => '行'; + @override String get pcs => '全消数'; + @override String get holds => '暂存数'; + @override String get spike => '最高暴击'; + @override String top({required Object percentage}) => '前 ${percentage}'; + @override String topRank({required Object rank}) => '最高段位:${rank}'; + @override String get floor => '层'; + @override String get split => '拆分'; + @override String get total => '总计'; + @override String get sent => '已发送'; + @override String get received => '已接收'; + @override String get placement => '排名'; + @override String get peak => '最高'; + @override String qpWithMods({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + one: '使用 1 个模组', + two: '使用 ${n} 个模组', + few: '使用 ${n} 个模组', + many: '使用 ${n} 个模组', + other: '使用 ${n} 个模组', + ); + @override String inputs({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 按键', + one: '${n} 按键', + two: '${n} 按键', + few: '${n} 按键', + many: '${n} 按键', + other: '${n} 按键', + ); + @override String tspinsTotal({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '总共 ${n} 次T旋', + one: '总共 ${n} 次T旋', + two: '总共 ${n} 次T旋', + few: '总共 ${n} 次T旋', + many: '总共 ${n} 次T旋', + other: '总共 ${n} 次T旋', + ); + @override String linesCleared({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '总共消除 ${n} 行', + one: '总共消除 ${n} 行', + two: '总共消除 ${n} 行', + few: '总共消除 ${n} 行', + many: '总共消除 ${n} 行', + other: '总共消除 ${n} 行', + ); + @override late final _StringsStatsGraphsZhCn graphs = _StringsStatsGraphsZhCn._(_root); + @override String players({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 名玩家', + one: '${n} 名玩家', + two: '${n} 名玩家', + few: '${n} 名玩家', + many: '${n} 名玩家', + other: '${n} 名玩家', + ); + @override String games({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 次游戏', + one: '${n} 次游戏', + two: '${n} 次游戏', + few: '${n} 次游戏', + many: '${n} 次游戏', + other: '${n} 次游戏', + ); + @override late final _StringsStatsLineClearZhCn lineClear = _StringsStatsLineClearZhCn._(_root); + @override late final _StringsStatsLineClearsZhCn lineClears = _StringsStatsLineClearsZhCn._(_root); + @override String get mini => 'Mini'; + @override String get tSpin => 'T-spin'; + @override String get tSpins => 'T-spins'; + @override String get spin => 'Spin'; + @override String get spins => 'Spins'; +} + +// Path: stats.xp +class _StringsStatsXpZhCn implements _StringsStatsXpEn { + _StringsStatsXpZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '经验值'; + @override String get full => '经验点'; +} + +// Path: stats.tr +class _StringsStatsTrZhCn implements _StringsStatsTrEn { + _StringsStatsTrZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'TR'; + @override String get full => 'Tetra 评分'; +} + +// Path: stats.glicko +class _StringsStatsGlickoZhCn implements _StringsStatsGlickoEn { + _StringsStatsGlickoZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'Glicko'; + @override String get full => 'Glicko'; +} + +// Path: stats.rd +class _StringsStatsRdZhCn implements _StringsStatsRdEn { + _StringsStatsRdZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'RD'; + @override String get full => '评分偏差'; +} + +// Path: stats.glixare +class _StringsStatsGlixareZhCn implements _StringsStatsGlixareEn { + _StringsStatsGlixareZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'GXE'; + @override String get full => 'GLIXARE'; +} + +// Path: stats.s1tr +class _StringsStatsS1trZhCn implements _StringsStatsS1trEn { + _StringsStatsS1trZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'S1 TR'; + @override String get full => '第 1 赛季式 TR'; +} + +// Path: stats.gp +class _StringsStatsGpZhCn implements _StringsStatsGpEn { + _StringsStatsGpZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'GP'; + @override String get full => '总场数'; +} + +// Path: stats.gw +class _StringsStatsGwZhCn implements _StringsStatsGwEn { + _StringsStatsGwZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'GW'; + @override String get full => '胜场数'; +} + +// Path: stats.winrate +class _StringsStatsWinrateZhCn implements _StringsStatsWinrateEn { + _StringsStatsWinrateZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'WR%'; + @override String get full => '胜率'; +} + +// Path: stats.apm +class _StringsStatsApmZhCn implements _StringsStatsApmEn { + _StringsStatsApmZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'APM'; + @override String get full => '每分钟攻击数'; +} + +// Path: stats.pps +class _StringsStatsPpsZhCn implements _StringsStatsPpsEn { + _StringsStatsPpsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'PPS'; + @override String get full => '每秒块数'; +} + +// Path: stats.vs +class _StringsStatsVsZhCn implements _StringsStatsVsEn { + _StringsStatsVsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'VS'; + @override String get full => 'VS 分数'; +} + +// Path: stats.app +class _StringsStatsAppZhCn implements _StringsStatsAppEn { + _StringsStatsAppZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'APP'; + @override String get full => '每块攻击数'; +} + +// Path: stats.vsapm +class _StringsStatsVsapmZhCn implements _StringsStatsVsapmEn { + _StringsStatsVsapmZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'VS/APM'; + @override String get full => 'VS / APM'; +} + +// Path: stats.dss +class _StringsStatsDssZhCn implements _StringsStatsDssEn { + _StringsStatsDssZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'DS/S'; + @override String get full => '每秒挖掘数'; +} + +// Path: stats.dsp +class _StringsStatsDspZhCn implements _StringsStatsDspEn { + _StringsStatsDspZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'DS/P'; + @override String get full => '每块挖掘数'; +} + +// Path: stats.appdsp +class _StringsStatsAppdspZhCn implements _StringsStatsAppdspEn { + _StringsStatsAppdspZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'APP+DSP'; + @override String get full => 'APP + DSP'; +} + +// Path: stats.cheese +class _StringsStatsCheeseZhCn implements _StringsStatsCheeseEn { + _StringsStatsCheeseZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'CI'; + @override String get full => '垃圾行混乱指数'; +} + +// Path: stats.gbe +class _StringsStatsGbeZhCn implements _StringsStatsGbeEn { + _StringsStatsGbeZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'GbE'; + @override String get full => '垃圾行效率'; +} + +// Path: stats.nyaapp +class _StringsStatsNyaappZhCn implements _StringsStatsNyaappEn { + _StringsStatsNyaappZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'wAPP'; + @override String get full => '加权APP'; +} + +// Path: stats.area +class _StringsStatsAreaZhCn implements _StringsStatsAreaEn { + _StringsStatsAreaZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '面积'; + @override String get full => '面积'; +} + +// Path: stats.etr +class _StringsStatsEtrZhCn implements _StringsStatsEtrEn { + _StringsStatsEtrZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'eTR'; + @override String get full => '预测 TR'; +} + +// Path: stats.etracc +class _StringsStatsEtraccZhCn implements _StringsStatsEtraccEn { + _StringsStatsEtraccZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '±eTR'; + @override String get full => '预测实际差量'; +} + +// Path: stats.opener +class _StringsStatsOpenerZhCn implements _StringsStatsOpenerEn { + _StringsStatsOpenerZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '定式'; + @override String get full => '定式'; +} + +// Path: stats.plonk +class _StringsStatsPlonkZhCn implements _StringsStatsPlonkEn { + _StringsStatsPlonkZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '太极'; + @override String get full => '太极'; +} + +// Path: stats.stride +class _StringsStatsStrideZhCn implements _StringsStatsStrideEn { + _StringsStatsStrideZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '速度'; + @override String get full => '速度'; +} + +// Path: stats.infds +class _StringsStatsInfdsZhCn implements _StringsStatsInfdsEn { + _StringsStatsInfdsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '挖掘'; + @override String get full => '挖掘'; +} + +// Path: stats.altitude +class _StringsStatsAltitudeZhCn implements _StringsStatsAltitudeEn { + _StringsStatsAltitudeZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'm'; + @override String get full => '高度'; +} + +// Path: stats.climbSpeed +class _StringsStatsClimbSpeedZhCn implements _StringsStatsClimbSpeedEn { + _StringsStatsClimbSpeedZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'CSP'; + @override String get full => '爬行速度'; + @override String get gaugetTitle => '爬行速度'; +} + +// Path: stats.peakClimbSpeed +class _StringsStatsPeakClimbSpeedZhCn implements _StringsStatsPeakClimbSpeedEn { + _StringsStatsPeakClimbSpeedZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '最高CSP'; + @override String get full => '最高爬行速度'; + @override String get gaugetTitle => '最高'; +} + +// Path: stats.kos +class _StringsStatsKosZhCn implements _StringsStatsKosEn { + _StringsStatsKosZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'KO\'s'; + @override String get full => '击杀'; +} + +// Path: stats.b2b +class _StringsStatsB2bZhCn implements _StringsStatsB2bEn { + _StringsStatsB2bZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'B2B'; + @override String get full => '背靠背/满贯'; +} + +// Path: stats.finesse +class _StringsStatsFinesseZhCn implements _StringsStatsFinesseEn { + _StringsStatsFinesseZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '极'; + @override String get full => '极简率'; + @override String get widgetTitle => '简率'; +} + +// Path: stats.finesseFaults +class _StringsStatsFinesseFaultsZhCn implements _StringsStatsFinesseFaultsEn { + _StringsStatsFinesseFaultsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '非极简'; + @override String get full => '非极简操作数'; +} + +// Path: stats.totalTime +class _StringsStatsTotalTimeZhCn implements _StringsStatsTotalTimeEn { + _StringsStatsTotalTimeZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => '时长'; + @override String get full => '总时长'; + @override String get widgetTitle => '总时长'; +} + +// Path: stats.level +class _StringsStatsLevelZhCn implements _StringsStatsLevelEn { + _StringsStatsLevelZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'Lvl'; + @override String get full => '等级'; +} + +// Path: stats.pieces +class _StringsStatsPiecesZhCn implements _StringsStatsPiecesEn { + _StringsStatsPiecesZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'P'; + @override String get full => '块'; +} + +// Path: stats.spp +class _StringsStatsSppZhCn implements _StringsStatsSppEn { + _StringsStatsSppZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'SPP'; + @override String get full => '每块得分'; +} + +// Path: stats.kp +class _StringsStatsKpZhCn implements _StringsStatsKpEn { + _StringsStatsKpZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'KP'; + @override String get full => '按键'; +} + +// Path: stats.kpp +class _StringsStatsKppZhCn implements _StringsStatsKppEn { + _StringsStatsKppZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'KPP'; + @override String get full => '每块按键数'; +} + +// Path: stats.kps +class _StringsStatsKpsZhCn implements _StringsStatsKpsEn { + _StringsStatsKpsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get short => 'KPS'; + @override String get full => '每秒按键数'; +} + +// Path: stats.graphs +class _StringsStatsGraphsZhCn implements _StringsStatsGraphsEn { + _StringsStatsGraphsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get attack => '攻击'; + @override String get speed => '速度'; + @override String get defense => '防御'; + @override String get cheese => '奶酪层'; +} + +// Path: stats.lineClear +class _StringsStatsLineClearZhCn implements _StringsStatsLineClearEn { + _StringsStatsLineClearZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get single => 'Single'; + @override String get double => 'Double'; + @override String get triple => 'Triple'; + @override String get quad => 'Quad'; + @override String get penta => 'Penta'; + @override String get hexa => 'Hexa'; + @override String get hepta => 'Hepta'; + @override String get octa => 'Octa'; + @override String get ennea => 'Ennea'; + @override String get deca => 'Deca'; + @override String get hendeca => 'Hendeca'; + @override String get dodeca => 'Dodeca'; + @override String get triadeca => 'Triadeca'; + @override String get tessaradeca => 'Tessaradeca'; + @override String get pentedeca => 'Pentedeca'; + @override String get hexadeca => 'Hexadeca'; + @override String get heptadeca => 'Heptadeca'; + @override String get octadeca => 'Octadeca'; + @override String get enneadeca => 'Enneadeca'; + @override String get eicosa => 'Eicosa'; + @override String get kagaris => 'Kagaris'; +} + +// Path: stats.lineClears +class _StringsStatsLineClearsZhCn implements _StringsStatsLineClearsEn { + _StringsStatsLineClearsZhCn._(this._root); + + @override final _StringsZhCn _root; // ignore: unused_field + + // Translations + @override String get zero => 'Zeros'; + @override String get single => 'Singles'; + @override String get double => 'Doubles'; + @override String get triple => 'Triples'; + @override String get quad => 'Quads'; + @override String get penta => 'Pentas'; } /// Flat map(s) containing all translations. @@ -1553,302 +4751,23 @@ extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { case 'locales.en': return 'English'; - case 'locales.ru': return 'Russian (Русский)'; - case 'tetraLeague': return 'Tetra League'; - case 'tlRecords': return 'TL Records'; - case 'history': return 'History'; - case 'sprint': return '40 Lines'; - case 'blitz': return 'Blitz'; - case 'recent': return 'Recent'; - case 'recentRuns': return 'Recent runs'; - case 'blitzScore': return ({required Object p}) => '${p} points'; - case 'openSPreplay': return 'Open replay in TETR.IO'; - case 'downloadSPreplay': return 'Download replay'; - case 'other': return 'Other'; - case 'distinguishment': return 'Distinguishment'; - case 'zen': return 'Zen'; - case 'bio': return 'Bio'; - case 'news': return 'News'; - case 'newsParts.leaderboardStart': return 'Got '; - case 'newsParts.leaderboardMiddle': return 'on '; - case 'newsParts.personalbest': return 'Got a new PB in '; - case 'newsParts.personalbestMiddle': return 'of '; - case 'newsParts.badgeStart': return 'Obtained a '; - case 'newsParts.badgeEnd': return 'badge'; - case 'newsParts.rankupStart': return 'Obtained '; - case 'newsParts.rankupMiddle': return ({required Object r}) => '${r} rank '; - case 'newsParts.rankupEnd': return 'in Tetra League'; - case 'newsParts.tetoSupporter': return 'TETR.IO supporter'; - case 'newsParts.supporterStart': return 'Become a '; - case 'newsParts.supporterGiftStart': return 'Received the gift of '; - case 'newsParts.unknownNews': return ({required Object type}) => 'Unknown news of type ${type}'; - case 'openSearch': return 'Search player'; - case 'closeSearch': return 'Close search'; - case 'searchHint': return 'Nickname, ID or Discord userID (with "ds:" prefix)'; - case 'refresh': return 'Refresh'; - case 'fetchAndsaveTLHistory': return 'Get player history'; - case 'fetchAndSaveOldTLmatches': return 'Get Tetra League matches history'; - case 'fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} states was found'; - case 'fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} matches was found'; - case 'showStoredData': return 'Show stored data'; - case 'statsCalc': return 'Stats Calculator'; - case 'settings': return 'Settings'; - case 'track': return 'Track'; - case 'stopTracking': return 'Stop\ntracking'; - case 'becameTracked': return 'Added to tracking list!'; - case 'compare': return 'Compare'; - case 'stoppedBeingTracked': return 'Removed from tracking list!'; - case 'tlLeaderboard': return 'Tetra League leaderboard'; - case 'noRecords': return 'No records'; - case 'noOldRecords': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: 'No records', - one: 'Only ${n} record', - two: 'Only ${n} records', - few: 'Only ${n} records', - many: 'Only ${n} records', - other: 'Only ${n} records', - ); - case 'noRecord': return 'No record'; - case 'botRecord': return 'Bots are not allowed to set records'; - case 'anonRecord': return 'Guests are not allowed to set records'; - case 'notEnoughData': return 'Not enough data'; - case 'noHistorySaved': return 'No history saved'; - case 'pseudoTooltipHeaderInit': return 'Hover over point'; - case 'pseudoTooltipFooterInit': return 'to see detailed data'; - case 'obtainDate': return ({required Object date}) => 'Obtained ${date}'; - case 'fetchDate': return ({required Object date}) => 'Fetched ${date}'; - case 'exactGametime': return 'Exact gametime'; - case 'bigRedBanned': return 'BANNED'; - case 'normalBanned': return 'Banned'; - case 'bigRedBadStanding': return 'BAD STANDING'; - case 'copiedToClipboard': return 'Copied to clipboard!'; - case 'playerRoleAccount': return ' account '; - case 'wasFromBeginning': return 'that was from very beginning'; - case 'created': return 'created'; - case 'botCreatedBy': return 'by'; - case 'notSupporter': return 'Not a supporter'; - case 'assignedManualy': return 'That badge was assigned manualy by TETR.IO admins'; - case 'supporter': return ({required Object tier}) => 'Supporter tier ${tier}'; - case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; - case 'top': return 'Top'; - case 'topRank': return 'Top rank'; - case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} than ${rank} rank average'; - case 'verdictBetter': return 'better'; - case 'verdictWorse': return 'worse'; - case 'smooth': return 'Smooth'; - case 'postSeason': return 'Off-season'; - case 'seasonStarts': return 'Season starts in:'; - case 'nanow': return 'Not avaliable for now...'; - case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}'; - case 'seasonEnded': return 'Season has ended'; - case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; - case 'numOfVictories': return ({required Object wins}) => '~${wins} victories'; - case 'promotionOnNextWin': return 'Promotion on next win'; - case 'numOfdefeats': return ({required Object losses}) => '~${losses} defeats'; - case 'demotionOnNextLoss': return 'Demotion on next loss'; - case 'nerdStats': return 'Nerd Stats'; - case 'playersYouTrack': return 'Players you track'; - case 'formula': return 'Formula'; - case 'exactValue': return 'Exact value'; - case 'neverPlayedTL': return 'That user never played Tetra League'; - case 'botTL': return 'Bots are not allowed to play Tetra League'; - case 'anonTL': return 'Guests are not allowed to play Tetra League'; - case 'quickPlay': return 'Quick Play'; - case 'expert': return 'Expert'; - case 'withMods': return 'With mods'; - case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: 'with ${n} mods', - one: 'with ${n} mod', - two: 'with ${n} mods', - few: 'with ${n} mods', - many: 'with ${n} mods', - other: 'with ${n} mods', - ); - case 'exportDB': return 'Export local database'; - case 'exportDBDescription': return 'It contains states and Tetra League records of the tracked players and list of tracked players.'; - case 'desktopExportAlertTitle': return 'Desktop export'; - case 'desktopExportText': return 'It seems like you using this app on desktop. Check your documents folder, you should find "TetraStats.db". Copy it somewhere'; - case 'androidExportAlertTitle': return 'Android export'; - case 'androidExportText': return ({required Object exportedDB}) => 'Exported.\n${exportedDB}'; - case 'importDB': return 'Import local database'; - case 'importDBDescription': return 'Restore your backup. Notice that already stored database will be overwritten.'; - case 'importWrongFileType': return 'Wrong file type'; - case 'importCancelled': return 'Operation was cancelled'; - case 'importSuccess': return 'Import successful'; - case 'yourID': return 'Your TETR.IO account'; - case 'yourIDAlertTitle': return 'Your nickname in TETR.IO'; - case 'yourIDText': return 'When app loads, it will retrieve data for this account'; - case 'language': return 'Language'; - case 'updateInBackground': return 'Update stats in the background'; - case 'updateInBackgroundDescription': return 'While Tetra Stats is running, it can update stats of the current player when cache expires'; - case 'customization': return 'Customization'; - case 'customizationDescription': return 'Change appearance of different things in Tetra Stats UI'; - case 'oskKagari': return 'Osk Kagari gimmick'; - case 'oskKagariDescription': return 'If on, osk\'s rank on main view will be rendered as :kagari:'; - case 'AccentColor': return 'Accent color'; - case 'AccentColorDescription': return 'Almost all interactive UI elements highlighted with this color'; - case 'timestamps': return 'Timestamps'; - case 'timestampsDescription': return 'You can choose, in which way timestamps shows time'; - case 'timestampsAbsoluteGMT': return 'Absolute (GMT)'; - case 'timestampsAbsoluteLocalTime': return 'Absolute (Your timezone)'; - case 'timestampsRelative': return 'Relative'; - case 'rating': return 'Main representation of rating'; - case 'ratingDescription': return 'TR is not linear, while Glicko does not have boundaries and percentile is volatile'; - case 'ratingLBposition': return 'LB position'; - case 'sheetbotGraphs': return 'Sheetbot-like behavior for radar graphs'; - case 'sheetbotGraphsDescription': return 'If on, points on the graphs can appear on the opposite half of the graph if value is negative'; - case 'lbStats': return 'Show leaderboard based stats'; - case 'lbStatsDescription': return 'That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values'; - case 'aboutApp': return 'About app'; - case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy'; - case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} account on ${date}'; - case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account'; - case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches'; - case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; - case 'stateRemoved': return ({required Object date}) => '${date} state was removed from database!'; - case 'matchRemoved': return ({required Object date}) => '${date} match was removed from database!'; - case 'viewAllMatches': return 'View all matches'; - case 'trackedPlayersViewTitle': return 'Stored data'; - case 'trackedPlayersZeroEntrys': return 'Empty list. Press "Track" button in previous view to add current player here'; - case 'trackedPlayersOneEntry': return 'There is only one player'; - case 'trackedPlayersManyEntrys': return ({required Object numberOfPlayers}) => 'There are ${numberOfPlayers} players'; - case 'trackedPlayersEntry': return ({required Object nickname, required Object numberOfStates}) => '${nickname}: ${numberOfStates} states'; - case 'trackedPlayersDescription': return ({required Object firstStateDate, required Object lastStateDate}) => 'From ${firstStateDate} until ${lastStateDate}'; - case 'trackedPlayersStatesDeleted': return ({required Object nickname}) => '${nickname} states was removed from database!'; - case 'duplicatedFix': return 'Remove duplicated TL mathces'; - case 'compressDB': return 'Compress DB'; - case 'SpaceSaved': return ({required Object size}) => 'Space saved: ${size}'; - case 'averageXrank': return ({required Object rankLetter}) => 'Average ${rankLetter} rank'; - case 'vs': return 'vs'; - case 'inTLmatch': return 'in TL match'; - case 'downloadReplay': return 'Download .ttrm replay'; - case 'openReplay': return 'Open replay in TETR.IO'; - case 'replaySaved': return ({required Object path}) => 'Replay saved to ${path}'; - case 'match': return 'Match'; - case 'timeWeightedmatch': return 'Match (time-weighted)'; - case 'roundNumber': return ({required Object n}) => 'Round ${n}'; - case 'statsFor': return 'Stats for'; - case 'numberOfRounds': return 'Number of rounds'; - case 'matchLength': return 'Match Length'; - case 'roundLength': return 'Round Length'; - case 'matchStats': return 'Match stats'; - case 'timeWeightedmatchStats': return 'Time-weighted match stats'; - case 'replayIssue': return 'Can\'t process replay'; - case 'matchIsTooOld': return 'Replay is not available'; - case 'winner': return 'Winner'; - case 'registred': return 'Registred'; - case 'playedTL': return 'Played Tetra League'; - case 'winChance': return 'Win Chance'; - case 'byGlicko': return 'By Glicko'; - case 'byEstTR': return 'By Est. TR'; - case 'compareViewNoValues': return ({required Object avgR}) => 'Please, enter username, user ID, APM-PPS-VS values (divider doesn\'t matter, only order matter) or ${avgR} (where R is rank) to both fields'; - case 'compareViewWrongValue': return ({required Object value}) => 'Falied to assign ${value}'; - case 'mostRecentOne': return 'Most recent one'; - case 'yes': return 'Yes'; - case 'no': return 'No'; - case 'daysLater': return 'days later'; - case 'dayseBefore': return 'days before'; - case 'fromBeginning': return 'From beginning'; - case 'calc': return 'Calc'; - case 'calcViewNoValues': return 'Enter values to calculate the stats'; - case 'rankAveragesViewTitle': return 'Ranks cutoffs'; - case 'sprintAndBlitsViewTitle': return '40 lines and Blitz averages'; - case 'sprintAndBlitsRelevance': return ({required Object date}) => 'Relevance: ${date}'; - case 'rank': return 'Rank'; - case 'averages': return 'Averages'; - case 'lbViewZeroEntrys': return 'Empty list'; - case 'lbViewOneEntry': return 'There is only one player'; - case 'lbViewManyEntrys': return ({required Object numberOfPlayers}) => 'There are ${numberOfPlayers}'; - case 'everyoneAverages': return 'Values for leaderboard'; - case 'sortBy': return 'Sort by'; - case 'reversed': return 'Reversed'; - case 'country': return 'Country'; - case 'rankAverages': return ({required Object rank}) => 'Values for ${rank} rank'; - case 'players': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} players', - one: '${n} player', - two: '${n} players', - few: '${n} players', - many: '${n} players', - other: '${n} players', - ); - case 'games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} games', - one: '${n} game', - two: '${n} games', - few: '${n} games', - many: '${n} games', - other: '${n} games', - ); - case 'gamesPlayed': return ({required Object games}) => '${games} played'; - case 'chart': return 'Chart'; - case 'entries': return 'Entries'; - case 'minimums': return 'Minimums'; - case 'maximums': return 'Maximums'; - case 'lowestValues': return 'Lowest Values'; - case 'averageValues': return 'Average Values'; - case 'highestValues': return 'Highest Values'; - case 'forPlayer': return ({required Object username}) => 'for player ${username}'; - case 'currentAxis': return ({required Object axis}) => '${axis} axis:'; - case 'p1nkl0bst3rAlert': return 'That data was retrived from third party API maintained by p1nkl0bst3r'; - case 'notForWeb': return 'Function is not available for web version'; - case 'graphs.attack': return 'Attack'; - case 'graphs.speed': return 'Speed'; - case 'graphs.defense': return 'Defense'; - case 'graphs.cheese': return 'Cheese'; - case 'statCellNum.xpLevel': return 'XP Level'; - case 'statCellNum.xpProgress': return 'Progress to next level'; - case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Progress from 0 XP to level ${n}'; - case 'statCellNum.xpLeft': return 'XP left'; - case 'statCellNum.hoursPlayed': return 'Hours\nPlayed'; - case 'statCellNum.onlineGames': return 'Online\nGames'; - case 'statCellNum.gamesWon': return 'Games\nWon'; - case 'statCellNum.totalGames': return 'Total Games Played'; - case 'statCellNum.totalWon': return 'Total Games Won'; - case 'statCellNum.friends': return 'Friends'; - case 'statCellNum.apm': return 'Attack\nPer Minute'; - case 'statCellNum.vs': return 'Versus\nScore'; - case 'statCellNum.recordLB': return 'Leaderboard placement'; - case 'statCellNum.lbp': return 'Leaderboard\nplacement'; - case 'statCellNum.lbpShort': return '№ in LB'; - case 'statCellNum.lbpc': return 'Country LB\nplacement'; - case 'statCellNum.lbpcShort': return '№ in local LB'; - case 'statCellNum.gamesPlayed': return 'Games\nplayed'; - case 'statCellNum.gamesWonTL': return 'Games\nWon'; - case 'statCellNum.winrate': return 'Winrate'; - case 'statCellNum.level': return 'Level'; - case 'statCellNum.score': return 'Score'; - case 'statCellNum.spp': return 'Score\nPer Piece'; - case 'statCellNum.pieces': return 'Pieces\nPlaced'; - case 'statCellNum.pps': return 'Pieces\nPer Second'; - case 'statCellNum.finesseFaults': return 'Finesse\nFaults'; - case 'statCellNum.finessePercentage': return 'Finesse\nPercentage'; - case 'statCellNum.keys': return 'Key\nPresses'; - case 'statCellNum.kpp': return 'KP Per\nPiece'; - case 'statCellNum.kps': return 'KP Per\nSecond'; - case 'statCellNum.tr': return 'Tetra Rating'; - case 'statCellNum.rd': return 'Rating Deviation'; - case 'statCellNum.app': return 'Attack Per Piece'; - case 'statCellNum.appDescription': return '(Abbreviated as APP) Main efficiency metric. Tells how many attack you producing per piece'; - case 'statCellNum.vsapmDescription': return 'Basically, tells how much and how efficient you using garbage in your attacks'; - case 'statCellNum.dss': return 'Downstack\nPer Second'; - case 'statCellNum.dssDescription': return '(Abbreviated as DS/S) Downstack per Second measures how many garbage lines you clear in a second.'; - case 'statCellNum.dsp': return 'Downstack\nPer Piece'; - case 'statCellNum.dspDescription': return '(Abbreviated as DS/P) Downstack per Piece measures how many garbage lines you clear per piece.'; - case 'statCellNum.appdsp': return 'APP + DS/P'; - case 'statCellNum.appdspDescription': return 'Just a sum of Attack per Piece and Downstack per Piece.'; - case 'statCellNum.cheese': return 'Cheese\nIndex'; - case 'statCellNum.cheeseDescription': return '(Abbreviated as Cheese) Cheese Index is an approximation how much clean / cheese garbage player sends. Lower = more clean. Higher = more cheese.\nInvented by kerrmunism'; - case 'statCellNum.gbe': return 'Garbage\nEfficiency'; - case 'statCellNum.gbeDescription': return '(Abbreviated as Gb Eff.) Garbage Efficiency measures how well player uses their garbage. Higher = better or they use their garbage more. Lower = they mostly send their garbage back at cheese or rarely clear garbage.\nInvented by Zepheniah and Dragonboy.'; - case 'statCellNum.nyaapp': return 'Weighted\nAPP'; - case 'statCellNum.nyaappDescription': return '(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.'; - case 'statCellNum.area': return 'Area'; - case 'statCellNum.areaDescription': return 'How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections'; - case 'statCellNum.estOfTR': return 'Estimated TR'; - case 'statCellNum.estOfTRShort': return 'Est. TR'; - case 'statCellNum.accOfEst': return 'Accuracy'; - case 'statCellNum.accOfEstShort': return 'Acc.'; + case 'locales.ru-RU': return 'Russian (Русский)'; + case 'locales.zh-CN': return 'Simplified Chinese (简体中文)'; + case 'gamemodes.league': return 'Tetra League'; + case 'gamemodes.zenith': return 'Quick Play'; + case 'gamemodes.zenithex': return 'Quick Play Expert'; + case 'gamemodes.40l': return '40 Lines'; + case 'gamemodes.blitz': return 'Blitz'; + case 'gamemodes.5mblast': return '5,000,000 Blast'; + case 'gamemodes.zen': return 'Zen'; + case 'destinations.home': return 'Home'; + case 'destinations.graphs': return 'Graphs'; + case 'destinations.leaderboards': return 'Leaderboards'; + case 'destinations.cutoffs': return 'Cutoffs'; + case 'destinations.calc': return 'Calculator'; + case 'destinations.info': return 'Info Center'; + case 'destinations.data': return 'Saved Data'; + case 'destinations.settings': return 'Settings'; case 'playerRole.user': return 'User'; case 'playerRole.banned': return 'Banned'; case 'playerRole.bot': return 'Bot'; @@ -1857,35 +4776,124 @@ extension on Translations { case 'playerRole.mod': return 'Moderator'; case 'playerRole.halfmod': return 'Community moderator'; case 'playerRole.anon': return 'Anonymous'; - case 'numOfGameActions.pc': return 'All Clears'; - case 'numOfGameActions.hold': return 'Holds'; - case 'numOfGameActions.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} key presses', - one: '${n} key press', - two: '${n} key presses', - few: '${n} key presses', - many: '${n} key presses', - other: '${n} key presses', - ); - case 'numOfGameActions.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} T-spins total', - one: '${n} T-spin total', - two: '${n} T-spins total', - few: '${n} T-spins total', - many: '${n} T-spins total', - other: '${n} T-spins total', - ); - case 'numOfGameActions.lineClears': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: '${n} lines cleared', - one: '${n} line cleared', - two: '${n} lines cleared', - few: '${n} lines cleared', - many: '${n} lines cleared', - other: '${n} lines cleared', - ); - case 'popupActions.cancel': return 'Cancel'; - case 'popupActions.submit': return 'Submit'; - case 'popupActions.ok': return 'OK'; + case 'goBackButton': return 'Go Back'; + case 'nanow': return 'Not avaliable for now...'; + case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}'; + case 'seasonEnded': return 'Season has ended'; + case 'overallPB': return ({required Object pb}) => 'Overall PB: ${pb} m'; + case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; + case 'numOfVictories': return ({required Object wins}) => '~${wins} victories'; + case 'promotionOnNextWin': return 'Promotion on next win'; + case 'numOfdefeats': return ({required Object losses}) => '~${losses} defeats'; + case 'demotionOnNextLoss': return 'Demotion on next loss'; + case 'records': return 'Records'; + case 'nerdStats': return 'Nerd Stats'; + case 'playstyles': return 'Playstyles'; + case 'horoscopes': return 'Horoscopes'; + case 'relatedAchievements': return 'Related Achievements'; + case 'season': return 'Season'; + case 'smooth': return 'Smooth'; + case 'dateAndTime': return 'Date & Time'; + case 'TLfullLBnote': return 'Heavy, but allows you to sort players by their stats and filter them by ranks'; + case 'rank': return 'Rank'; + case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => '${n} ${verdict} of ${rank} rank avg'; + case 'verdictBetter': return 'ahead'; + case 'verdictWorse': return 'behind'; + case 'localStanding': return 'local'; + case 'xp.title': return 'XP Level'; + case 'xp.progressToNextLevel': return ({required Object percentage}) => 'Progress to next level: ${percentage}'; + case 'xp.progressTowardsGoal': return ({required Object goal, required Object percentage, required Object left}) => 'Progress from 0 XP to level ${goal}: ${percentage} (${left} XP left)'; + case 'gametime.title': return 'Exact gametime'; + case 'gametime.gametimeAday': return ({required Object gametime}) => '${gametime} a day in average'; + case 'gametime.breakdown': return ({required Object years, required Object months, required Object days, required Object minutes, required Object seconds}) => 'It\'s ${years} years,\nor ${months} months,\nor ${days} days,\nor ${minutes} minutes\nor ${seconds} seconds'; + case 'track': return 'Track'; + case 'stopTracking': return 'Stop tracking'; + case 'supporter': return ({required Object tier}) => 'Supporter tier ${tier}'; + case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Data from ${newDate} comparing with ${oldDate}'; + case 'compare': return 'Compare'; + case 'comparison': return 'Comparison'; + case 'enterUsername': return 'Enter username or \$avgX (where X is rank)'; + case 'general': return 'General'; + case 'badges': return 'Badges'; + case 'obtainDate': return ({required Object date}) => 'Obtained ${date}'; + case 'assignedManualy': return 'That badge was assigned manualy by TETR.IO admins'; + case 'distinguishment': return 'Distinguishment'; + case 'banned': return 'Banned'; + case 'bannedSubtext': return 'Bans are placed when TETR.IO rules or terms of service are broken'; + case 'badStanding': return 'Bad standing'; + case 'badStandingSubtext': return 'One or more recent bans on record'; + case 'botAccount': return 'Bot account'; + case 'botAccountSubtext': return ({required Object botMaintainers}) => 'Operated by ${botMaintainers}'; + case 'copiedToClipboard': return 'Copied to clipboard!'; + case 'bio': return 'Bio'; + case 'news': return 'News'; + case 'matchResult.victory': return 'Victory'; + case 'matchResult.defeat': return 'Defeat'; + case 'matchResult.tie': return 'Tie'; + case 'matchResult.dqvictory': return 'Opponent was DQ\'ed'; + case 'matchResult.dqdefeat': return 'Disqualified'; + case 'matchResult.nocontest': return 'No Contest'; + case 'matchResult.nullified': return 'Nullified'; + case 'distinguishments.noHeader': return 'Header is missing'; + case 'distinguishments.noFooter': return 'Footer is missing'; + case 'distinguishments.twc': return 'TETR.IO World Champion'; + case 'distinguishments.twcYear': return ({required Object year}) => '${year} TETR.IO World Championship'; + case 'newsEntries.leaderboard': return ({required InlineSpan rank, required InlineSpan gametype}) => TextSpan(children: [ + const TextSpan(text: 'Got № '), + rank, + const TextSpan(text: ' in '), + gametype, + ]); + case 'newsEntries.personalbest': return ({required InlineSpan gametype, required InlineSpan pb}) => TextSpan(children: [ + const TextSpan(text: 'Got a new PB in '), + gametype, + const TextSpan(text: ' of '), + pb, + ]); + case 'newsEntries.badge': return ({required InlineSpan badge}) => TextSpan(children: [ + const TextSpan(text: 'Obtained a '), + badge, + const TextSpan(text: ' badge'), + ]); + case 'newsEntries.rankup': return ({required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: 'Obtained '), + rank, + const TextSpan(text: ' in Tetra League'), + ]); + case 'newsEntries.supporter': return ({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Became a '), + s('TETR.IO supporter'), + ]); + case 'newsEntries.supporter_gift': return ({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Received the gift of '), + s('TETR.IO supporter'), + ]); + case 'newsEntries.unknown': return ({required InlineSpan type}) => TextSpan(children: [ + const TextSpan(text: 'Unknown news of type '), + type, + ]); + case 'rankupMiddle': return ({required Object r}) => '${r} rank'; + case 'copyUserID': return 'Click to copy user ID'; + case 'searchHint': return 'Username or ID'; + case 'navMenu': return 'Navigation menu'; + case 'navMenuTooltip': return 'Open navigation menu'; + case 'refresh': return 'Refresh data'; + case 'searchButton': return 'Search'; + case 'trackedPlayers': return 'Tracked Players'; + case 'standing': return 'Standing'; + case 'previousSeasons': return 'Previous Seasons'; + case 'recent': return 'Recent'; + case 'top': return 'Top'; + case 'noRecord': return 'No record'; + case 'sprintAndBlitsRelevance': return ({required Object date}) => 'Relevance: ${date}'; + case 'snackBarMessages.stateRemoved': return ({required Object date}) => '${date} state was removed from database!'; + case 'snackBarMessages.matchRemoved': return ({required Object date}) => '${date} match was removed from database!'; + case 'snackBarMessages.notForWeb': return 'Function is not available for web version'; + case 'snackBarMessages.importSuccess': return 'Import successful'; + case 'snackBarMessages.importCancelled': return 'Import was cancelled'; + case 'errors.noRecords': return 'No records'; + case 'errors.notEnoughData': return 'Not enough data'; + case 'errors.noHistorySaved': return 'No history saved'; case 'errors.connection': return ({required Object code, required Object message}) => 'Some issue with connection: ${code} ${message}'; case 'errors.noSuchUser': return 'No such user'; case 'errors.noSuchUserSub': return 'Either you mistyped something, or the account no longer exists'; @@ -1912,7 +4920,384 @@ extension on Translations { case 'errors.replayAlreadySaved': return 'Replay already saved'; case 'errors.replayExpired': return 'Replay expired and not available anymore'; case 'errors.replayRejected': return 'Third party API blocked your IP address'; - case 'countries.': return 'Not selected'; + case 'actions.cancel': return 'Cancel'; + case 'actions.submit': return 'Submit'; + case 'actions.ok': return 'OK'; + case 'actions.apply': return 'Apply'; + case 'actions.refresh': return 'Refresh'; + case 'graphsDestination.fetchAndsaveTLHistory': return 'Get player history'; + case 'graphsDestination.fetchAndSaveOldTLmatches': return 'Get Tetra League matches history'; + case 'graphsDestination.fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} states was found'; + case 'graphsDestination.fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} matches was found'; + case 'graphsDestination.gamesPlayed': return ({required Object games}) => '${games} played'; + case 'graphsDestination.dateAndTime': return 'Date & Time'; + case 'graphsDestination.filterModaleTitle': return 'Filter ranks on graph'; + case 'filterModale.all': return 'All'; + case 'cutoffsDestination.title': return 'Tetra League State'; + case 'cutoffsDestination.relevance': return ({required Object timestamp}) => 'as of ${timestamp}'; + case 'cutoffsDestination.actual': return 'Actual'; + case 'cutoffsDestination.target': return 'Target'; + case 'cutoffsDestination.cutoffTR': return 'Cutoff TR'; + case 'cutoffsDestination.targetTR': return 'Target TR'; + case 'cutoffsDestination.state': return 'State'; + case 'cutoffsDestination.advanced': return 'Advanced'; + case 'cutoffsDestination.players': return ({required Object n}) => 'Players (${n})'; + case 'cutoffsDestination.moreInfo': return 'More Info'; + case 'cutoffsDestination.NumberOne': return ({required Object tr}) => '№ 1 is ${tr} TR'; + case 'cutoffsDestination.inflated': return ({required Object tr}) => 'Inflated on ${tr} TR'; + case 'cutoffsDestination.notInflated': return 'Not inflated'; + case 'cutoffsDestination.deflated': return ({required Object tr}) => 'Deflated on ${tr} TR'; + case 'cutoffsDestination.notDeflated': return 'Not deflated'; + case 'cutoffsDestination.wellDotDotDot': return 'Well...'; + case 'cutoffsDestination.fromPlace': return ({required Object n}) => 'from № ${n}'; + case 'cutoffsDestination.viewButton': return 'View'; + case 'rankView.rankTitle': return ({required Object rank}) => '${rank} rank data'; + case 'rankView.everyoneTitle': return 'Entire leaderboard'; + case 'rankView.trRange': return 'TR Range'; + case 'rankView.supposedToBe': return 'Supposed to be'; + case 'rankView.gap': return ({required Object value}) => '${value} gap'; + case 'rankView.trGap': return ({required Object value}) => '${value} TR gap'; + case 'rankView.deflationGap': return 'Deflation gap'; + case 'rankView.inflationGap': return 'Inflation gap'; + case 'rankView.LBposRange': return 'LB pos range'; + case 'rankView.overpopulated': return ({required Object players}) => 'Overpopulated by a ${players}'; + case 'rankView.underpopulated': return ({required Object players}) => 'Underpopulated by a ${players}'; + case 'rankView.PlayersEqualSupposedToBe': return 'cute'; + case 'rankView.avgStats': return 'Average Stats'; + case 'rankView.avgForRank': return ({required Object rank}) => 'Average for ${rank} rank'; + case 'rankView.avgNerdStats': return 'Average Nerd Stats'; + case 'rankView.minimums': return 'Minimums'; + case 'rankView.maximums': return 'Maximums'; + case 'stateView.title': return ({required Object date}) => 'State from ${date}'; + case 'tlMatchView.match': return 'Match'; + case 'tlMatchView.vs': return 'vs'; + case 'tlMatchView.winner': return 'Winner'; + case 'tlMatchView.roundNumber': return ({required Object n}) => 'Round ${n}'; + case 'tlMatchView.statsFor': return 'Stats for'; + case 'tlMatchView.numberOfRounds': return 'Number of rounds'; + case 'tlMatchView.matchLength': return 'Match Length'; + case 'tlMatchView.roundLength': return 'Round Length'; + case 'tlMatchView.matchStats': return 'Match stats'; + case 'tlMatchView.downloadReplay': return 'Download .ttrm replay'; + case 'tlMatchView.openReplay': return 'Open replay in TETR.IO'; + case 'calcDestination.placeholders': return ({required Object stat}) => 'Enter your ${stat}'; + case 'calcDestination.tip': return 'Enter values and press "Calc" to see Nerd Stats for them'; + case 'calcDestination.statsCalcButton': return 'Calc'; + case 'calcDestination.damageCalcTip': return 'Click on the actions on the left to add them here'; + case 'calcDestination.actions': return 'Actions'; + case 'calcDestination.results': return 'Results'; + case 'calcDestination.rules': return 'Rules'; + case 'calcDestination.noSpinClears': return 'No Spin Clears'; + case 'calcDestination.spins': return 'Spins'; + case 'calcDestination.miniSpins': return 'Mini spins'; + case 'calcDestination.noLineclear': return 'No lineclear (Break Combo)'; + case 'calcDestination.custom': return 'Custom'; + case 'calcDestination.multiplier': return 'Multiplier'; + case 'calcDestination.pcDamage': return 'Perfect Clear Damage'; + case 'calcDestination.comboTable': return 'Combo Table'; + case 'calcDestination.b2bChaining': return 'Back-To-Back Chaining'; + case 'calcDestination.surgeStartAtB2B': return 'Starts at B2B'; + case 'calcDestination.surgeStartAmount': return 'Start amount'; + case 'calcDestination.totalDamage': return 'Total damage'; + case 'calcDestination.lineclears': return 'Lineclears'; + case 'calcDestination.combo': return 'Combo'; + case 'calcDestination.surge': return 'Surge'; + case 'calcDestination.pcs': return 'PCs'; + case 'infoDestination.title': return 'Information Center'; + case 'infoDestination.sprintAndBlitzAverages': return '40 Lines & Blitz Averages'; + case 'infoDestination.sprintAndBlitzAveragesDescription': return 'Since calculating 40 Lines & Blitz averages is tedious process, it gets updated only once in a while. Click on the title of this card to see the full 40 Lines & Blitz averages table'; + case 'infoDestination.tetraStatsWiki': return 'Tetra Stats Wiki'; + case 'infoDestination.tetraStatsWikiDescription': return 'Find more information about Tetra Stats functions and statictic, that it provides'; + case 'infoDestination.about': return 'About Tetra Stats'; + case 'infoDestination.aboutDescription': return 'Developed by dan63\n'; + case 'leaderboardsDestination.title': return 'Leaderboards'; + case 'leaderboardsDestination.tl': return 'Tetra League (Current Season)'; + case 'leaderboardsDestination.fullTL': return 'Tetra League (Current Season, full one)'; + case 'leaderboardsDestination.ar': return 'Acievement Points'; + case 'savedDataDestination.title': return 'Saved Data'; + case 'savedDataDestination.tip': return 'Select nickname on the left to see data assosiated with it'; + case 'savedDataDestination.seasonTLstates': return ({required Object s}) => 'S${s} TL States'; + case 'savedDataDestination.TLrecords': return 'TL Records'; + case 'settingsDestination.title': return 'Settings'; + case 'settingsDestination.general': return 'General'; + case 'settingsDestination.customization': return 'Customization'; + case 'settingsDestination.database': return 'Local database'; + case 'settingsDestination.checking': return 'Checking...'; + case 'settingsDestination.enterToSubmit': return 'Press Enter to submit'; + case 'settingsDestination.account': return 'Your account in TETR.IO'; + case 'settingsDestination.accountDescription': return 'Stats of that player will be loaded initially right after launching this app. By default it loads my (dan63) stats. To change that, enter your nickname here.'; + case 'settingsDestination.done': return 'Done!'; + case 'settingsDestination.noSuchAccount': return 'No such account'; + case 'settingsDestination.language': return 'Language'; + case 'settingsDestination.languageDescription': return ({required Object languages}) => 'Tetra Stats was translated on ${languages}. By default, app will pick your system one or English, if locale of your system isn\'t avaliable.'; + case 'settingsDestination.languages': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'zero languages', + one: '${n} language', + two: '${n} languages', + few: '${n} languages', + many: '${n} languages', + other: '${n} languages', + ); + case 'settingsDestination.updateInTheBackground': return 'Update data in the background'; + case 'settingsDestination.updateInTheBackgroundDescription': return 'If on, Tetra Stats will attempt to retrieve new info once cache expires. Usually that happen every 5 minutes'; + case 'settingsDestination.compareStats': return 'Compare TL stats with rank averages'; + case 'settingsDestination.compareStatsDescription': return 'If on, Tetra Stats will provide additional metrics, which allow you to compare yourself with average player on your rank. The way you\'ll see it — stats will be highlited with corresponding color, hover over them with cursor for more info.'; + case 'settingsDestination.showPosition': return 'Show position on leaderboard by stats'; + case 'settingsDestination.showPositionDescription': return 'This can take some time (and traffic) to load, but will allow you to see your position on the leaderboard, sorted by a stat'; + case 'settingsDestination.accentColor': return 'Accent color'; + case 'settingsDestination.accentColorDescription': return 'That color is seen across this app and usually highlites interactive UI elements.'; + case 'settingsDestination.accentColorModale': return 'Pick an accent color'; + case 'settingsDestination.timestamps': return 'Timestamps format'; + case 'settingsDestination.timestampsDescriptionPart1': return ({required Object d}) => 'You can choose, in which way timestamps shows time. By default, they show time in GMT timezone, formatted according to chosen locale, example: ${d}.'; + case 'settingsDestination.timestampsDescriptionPart2': return ({required Object y, required Object r}) => 'There is also:\n• Locale formatted in your timezone: ${y}\n• Relative timestamp: ${r}'; + case 'settingsDestination.timestampsAbsoluteGMT': return 'Absolute (GMT)'; + case 'settingsDestination.timestampsAbsoluteLocalTime': return 'Absolute (Your timezone)'; + case 'settingsDestination.timestampsRelative': return 'Relative'; + case 'settingsDestination.sheetbotLikeGraphs': return 'Sheetbot-like behavior for radar graphs'; + case 'settingsDestination.sheetbotLikeGraphsDescription': return 'Altough it was considered by me, that the way graphs work in SheetBot is not very correct, some people were confused to see, that -0.5 stride dosen\'t look the way it looks on SheetBot graph. Hence, he we are: if this toggle is on, points on the graphs can appear on the opposite half of the graph if value is negative.'; + case 'settingsDestination.oskKagariGimmick': return 'Osk-Kagari gimmick'; + case 'settingsDestination.oskKagariGimmickDescription': return 'If on, instead of osk\'s rank, :kagari: will be rendered.'; + case 'settingsDestination.bytesOfDataStored': return 'of data stored'; + case 'settingsDestination.TLrecordsSaved': return 'Tetra League records saved'; + case 'settingsDestination.TLplayerstatesSaved': return 'Tetra League playerstates saved'; + case 'settingsDestination.fixButton': return 'Fix'; + case 'settingsDestination.compressButton': return 'Compress'; + case 'settingsDestination.exportDB': return 'Export local database'; + case 'settingsDestination.desktopExportAlertTitle': return 'Desktop export'; + case 'settingsDestination.desktopExportText': return 'It seems like you using this app on desktop. Check your documents folder, you should find "TetraStats.db". Copy it somewhere'; + case 'settingsDestination.androidExportAlertTitle': return 'Android export'; + case 'settingsDestination.androidExportText': return ({required Object exportedDB}) => 'Exported.\n${exportedDB}'; + case 'settingsDestination.importDB': return 'Import local database'; + case 'settingsDestination.importDBDescription': return 'Restore your backup. Notice that already stored database will be overwritten.'; + case 'settingsDestination.importWrongFileType': return 'Wrong file type'; + case 'homeNavigation.overview': return 'Overview'; + case 'homeNavigation.standing': return 'Standing'; + case 'homeNavigation.seasons': return 'Seasons'; + case 'homeNavigation.mathces': return 'Matches'; + case 'homeNavigation.pb': return 'PB'; + case 'homeNavigation.normal': return 'Normal'; + case 'homeNavigation.expert': return 'Expert'; + case 'homeNavigation.expertRecords': return 'Ex Records'; + case 'graphsNavigation.history': return 'Player History'; + case 'graphsNavigation.league': return 'League State'; + case 'graphsNavigation.cutoffs': return 'Cutoffs History'; + case 'calcNavigation.stats': return 'Stats Calculator'; + case 'calcNavigation.damage': return 'Damage Calculator'; + case 'firstTimeView.welcome': return 'Welcome to Tetra Stats'; + case 'firstTimeView.description': return 'Service, that allows you to keep track of various statistics for TETR.IO'; + case 'firstTimeView.nicknameQuestion': return 'What\'s your nickname?'; + case 'firstTimeView.inpuntHint': return 'Type it here... (3-16 symbols)'; + case 'firstTimeView.emptyInputError': return 'Can\'t submit an empty string'; + case 'firstTimeView.niceToSeeYou': return ({required Object n}) => 'Nice to see you, ${n}'; + case 'firstTimeView.letsTakeALook': return 'Let\'s take a look at your stats...'; + case 'firstTimeView.skip': return 'Skip'; + case 'aboutView.title': return 'About Tetra Stats'; + case 'aboutView.about': return 'Tetra Stats is a service, that works with TETR.IO Tetra Channel API, providing data from it and calculating some addtitional metrics, based on this data. Service allows user to track their progress in Tetra League with "Track" function, which records every Tetra League change into local database (not automatically, you have to visit service from time to time), so these changes could be looked through graphs.\n\nBeanserver blaster is a part of a Tetra Stats, that decoupled into a serverside script. It provides full Tetra League leaderboard, allowing Tetra Stats to sort leaderboard by any metric and build scatter chart, that allows user to analyse Tetra League trends. It also provides history of Tetra League ranks cutoffs, which can be viewed by user via graph as well.\n\nThere is a plans to add replay analysis and tournaments history, so stay tuned!\n\nService is not associated with TETR.IO or osk in any capacity.'; + case 'aboutView.appVersion': return 'App Version'; + case 'aboutView.build': return ({required Object build}) => 'Build ${build}'; + case 'aboutView.GHrepo': return 'GitHub Repository'; + case 'aboutView.submitAnIssue': return 'Submit an issue'; + case 'aboutView.credits': return 'Credits'; + case 'aboutView.authorAndDeveloper': return 'Autor & developer'; + case 'aboutView.providedFormulas': return 'Provided formulas'; + case 'aboutView.providedS1history': return 'Provided S1 history'; + case 'aboutView.inoue': return 'Inoue (replay grabber)'; + case 'aboutView.zhCNlocale': return 'Simplfied Chinese locale'; + case 'aboutView.supportHim': return 'Support him!'; + case 'stats.registrationDate': return 'Registration Date'; + case 'stats.gametime': return 'Time Played'; + case 'stats.ogp': return 'Online Games Played'; + case 'stats.ogw': return 'Online Games Won'; + case 'stats.followers': return 'Followers'; + case 'stats.xp.short': return 'XP'; + case 'stats.xp.full': return 'Experience Points'; + case 'stats.tr.short': return 'TR'; + case 'stats.tr.full': return 'Tetra Rating'; + case 'stats.glicko.short': return 'Glicko'; + case 'stats.glicko.full': return 'Glicko'; + case 'stats.rd.short': return 'RD'; + case 'stats.rd.full': return 'Rating Deviation'; + case 'stats.glixare.short': return 'GXE'; + case 'stats.glixare.full': return 'GLIXARE'; + case 'stats.s1tr.short': return 'S1 TR'; + case 'stats.s1tr.full': return 'Season 1 like TR'; + case 'stats.gp.short': return 'GP'; + case 'stats.gp.full': return 'Games Played'; + case 'stats.gw.short': return 'GW'; + case 'stats.gw.full': return 'Games Won'; + case 'stats.winrate.short': return 'WR%'; + case 'stats.winrate.full': return 'Win Rate'; + case 'stats.apm.short': return 'APM'; + case 'stats.apm.full': return 'Attack Per Minute'; + case 'stats.pps.short': return 'PPS'; + case 'stats.pps.full': return 'Pieces Per Second'; + case 'stats.vs.short': return 'VS'; + case 'stats.vs.full': return 'Versus Score'; + case 'stats.app.short': return 'APP'; + case 'stats.app.full': return 'Attack Per Piece'; + case 'stats.vsapm.short': return 'VS/APM'; + case 'stats.vsapm.full': return 'VS / APM'; + case 'stats.dss.short': return 'DS/S'; + case 'stats.dss.full': return 'Downstack Per Second'; + case 'stats.dsp.short': return 'DS/P'; + case 'stats.dsp.full': return 'Downstack Per Piece'; + case 'stats.appdsp.short': return 'APP+DSP'; + case 'stats.appdsp.full': return 'APP + DSP'; + case 'stats.cheese.short': return 'Cheese'; + case 'stats.cheese.full': return 'Cheese Index'; + case 'stats.gbe.short': return 'GbE'; + case 'stats.gbe.full': return 'Garbage Efficiency'; + case 'stats.nyaapp.short': return 'wAPP'; + case 'stats.nyaapp.full': return 'Weighted APP'; + case 'stats.area.short': return 'Area'; + case 'stats.area.full': return 'Area'; + case 'stats.etr.short': return 'eTR'; + case 'stats.etr.full': return 'Estimated TR'; + case 'stats.etracc.short': return '±eTR'; + case 'stats.etracc.full': return 'Accuracy of Estimated TR'; + case 'stats.opener.short': return 'Opener'; + case 'stats.opener.full': return 'Opener'; + case 'stats.plonk.short': return 'Plonk'; + case 'stats.plonk.full': return 'Plonk'; + case 'stats.stride.short': return 'Stride'; + case 'stats.stride.full': return 'Stride'; + case 'stats.infds.short': return 'Inf. DS'; + case 'stats.infds.full': return 'Infinite Downstack'; + case 'stats.altitude.short': return 'm'; + case 'stats.altitude.full': return 'Altitude'; + case 'stats.climbSpeed.short': return 'CSP'; + case 'stats.climbSpeed.full': return 'Climb Speed'; + case 'stats.climbSpeed.gaugetTitle': return 'Climb\nSpeed'; + case 'stats.peakClimbSpeed.short': return 'Peak CSP'; + case 'stats.peakClimbSpeed.full': return 'Peak Climb Speed'; + case 'stats.peakClimbSpeed.gaugetTitle': return 'Peak'; + case 'stats.kos.short': return 'KO\'s'; + case 'stats.kos.full': return 'Knockouts'; + case 'stats.b2b.short': return 'B2B'; + case 'stats.b2b.full': return 'Back-To-Back'; + case 'stats.finesse.short': return 'F'; + case 'stats.finesse.full': return 'Finesse'; + case 'stats.finesse.widgetTitle': return 'inesse'; + case 'stats.finesseFaults.short': return 'FF'; + case 'stats.finesseFaults.full': return 'Finesse Faults'; + case 'stats.totalTime.short': return 'Time'; + case 'stats.totalTime.full': return 'Total Time'; + case 'stats.totalTime.widgetTitle': return 'otal Time'; + case 'stats.level.short': return 'Lvl'; + case 'stats.level.full': return 'Level'; + case 'stats.pieces.short': return 'P'; + case 'stats.pieces.full': return 'Pieces'; + case 'stats.spp.short': return 'SPP'; + case 'stats.spp.full': return 'Score Per Piece'; + case 'stats.kp.short': return 'KP'; + case 'stats.kp.full': return 'Key presses'; + case 'stats.kpp.short': return 'KPP'; + case 'stats.kpp.full': return 'Key presses Per Piece'; + case 'stats.kps.short': return 'KPS'; + case 'stats.kps.full': return 'Key presses Per Second'; + case 'stats.blitzScore': return ({required Object p}) => '${p} points'; + case 'stats.levelUpRequirement': return ({required Object p}) => 'Level up requirement: ${p}'; + case 'stats.piecesTotal': return 'Total pieces placed'; + case 'stats.piecesWithPerfectFinesse': return 'Placed with perfect finesse'; + case 'stats.score': return 'Score'; + case 'stats.lines': return 'Lines'; + case 'stats.linesShort': return 'L'; + case 'stats.pcs': return 'Perfect Clears'; + case 'stats.holds': return 'Holds'; + case 'stats.spike': return 'Top Spike'; + case 'stats.top': return ({required Object percentage}) => 'Top ${percentage}'; + case 'stats.topRank': return ({required Object rank}) => 'Top rank: ${rank}'; + case 'stats.floor': return 'Floor'; + case 'stats.split': return 'Split'; + case 'stats.total': return 'Total'; + case 'stats.sent': return 'Sent'; + case 'stats.received': return 'Received'; + case 'stats.placement': return 'Placement'; + case 'stats.peak': return 'Peak'; + case 'stats.qpWithMods': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: 'With 1 mod', + two: 'With ${n} mods', + few: 'With ${n} mods', + many: 'With ${n} mods', + other: 'With ${n} mods', + ); + case 'stats.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} key presses', + one: '${n} key press', + two: '${n} key presses', + few: '${n} key presses', + many: '${n} key presses', + other: '${n} key presses', + ); + case 'stats.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} T-spins total', + one: '${n} T-spin total', + two: '${n} T-spins total', + few: '${n} T-spins total', + many: '${n} T-spins total', + other: '${n} T-spins total', + ); + case 'stats.linesCleared': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} lines cleared', + one: '${n} line cleared', + two: '${n} lines cleared', + few: '${n} lines cleared', + many: '${n} lines cleared', + other: '${n} lines cleared', + ); + case 'stats.graphs.attack': return 'Attack'; + case 'stats.graphs.speed': return 'Speed'; + case 'stats.graphs.defense': return 'Defense'; + case 'stats.graphs.cheese': return 'Cheese'; + case 'stats.players': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} players', + one: '${n} player', + two: '${n} players', + few: '${n} players', + many: '${n} players', + other: '${n} players', + ); + case 'stats.games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: '${n} games', + one: '${n} game', + two: '${n} games', + few: '${n} games', + many: '${n} games', + other: '${n} games', + ); + case 'stats.lineClear.single': return 'Single'; + case 'stats.lineClear.double': return 'Double'; + case 'stats.lineClear.triple': return 'Triple'; + case 'stats.lineClear.quad': return 'Quad'; + case 'stats.lineClear.penta': return 'Penta'; + case 'stats.lineClear.hexa': return 'Hexa'; + case 'stats.lineClear.hepta': return 'Hepta'; + case 'stats.lineClear.octa': return 'Octa'; + case 'stats.lineClear.ennea': return 'Ennea'; + case 'stats.lineClear.deca': return 'Deca'; + case 'stats.lineClear.hendeca': return 'Hendeca'; + case 'stats.lineClear.dodeca': return 'Dodeca'; + case 'stats.lineClear.triadeca': return 'Triadeca'; + case 'stats.lineClear.tessaradeca': return 'Tessaradeca'; + case 'stats.lineClear.pentedeca': return 'Pentedeca'; + case 'stats.lineClear.hexadeca': return 'Hexadeca'; + case 'stats.lineClear.heptadeca': return 'Heptadeca'; + case 'stats.lineClear.octadeca': return 'Octadeca'; + case 'stats.lineClear.enneadeca': return 'Enneadeca'; + case 'stats.lineClear.eicosa': return 'Eicosa'; + case 'stats.lineClear.kagaris': return 'Kagaris'; + case 'stats.lineClears.zero': return 'Zeros'; + case 'stats.lineClears.single': return 'Singles'; + case 'stats.lineClears.double': return 'Doubles'; + case 'stats.lineClears.triple': return 'Triples'; + case 'stats.lineClears.quad': return 'Quads'; + case 'stats.lineClears.penta': return 'Pentas'; + case 'stats.mini': return 'Mini'; + case 'stats.tSpin': return 'T-spin'; + case 'stats.tSpins': return 'T-spins'; + case 'stats.spin': return 'Spin'; + case 'stats.spins': return 'Spins'; + case 'countries.': return 'Worldwide'; case 'countries.AF': return 'Afghanistan'; case 'countries.AX': return 'Åland Islands'; case 'countries.AL': return 'Albania'; @@ -2176,306 +5561,27 @@ extension on Translations { } } -extension on _StringsRu { +extension on _StringsRuRu { dynamic _flatMapFunction(String path) { switch (path) { case 'locales.en': return 'Английский (English)'; - case 'locales.ru': return 'Русский'; - case 'tetraLeague': return 'Тетра Лига'; - case 'tlRecords': return 'Матчи ТЛ'; - case 'history': return 'История'; - case 'sprint': return '40 линий'; - case 'blitz': return 'Блиц'; - case 'recent': return 'Недавно'; - case 'recentRuns': return 'Недавние'; - case 'blitzScore': return ({required Object p}) => '${p} очков'; - case 'openSPreplay': return 'Открыть повтор в TETR.IO'; - case 'downloadSPreplay': return 'Скачать повтор'; - case 'other': return 'Другое'; - case 'distinguishment': return 'Заслуга'; - case 'zen': return 'Дзен'; - case 'bio': return 'Биография'; - case 'news': return 'Новости'; - case 'newsParts.leaderboardStart': return 'Взял '; - case 'newsParts.leaderboardMiddle': return 'в таблице '; - case 'newsParts.personalbest': return 'Поставил новый ЛР в '; - case 'newsParts.personalbestMiddle': return 'с результатом в '; - case 'newsParts.badgeStart': return 'Заработал значок '; - case 'newsParts.badgeEnd': return ''; - case 'newsParts.rankupStart': return 'Заработал '; - case 'newsParts.rankupMiddle': return ({required Object r}) => '${r} ранг '; - case 'newsParts.rankupEnd': return 'в Тетра Лиге'; - case 'newsParts.tetoSupporter': return 'TETR.IO supporter'; - case 'newsParts.supporterStart': return 'Стал обладателем '; - case 'newsParts.supporterGiftStart': return 'Получил подарок в виде '; - case 'newsParts.unknownNews': return ({required Object type}) => 'Неизвестная новость типа ${type}'; - case 'openSearch': return 'Искать игрока'; - case 'closeSearch': return 'Закрыть поиск'; - case 'searchHint': return 'Ник, ID или ID в Discord (с префиксом "ds:")'; - case 'refresh': return 'Обновить'; - case 'fetchAndsaveTLHistory': return 'Получить историю игрока'; - case 'fetchAndSaveOldTLmatches': return 'Получить старые матчи Тетра Лиги'; - case 'fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} состояний было найдено'; - case 'fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} старых матчей было найдено'; - case 'showStoredData': return 'Показать сохранённые данные'; - case 'statsCalc': return 'Калькулятор статистики'; - case 'settings': return 'Настройки'; - case 'track': return 'Отслеживать'; - case 'stopTracking': return 'Перестать\nотслеживать'; - case 'becameTracked': return 'Добавлен в список отслеживания!'; - case 'stoppedBeingTracked': return 'Удалён из списка отслеживания!'; - case 'compare': return 'Сравнить'; - case 'tlLeaderboard': return 'Рейтинговая таблица'; - case 'noRecords': return 'Нет записей'; - case 'noOldRecords': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: 'Нет записей', - one: 'Всего один матч', - two: 'Всего ${n} матча', - few: 'Всего ${n} матча', - many: 'Всего ${n} матчей', - other: '${n} матчей', - ); - case 'noRecord': return 'Нет рекорда'; - case 'botRecord': return 'Ботам нельзя ставить рекорды'; - case 'anonRecord': return 'Гостям нельзя ставить рекорды'; - case 'notEnoughData': return 'Недостаточно данных'; - case 'noHistorySaved': return 'Нет сохранённой истории'; - case 'pseudoTooltipHeaderInit': return 'Наведите курсор на точку'; - case 'pseudoTooltipFooterInit': return 'чтобы узнать подробности'; - case 'obtainDate': return ({required Object date}) => 'Получено ${date}'; - case 'fetchDate': return ({required Object date}) => 'На момент ${date}'; - case 'exactGametime': return 'Время, проведённое в игре'; - case 'bigRedBanned': return 'ЗАБАНЕН'; - case 'normalBanned': return 'Забанен'; - case 'bigRedBadStanding': return 'ПЛОХАЯ РЕПУТАЦИЯ'; - case 'copiedToClipboard': return 'Скопировано в буфер обмена!'; - case 'playerRoleAccount': return ', аккаунт которого '; - case 'wasFromBeginning': return 'существовал с самого начала'; - case 'created': return 'создан'; - case 'botCreatedBy': return 'игроком'; - case 'notSupporter': return 'Нет саппортерки'; - case 'supporter': return ({required Object tier}) => 'Саппортерка ${tier} уровня'; - case 'assignedManualy': return 'Этот значок был присвоен вручную администрацией TETR.IO'; - case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; - case 'top': return 'Топ'; - case 'topRank': return 'Топ ранг'; - case 'verdictGeneral': return ({required Object verdict, required Object rank, required Object n}) => '${verdict} среднего ${rank} ранга на ${n}'; - case 'verdictBetter': return 'Лучше'; - case 'verdictWorse': return 'Хуже'; - case 'smooth': return 'Гладкий'; - case 'postSeason': return 'Внесезонье'; - case 'seasonStarts': return 'Сезон начнётся через:'; - case 'nanow': return 'Пока недоступно...'; - case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}'; - case 'seasonEnded': return 'Сезон закончился'; - case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; - case 'numOfVictories': return ({required Object wins}) => '~${wins} побед'; - case 'promotionOnNextWin': return 'Повышение после следующей победы'; - case 'numOfdefeats': return ({required Object losses}) => '~${losses} поражений'; - case 'demotionOnNextLoss': return 'Понижение после следующего поражения'; - case 'nerdStats': return 'Для задротов'; - case 'playersYouTrack': return 'Отслеживаемые игроки'; - case 'formula': return 'Формула'; - case 'exactValue': return 'Точное значение'; - case 'neverPlayedTL': return 'Этот игрок никогда не играл в Тетра Лигу'; - case 'botTL': return 'Ботам нельзя играть в Тетра Лигу'; - case 'anonTL': return 'Гостям нельзя играть в Тетра Лигу'; - case 'quickPlay': return 'Быстрая Игра'; - case 'expert': return 'Эксперт'; - case 'withMods': return 'С модами'; - case 'withModsPlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: 'с ${n} модами', - one: 'с ${n} модом', - two: 'с ${n} модами', - few: 'с ${n} модами', - many: 'с ${n} модами', - other: 'с ${n} модами', - ); - case 'exportDB': return 'Экспортировать локальную базу данных'; - case 'exportDBDescription': return 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; - case 'desktopExportAlertTitle': return 'Экспорт на десктопе'; - case 'desktopExportText': return 'Похоже, вы используете десктопную версию. Проверьте папку "Документы", там вы должны найти файл "TetraStats.db". Скопируйте его куда-нибудь'; - case 'androidExportAlertTitle': return 'Экспорт на Android'; - case 'androidExportText': return ({required Object exportedDB}) => 'Экспортировано.\n${exportedDB}'; - case 'importDB': return 'Импортировать локальную базу данных'; - case 'importDBDescription': return 'Восстановите свою резеврную копию. Обратите внимание, что текущая база данных будет перезаписана.'; - case 'importWrongFileType': return 'Неверный тип файла'; - case 'importCancelled': return 'Операция была отменена'; - case 'importSuccess': return 'Успешно импортировано'; - case 'yourID': return 'Ваш аккаунт в TETR.IO'; - case 'yourIDAlertTitle': return 'Ваш ник в TETR.IO'; - case 'yourIDText': return 'При запуске приложения оно будет получать статистику этого игрока.'; - case 'language': return 'Язык (Language)'; - case 'updateInBackground': return 'Обновлять статистику в фоне'; - case 'updateInBackgroundDescription': return 'Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кеш истекает'; - case 'customization': return 'Кастомизация'; - case 'customizationDescription': return 'Измените внешний вид пользовательского интерфейса Tetra Stats'; - case 'oskKagari': return '"Оск Кагари" прикол'; - case 'oskKagariDescription': return 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:'; - case 'AccentColor': return 'Цветовой акцент'; - case 'AccentColorDescription': return 'Почти все интерактивные элементы пользовательского интерфейса окрашены в этот цвет'; - case 'timestamps': return 'Метки времени'; - case 'timestampsDescription': return 'Вы можете выбрать, каким образом метки времени показывают время'; - case 'timestampsAbsoluteGMT': return 'Абсолютные (GMT)'; - case 'timestampsAbsoluteLocalTime': return 'Абсолютные (Ваш часовой пояс)'; - case 'timestampsRelative': return 'Относительные'; - case 'rating': return 'Основное представление рейтинга'; - case 'ratingDescription': return 'TR нелинеен, тогда как Glicko не имеет границ, а положение в таблице лидеров волатильно'; - case 'ratingLBposition': return 'Позиция в рейтинге'; - case 'sheetbotGraphs': return 'Графики-радары как у sheetBot'; - case 'sheetbotGraphsDescription': return 'Если включено, точки на графике могут появляться на противоположной стороне графика если значение отрицательное'; - case 'lbStats': return 'Показывать статистику, основанную на рейтинговой таблице'; - case 'lbStatsDescription': return 'Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате'; - case 'aboutApp': return 'О приложении'; - case 'aboutAppText': return ({required Object appName, required Object packageName, required Object version, required Object buildNumber}) => '${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy'; - case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}'; - case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}'; - case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${nickname}'; - case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно'; - case 'stateRemoved': return ({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!'; - case 'matchRemoved': return ({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!'; - case 'viewAllMatches': return 'Все матчи'; - case 'trackedPlayersViewTitle': return 'Сохранённые данные'; - case 'trackedPlayersZeroEntrys': return 'Пустой список. Вернитесь на предыдущий экран и нажмите кнопку "Отслеживать", чтобы текущий игрок появился здесь'; - case 'trackedPlayersOneEntry': return 'В списке только один игрок'; - case 'trackedPlayersManyEntrys': return ({required Object numberOfPlayers}) => 'В списке ${numberOfPlayers} игроков'; - case 'trackedPlayersEntry': return ({required Object nickname, required Object numberOfStates}) => '${nickname}: ${numberOfStates} состояний'; - case 'trackedPlayersDescription': return ({required Object firstStateDate, required Object lastStateDate}) => 'Начиная с ${firstStateDate} и заканчивая ${lastStateDate}'; - case 'trackedPlayersStatesDeleted': return ({required Object nickname}) => 'Состояния аккаунта ${nickname} были удалены из локальной базы данных!'; - case 'duplicatedFix': return 'Удалить дубликаты матчей в Тетра Лиге'; - case 'compressDB': return 'Сжать базу данных'; - case 'SpaceSaved': return ({required Object size}) => 'Места освобождено: ${size}'; - case 'averageXrank': return ({required Object rankLetter}) => 'Средний ${rankLetter} ранг'; - case 'vs': return 'против'; - case 'inTLmatch': return 'в матче ТЛ'; - case 'downloadReplay': return 'Скачать .ttrm повтор'; - case 'openReplay': return 'Открыть повтор в TETR.IO'; - case 'replaySaved': return ({required Object path}) => 'Повтор сохранён по пути ${path}'; - case 'match': return 'Матч'; - case 'timeWeightedmatch': return 'Матч (взвешенная по времени)'; - case 'roundNumber': return ({required Object n}) => 'Раунд ${n}'; - case 'statsFor': return 'Статистика за'; - case 'numberOfRounds': return 'Количество раундов'; - case 'matchLength': return 'Продолжительность матча'; - case 'roundLength': return 'Продолжительность раунда'; - case 'matchStats': return 'Статистика матча'; - case 'timeWeightedmatchStats': return 'Взвешенная по времени cтатистика матча'; - case 'replayIssue': return 'Ошибка обработки повтора'; - case 'matchIsTooOld': return 'Информация о повторе недоступна'; - case 'winner': return 'Победитель'; - case 'registred': return 'Зарегистрирован'; - case 'playedTL': return 'Играл в Тетра Лигу'; - case 'winChance': return 'Шансы на победу'; - case 'byGlicko': return 'По Glicko'; - case 'byEstTR': return 'По расч. TR'; - case 'compareViewNoValues': return ({required Object avgR}) => 'Пожалуйста, введите никнейм, ID, APM-PPS-VS (неважно, какой разделитель, важен порядок) или ${avgR} (где R это ранг), в оба поля'; - case 'compareViewWrongValue': return ({required Object value}) => 'Не удалось получить ${value}'; - case 'mostRecentOne': return 'Самый последний'; - case 'yes': return 'Да'; - case 'no': return 'Нет'; - case 'daysLater': return 'дней позже'; - case 'dayseBefore': return 'дней раньше'; - case 'fromBeginning': return 'С начала'; - case 'calc': return 'Считать'; - case 'calcViewNoValues': return 'Введите значения, чтобы посчитать статистику'; - case 'rankAveragesViewTitle': return 'Требования рангов'; - case 'sprintAndBlitsViewTitle': return 'Средние результаты 40 линий и блица'; - case 'sprintAndBlitsRelevance': return ({required Object date}) => 'Актуальность: ${date}'; - case 'rank': return 'Ранг'; - case 'averages': return 'Средние значения'; - case 'lbViewZeroEntrys': return 'Рейтинговая таблица пуста'; - case 'lbViewOneEntry': return 'В рейтинговой таблице всего один игрок'; - case 'lbViewManyEntrys': return ({required Object numberOfPlayers}) => 'В рейтинговой таблице находится ${numberOfPlayers}'; - case 'everyoneAverages': return 'Значения таблицы'; - case 'sortBy': return 'Cортировать по'; - case 'reversed': return 'Наоборот'; - case 'country': return 'Страна'; - case 'rankAverages': return ({required Object rank}) => 'Значения для ${rank} ранга'; - case 'players': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: '${n} игроков', - one: '${n} игрок', - two: '${n} игрока', - few: '${n} игрока', - many: '${n} игроков', - other: '${n} игроков', - ); - case 'games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: '${n} игр', - one: '${n} игра', - two: '${n} игры', - few: '${n} игры', - many: '${n} игр', - other: '${n} игр', - ); - case 'gamesPlayed': return ({required Object games}) => '${games} сыграно'; - case 'chart': return 'График'; - case 'entries': return 'Список'; - case 'minimums': return 'Минимумы'; - case 'maximums': return 'Максимумы'; - case 'lowestValues': return 'Самые низкие показатели'; - case 'averageValues': return 'Средние значения показателей'; - case 'highestValues': return 'Самые высокие показатели'; - case 'forPlayer': return ({required Object username}) => 'для игрока ${username}'; - case 'currentAxis': return ({required Object axis}) => 'Ось ${axis}:'; - case 'p1nkl0bst3rAlert': return 'Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r'; - case 'notForWeb': return 'Функция недоступна для веб версии'; - case 'graphs.attack': return 'Атака'; - case 'graphs.speed': return 'Скорость'; - case 'graphs.defense': return 'Защита'; - case 'graphs.cheese': return 'Сыр'; - case 'statCellNum.xpLevel': return 'Уровень\nопыта'; - case 'statCellNum.xpProgress': return 'Прогресс до следующего уровня'; - case 'statCellNum.xpFrom0ToLevel': return ({required Object n}) => 'Прогресс от 0 XP до ${n} уровня'; - case 'statCellNum.xpLeft': return 'XP осталось'; - case 'statCellNum.hoursPlayed': return 'Часов\nСыграно'; - case 'statCellNum.onlineGames': return 'Онлайн\nИгр'; - case 'statCellNum.gamesWon': return 'Онлайн\nПобед'; - case 'statCellNum.totalGames': return 'Всего матчей'; - case 'statCellNum.totalWon': return 'Всего побед'; - case 'statCellNum.friends': return 'Друзей'; - case 'statCellNum.apm': return 'Атака в\nМинуту'; - case 'statCellNum.vs': return 'Показатель\nVersus'; - case 'statCellNum.recordLB': return 'Место в таблице'; - case 'statCellNum.lbp': return 'Положение\nв рейтинге'; - case 'statCellNum.lbpShort': return '№ в рейтинге'; - case 'statCellNum.lbpc': return 'Положение\nв рейтинге страны'; - case 'statCellNum.lbpcShort': return '№ по стране'; - case 'statCellNum.gamesPlayed': return 'Игр\nСыграно'; - case 'statCellNum.gamesWonTL': return 'Побед'; - case 'statCellNum.winrate': return 'Процент\nпобед'; - case 'statCellNum.level': return 'Уровень'; - case 'statCellNum.score': return 'Счёт'; - case 'statCellNum.spp': return 'Очков\nна Фигуру'; - case 'statCellNum.pieces': return 'Фигур\nУстановлено'; - case 'statCellNum.pps': return 'Фигур в\nСекунду'; - case 'statCellNum.finesseFaults': return 'Ошибок\nТехники'; - case 'statCellNum.finessePercentage': return '% Качества\nТехники'; - case 'statCellNum.keys': return 'Нажатий\nКлавиш'; - case 'statCellNum.kpp': return 'Нажатий\nна Фигуру'; - case 'statCellNum.kps': return 'Нажатий\nв Секунду'; - case 'statCellNum.tr': return 'Тетра Рейтинг'; - case 'statCellNum.rd': return 'Отклонение рейтинга'; - case 'statCellNum.app': return 'Атака на Фигуру'; - case 'statCellNum.appDescription': return '(Сокращенно APP) Главный показатель эффективности. Показывает, сколько атаки приходится на одну фигуру'; - case 'statCellNum.vsapmDescription': return 'В основном, показывает как много мусора игрок использует в своих атаках и насколько эффективно.'; - case 'statCellNum.dss': return 'Downstack\nв Секунду'; - case 'statCellNum.dssDescription': return '(Сокращенно DS/S) Downstack (спуск вниз) в Секунду показывает как много мусорных линий в среднем игрок убирает за одну секунду.'; - case 'statCellNum.dsp': return 'Downstack\nна Фигуру'; - case 'statCellNum.dspDescription': return '(Сокращенно DS/P) Downstack (спуск вниз) на Фигуру показывает как много мусорных линий в среднем игрок убирает одну фигуру.'; - case 'statCellNum.appdsp': return 'APP + DS/P'; - case 'statCellNum.appdspDescription': return 'Просто сумма Атаки на Фигуру и Downstack на Фигуру.'; - case 'statCellNum.cheese': return 'Индекс сыра'; - case 'statCellNum.cheeseDescription': return '(Сокращенно Cheese) Индекс сыра является аппроксимацией того, насколько чистый / дырявый мусор игрок отправляет. Меньше = более чистый. Больше = более дырявый.\nПридумал kerrmunism'; - case 'statCellNum.gbe': return 'Garbage\nEfficiency'; - case 'statCellNum.gbeDescription': return '(Сокращенно Gb Eff.) Garbage Efficiency показывает насколько хорошо игрок использует свой мусор. Больше = лучше (или он использует больше мусора). Меньше = в основном отправляют сыр (или он редко чистит мусор).\nПридумали Zepheniah и Dragonboy.'; - case 'statCellNum.nyaapp': return 'Взвешенный\nAPP'; - case 'statCellNum.nyaappDescription': return '(Сокращенно wAPP) По сути, показывает способность отправлять сыр, сохраняя при этом высокую эффективность.\nПридумал Wertj.'; - case 'statCellNum.area': return 'Area'; - case 'statCellNum.areaDescription': return 'Какую площадь занимает диаграмма, если не брать в расчёт индекс сыра и VS/APM'; - case 'statCellNum.estOfTR': return 'Расчётный TR'; - case 'statCellNum.estOfTRShort': return 'Расч. TR'; - case 'statCellNum.accOfEst': return 'Точность расчёта'; - case 'statCellNum.accOfEstShort': return 'Точность'; + case 'locales.ru-RU': return 'Русский'; + case 'locales.zh-CN': return 'Упрощенный Китайский (简体中文)'; + case 'gamemodes.league': return 'Тетра Лига'; + case 'gamemodes.zenith': return 'Quick Play'; + case 'gamemodes.zenithex': return 'Quick Play Expert'; + case 'gamemodes.40l': return '40 линий'; + case 'gamemodes.blitz': return 'Блиц'; + case 'gamemodes.5mblast': return '5 000 000 бласт'; + case 'gamemodes.zen': return 'Дзен'; + case 'destinations.home': return 'Дом'; + case 'destinations.graphs': return 'Графики'; + case 'destinations.leaderboards': return 'Таблицы лидеров'; + case 'destinations.cutoffs': return 'Требования рангов'; + case 'destinations.calc': return 'Калькулятор'; + case 'destinations.info': return 'Инфо-центр'; + case 'destinations.data': return 'Сохранённые данные'; + case 'destinations.settings': return 'Настройки'; case 'playerRole.user': return 'Пользователь'; case 'playerRole.banned': return 'Заблокированный пользователь'; case 'playerRole.bot': return 'Бот'; @@ -2484,17 +5590,460 @@ extension on _StringsRu { case 'playerRole.mod': return 'Модератор'; case 'playerRole.halfmod': return 'Модератор сообщества'; case 'playerRole.anon': return 'Аноним'; - case 'numOfGameActions.pc': return 'Все чисто'; - case 'numOfGameActions.hold': return 'В запас'; - case 'numOfGameActions.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, - zero: '${n} нажатий клавиш', - one: '${n} нажатие на клавишу', - two: '${n} нажатия на клавишы', - few: '${n} нажатия на клавишы', - many: '${n} нажатий на клавиш', - other: '${n} нажатий на клавиш', + case 'goBackButton': return 'Назад'; + case 'nanow': return 'Сейчас недоступно...'; + case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}'; + case 'seasonEnded': return 'Сезон завершён'; + case 'overallPB': return ({required Object pb}) => 'Абсолютный рекорд: ${pb} м'; + case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; + case 'numOfVictories': return ({required Object wins}) => '~${wins} побед'; + case 'promotionOnNextWin': return 'Повышение после следующей победы'; + case 'numOfdefeats': return ({required Object losses}) => '~${losses} поражений'; + case 'demotionOnNextLoss': return 'Понижение после следующего поражения'; + case 'records': return 'Записи'; + case 'nerdStats': return 'Для Задротов'; + case 'playstyles': return 'Стили игры'; + case 'horoscopes': return 'Гороскопы'; + case 'relatedAchievements': return 'Достижения режима'; + case 'season': return 'Сезон'; + case 'smooth': return 'Сглаживание'; + case 'dateAndTime': return 'Дата и время'; + case 'TLfullLBnote': return 'Большая, но позволяет сортировать игроков по их статам и фильтровать их по рангам'; + case 'rank': return 'Ранг'; + case 'verdictGeneral': return ({required Object n, required Object verdict, required Object rank}) => 'На ${n} ${verdict} среднего ${rank}'; + case 'verdictBetter': return 'впереди'; + case 'verdictWorse': return 'позади'; + case 'localStanding': return 'по стране'; + case 'xp.title': return 'Уровень Опыта'; + case 'xp.progressToNextLevel': return ({required Object percentage}) => 'Прогресс до следующего уровня: ${percentage}'; + case 'xp.progressTowardsGoal': return ({required Object goal, required Object percentage, required Object left}) => 'Прогресс с 0 XP до уровня ${goal}: ${percentage} (${left} XP осталось)'; + case 'gametime.title': return 'Времени проведено в игре'; + case 'gametime.gametimeAday': return ({required Object gametime}) => '${gametime} в день в среднем'; + case 'gametime.breakdown': return ({required Object years, required Object months, required Object days, required Object minutes, required Object seconds}) => 'Это ${years} лет,\nили ${months} месяцев,\nили ${days} дней,\nили ${minutes} минут\nили ${seconds} секунд'; + case 'track': return 'Отслеживать'; + case 'stopTracking': return 'Не отслеживать'; + case 'supporter': return ({required Object tier}) => 'Спонсор ${tier}-го уровня'; + case 'comparingWith': return ({required Object newDate, required Object oldDate}) => 'Данные от ${newDate} в сравнении с данными от ${oldDate}'; + case 'compare': return 'Сравнить'; + case 'comparison': return 'Сравнение'; + case 'enterUsername': return 'Введите ник или \$avgX (где X это ранг)'; + case 'general': return 'Основное'; + case 'badges': return 'Значки'; + case 'obtainDate': return ({required Object date}) => 'Получен ${date}'; + case 'assignedManualy': return 'Этот значок был присвоен вручную администрацией TETR.IO'; + case 'distinguishment': return 'Заслуга'; + case 'banned': return 'Забанен'; + case 'bannedSubtext': return 'Баны выдаются в случаях нарушений правил TETR.IO'; + case 'badStanding': return 'Плохая репутация'; + case 'badStandingSubtext': return 'Один или более банов на счету'; + case 'botAccount': return 'Бот аккаунт'; + case 'botAccountSubtext': return ({required Object botMaintainers}) => 'Операторы: ${botMaintainers}'; + case 'copiedToClipboard': return 'Скопировано в буфер обмена!'; + case 'bio': return 'Биография'; + case 'news': return 'Новости'; + case 'matchResult.victory': return 'Победа'; + case 'matchResult.defeat': return 'Поражение'; + case 'matchResult.tie': return 'Ничья'; + case 'matchResult.dqvictory': return 'Оппонент дисквалифицирован'; + case 'matchResult.dqdefeat': return 'Дисквалифицирован'; + case 'matchResult.nocontest': return 'Без согласия'; + case 'matchResult.nullified': return 'Отменен'; + case 'distinguishments.noHeader': return 'Заголовок отсутствует'; + case 'distinguishments.noFooter': return 'Подзаголовок отсуствует'; + case 'distinguishments.twc': return 'Чемпион мира TETR.IO'; + case 'distinguishments.twcYear': return ({required Object year}) => 'Чемпионат мира по TETR.IO ${year} года'; + case 'newsEntries.leaderboard': return ({required InlineSpan rank, required InlineSpan gametype}) => TextSpan(children: [ + const TextSpan(text: 'Заработал №'), + rank, + const TextSpan(text: ' в режиме '), + gametype, + ]); + case 'newsEntries.personalbest': return ({required InlineSpan gametype, required InlineSpan pb}) => TextSpan(children: [ + const TextSpan(text: 'Новый ЛР в '), + gametype, + const TextSpan(text: ': '), + pb, + ]); + case 'newsEntries.badge': return ({required InlineSpan badge}) => TextSpan(children: [ + const TextSpan(text: 'Заработал значок '), + badge, + ]); + case 'newsEntries.rankup': return ({required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: 'Заработал '), + rank, + const TextSpan(text: ' в Тетра Лиге'), + ]); + case 'newsEntries.supporter': return ({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Стал '), + s('спонсором TETR.IO'), + ]); + case 'newsEntries.supporter_gift': return ({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: 'Получил '), + s('спонсорку TETR.IO'), + const TextSpan(text: ' в качестве подарка'), + ]); + case 'newsEntries.unknown': return ({required InlineSpan type}) => TextSpan(children: [ + const TextSpan(text: 'Неизвестная новость типа '), + type, + ]); + case 'rankupMiddle': return ({required Object r}) => '${r} ранг'; + case 'copyUserID': return 'Нажмите, чтобы скопировать ID'; + case 'searchHint': return 'Никнейм или ID'; + case 'navMenu': return 'Меню навигации'; + case 'navMenuTooltip': return 'Открыть меню навигации'; + case 'refresh': return 'Обновить данные'; + case 'searchButton': return 'Искать'; + case 'trackedPlayers': return 'Отслеживаемые игроки'; + case 'standing': return 'Положение'; + case 'previousSeasons': return 'Предыдущие сезоны'; + case 'recent': return 'Недавние'; + case 'top': return 'Топ'; + case 'noRecord': return 'Нет записи'; + case 'sprintAndBlitsRelevance': return ({required Object date}) => 'Актуальность: ${date}'; + case 'snackBarMessages.stateRemoved': return ({required Object date}) => 'Состояние от ${date} удалено из базы данных!'; + case 'snackBarMessages.matchRemoved': return ({required Object date}) => 'Матч от ${date} удален из базы данных!'; + case 'snackBarMessages.notForWeb': return 'Функция недоступна для веб-версии'; + case 'snackBarMessages.importSuccess': return 'Импорт выполнен успешно'; + case 'snackBarMessages.importCancelled': return 'Импорт был отменен'; + case 'errors.noRecords': return 'Нет записей'; + case 'errors.notEnoughData': return 'Недостаточно данных'; + case 'errors.noHistorySaved': return 'Нет сохраненной истории'; + case 'errors.connection': return ({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; + case 'errors.noSuchUser': return 'Нет такого пользователя'; + case 'errors.noSuchUserSub': return 'Либо вы опечатались, либо аккаунт больше не существует'; + case 'errors.discordNotAssigned': return 'К данному Discord ID не привязан аккаунт'; + case 'errors.discordNotAssignedSub': return 'Убедитесь, что указан правильный ID'; + case 'errors.history': return 'История этого игрока отсутствует'; + case 'errors.actionSuggestion': return 'Возможно, вы хотите'; + case 'errors.p1nkl0bst3rTLmatches': return 'Матчей Тетра Лиги не найдено'; + case 'errors.clientException': return 'Нет подключения к Интернету'; + case 'errors.forbidden': return 'Ваш IP-адрес заблокирован'; + case 'errors.forbiddenSub': return ({required Object nickname}) => 'Если вы используете VPN или Proxy, выключите его. Если это не помогает, свяжитесь с ${nickname}'; + case 'errors.tooManyRequests': return 'Слишком много запросов'; + case 'errors.tooManyRequestsSub': return 'Повторите попытку позже'; + case 'errors.internal': return 'Что-то случилось на стороне tetr.io'; + case 'errors.internalSub': return 'Скорее всего, osk уже в курсе'; + case 'errors.internalWebVersion': return 'Что-то случилось на стороне TETR.IO (или у oskware_bridge, я хз)'; + case 'errors.internalWebVersionSub': return 'Если на osk status page нет сообщений о проблемах, дайте знать dan63047'; + case 'errors.oskwareBridge': return 'Что-то случилось с oskware_bridge'; + case 'errors.oskwareBridgeSub': return 'Дайте знать dan63047'; + case 'errors.p1nkl0bst3rForbidden': return 'Сторонний API заблокировал ваш IP-адрес'; + case 'errors.p1nkl0bst3rTooManyRequests': return 'Слишком много запросов к стороннему API. Попробуйте позже'; + case 'errors.p1nkl0bst3rinternal': return 'Что-то случилось на стороне p1nkl0bst3r'; + case 'errors.p1nkl0bst3rinternalWebVersion': return 'Что-то случилось на стороне p1nkl0bst3r (или на oskware_bridge, я хз)'; + case 'errors.replayAlreadySaved': return 'Повтор уже был сохранен'; + case 'errors.replayExpired': return 'Повтор истек и больше не доступен'; + case 'errors.replayRejected': return 'Сторонний API заблокировал ваш IP-адрес'; + case 'actions.cancel': return 'Отменить'; + case 'actions.submit': return 'Подтвердить'; + case 'actions.ok': return 'ОК'; + case 'actions.apply': return 'Применить'; + case 'actions.refresh': return 'Обновить'; + case 'graphsDestination.fetchAndsaveTLHistory': return 'Получить историю игрока'; + case 'graphsDestination.fetchAndSaveOldTLmatches': return 'Получить историю матчей Тетра Лиги'; + case 'graphsDestination.fetchAndsaveTLHistoryResult': return ({required Object number}) => '${number} состояний было найдено'; + case 'graphsDestination.fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '${number} матчей было найдено'; + case 'graphsDestination.gamesPlayed': return ({required Object games}) => '${games} сыграно'; + case 'graphsDestination.dateAndTime': return 'Дата и время'; + case 'graphsDestination.filterModaleTitle': return 'Фильтровать график по рангам'; + case 'filterModale.all': return 'Все'; + case 'cutoffsDestination.title': return 'Состояние Тетра Лиги'; + case 'cutoffsDestination.relevance': return ({required Object timestamp}) => 'на момент ${timestamp}'; + case 'cutoffsDestination.actual': return 'Требование'; + case 'cutoffsDestination.target': return 'Цель'; + case 'cutoffsDestination.cutoffTR': return 'Треб. TR'; + case 'cutoffsDestination.targetTR': return 'Целевой TR'; + case 'cutoffsDestination.state': return 'Состояние'; + case 'cutoffsDestination.advanced': return 'Продвинутая'; + case 'cutoffsDestination.players': return ({required Object n}) => 'Игроков (${n})'; + case 'cutoffsDestination.moreInfo': return 'Подробнее'; + case 'cutoffsDestination.NumberOne': return ({required Object tr}) => '№ 1 - ${tr} TR'; + case 'cutoffsDestination.inflated': return ({required Object tr}) => 'Инфляция - ${tr} TR'; + case 'cutoffsDestination.notInflated': return 'Нет инфляции'; + case 'cutoffsDestination.deflated': return ({required Object tr}) => 'Дефляция - ${tr} TR'; + case 'cutoffsDestination.notDeflated': return 'Нет дефляции'; + case 'cutoffsDestination.wellDotDotDot': return 'Ну-у...'; + case 'cutoffsDestination.fromPlace': return ({required Object n}) => 'от № ${n}'; + case 'cutoffsDestination.viewButton': return 'Посмотреть'; + case 'rankView.rankTitle': return ({required Object rank}) => 'Данные ${rank} ранга'; + case 'rankView.everyoneTitle': return 'Вся таблица'; + case 'rankView.trRange': return 'Диапазон TR'; + case 'rankView.supposedToBe': return 'Должен быть'; + case 'rankView.gap': return ({required Object value}) => 'промежуток в ${value}'; + case 'rankView.trGap': return ({required Object value}) => 'промежуток в ${value} TR'; + case 'rankView.deflationGap': return 'Зона дефляции'; + case 'rankView.inflationGap': return 'Зона инфляции'; + case 'rankView.LBposRange': return 'Диапазон по позициям'; + case 'rankView.overpopulated': return ({required Object players}) => 'Переполнен ${players}'; + case 'rankView.underpopulated': return ({required Object players}) => 'Не хватает ${players}'; + case 'rankView.PlayersEqualSupposedToBe': return 'лол'; + case 'rankView.avgStats': return 'Средние значения'; + case 'rankView.avgForRank': return ({required Object rank}) => 'Среднее для ${rank} ранга'; + case 'rankView.avgNerdStats': return 'Средние задротские значения'; + case 'rankView.minimums': return 'Минимумы'; + case 'rankView.maximums': return 'Максимумы'; + case 'stateView.title': return ({required Object date}) => 'Состояние от ${date}'; + case 'tlMatchView.match': return 'Матч'; + case 'tlMatchView.vs': return 'против'; + case 'tlMatchView.winner': return 'Победитель'; + case 'tlMatchView.roundNumber': return ({required Object n}) => 'Раунд ${n}'; + case 'tlMatchView.statsFor': return 'Статистика для'; + case 'tlMatchView.numberOfRounds': return 'Количество раундов'; + case 'tlMatchView.matchLength': return 'Продолжительность матча'; + case 'tlMatchView.roundLength': return 'Продолжительность раунда'; + case 'tlMatchView.matchStats': return 'Статистика матча'; + case 'tlMatchView.downloadReplay': return 'Скачать .ttrm повтор'; + case 'tlMatchView.openReplay': return 'Открыть повтор в TETR.IO'; + case 'calcDestination.placeholders': return ({required Object stat}) => 'Введите ваш ${stat}'; + case 'calcDestination.tip': return 'Введите значения и нажмите "Считать", чтобы увидеть статистику для задротов'; + case 'calcDestination.statsCalcButton': return 'Считать'; + case 'calcDestination.damageCalcTip': return 'Нажмите на действия слева, чтобы добавить их сюда'; + case 'calcDestination.actions': return 'Действия'; + case 'calcDestination.results': return 'Результаты'; + case 'calcDestination.rules': return 'Правила'; + case 'calcDestination.noSpinClears': return 'Без спинов'; + case 'calcDestination.spins': return 'Спины'; + case 'calcDestination.miniSpins': return 'Мини спины'; + case 'calcDestination.noLineclear': return '0 линий (сброс комбо)'; + case 'calcDestination.custom': return 'Custom'; + case 'calcDestination.multiplier': return 'Множитель'; + case 'calcDestination.pcDamage': return 'PC урон'; + case 'calcDestination.comboTable': return 'Таблица комбо'; + case 'calcDestination.b2bChaining': return 'Таблица комбо'; + case 'calcDestination.surgeStartAtB2B': return 'Начинается с B2B'; + case 'calcDestination.surgeStartAmount': return 'Начинается с'; + case 'calcDestination.totalDamage': return 'Всего урона'; + case 'calcDestination.lineclears': return 'Lineclears'; + case 'calcDestination.combo': return 'Комбо'; + case 'calcDestination.surge': return 'Surge'; + case 'calcDestination.pcs': return 'PCs'; + case 'infoDestination.title': return 'Информационный Центр'; + case 'infoDestination.sprintAndBlitzAverages': return 'Средние значения для 40 линий и блиц'; + case 'infoDestination.sprintAndBlitzAveragesDescription': return 'Поскольку считать средние значения 40 линий и Блиц неудобно, они обновляется довольно редко. Кликните по названию этой карточки, чтобы увидеть таблицу средних значений 40 линий и Блиц'; + case 'infoDestination.tetraStatsWiki': return 'Tetra Stats Вики'; + case 'infoDestination.tetraStatsWikiDescription': return 'Узнайте больше о функциях Tetra Stats и статистике, что он предоставляет'; + case 'infoDestination.about': return 'О Tetra Stats'; + case 'infoDestination.aboutDescription': return 'Разработано dan63\n'; + case 'leaderboardsDestination.title': return 'Таблицы лидеров'; + case 'leaderboardsDestination.tl': return 'Тетра Лига (Текущий сезон)'; + case 'leaderboardsDestination.fullTL': return 'Тетра Лига (Текущий сезон, вся за раз)'; + case 'leaderboardsDestination.ar': return 'Очки достижений'; + case 'savedDataDestination.title': return 'Сохранённые данные'; + case 'savedDataDestination.tip': return 'Выберите никнейм слева, чтобы увидеть данные ассоциированные с ним'; + case 'savedDataDestination.seasonTLstates': return ({required Object s}) => 'TL ${s} сезона'; + case 'savedDataDestination.TLrecords': return 'Записи TL'; + case 'settingsDestination.title': return 'Настройки'; + case 'settingsDestination.general': return 'Общие'; + case 'settingsDestination.customization': return 'Кастомизация'; + case 'settingsDestination.database': return 'Локальная база данных'; + case 'settingsDestination.checking': return 'Проверяем...'; + case 'settingsDestination.enterToSubmit': return 'Enter, чтобы подтвердить'; + case 'settingsDestination.account': return 'Ваш аккаунт в TETR.IO'; + case 'settingsDestination.accountDescription': return 'Статистика этого игрока будет загружена сразу после запуска приложения. По умолчанию программа загружает мою (dan63) статистику. Чтобы изменить это, введите свой ник.'; + case 'settingsDestination.done': return 'Готово!'; + case 'settingsDestination.noSuchAccount': return 'Нет такого аккаунта'; + case 'settingsDestination.language': return 'Язык'; + case 'settingsDestination.languageDescription': return ({required Object languages}) => 'Tetra Stats был переведен на ${languages}. По умолчанию приложение выберет язык системы или Английский, если перевода на язык системы нету.'; + case 'settingsDestination.languages': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: 'ноль языков', + one: '${n} язык', + two: '${n} языка', + few: '${n} языка', + many: '${n} языков', + other: '${n} языков', ); - case 'numOfGameActions.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + case 'settingsDestination.updateInTheBackground': return 'Обновлять данные в фоновом режиме'; + case 'settingsDestination.updateInTheBackgroundDescription': return 'Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кэш истекает. Обычно это происходит каждые 5 минут'; + case 'settingsDestination.compareStats': return 'Сравнивать статистику со средними значениями ранга'; + case 'settingsDestination.compareStatsDescription': return 'Если включено, Tetra Stats загрузит средние значения и будет сравнивать вас со средними значениями вашего ранга. В результате этого почти каждый пункт статистики обретёт цвет, наводите курсор, что-бы узнать больше.'; + case 'settingsDestination.showPosition': return 'Показывать позиции по статам'; + case 'settingsDestination.showPositionDescription': return 'На загрузку потребуется немного времени (и трафика), но зато вы сможете видеть своё положение в таблице Тетра Лиги, отсортированной по статам'; + case 'settingsDestination.accentColor': return 'Цветовой акцент'; + case 'settingsDestination.accentColorDescription': return 'Этот цвет подчёркивает интерактивные элементы интерфейса.'; + case 'settingsDestination.accentColorModale': return 'Выберите цвет акцента'; + case 'settingsDestination.timestamps': return 'Формат отметок времени'; + case 'settingsDestination.timestampsDescriptionPart1': return ({required Object d}) => 'Вы можете выбрать вид отметок времени. По умолчанию показывается дата и время по Гринвичу, форматированная в соответствии с выбранной локалью. Пример: ${d}.'; + case 'settingsDestination.timestampsDescriptionPart2': return ({required Object y, required Object r}) => 'Также можно выбрать:\n• Дата и время в вашем часовом поясе: ${y}\n• Относительные отметки времени: ${r}'; + case 'settingsDestination.timestampsAbsoluteGMT': return 'Абсолютные (по Гринвичу)'; + case 'settingsDestination.timestampsAbsoluteLocalTime': return 'Абсолютные (ваша временная зона)'; + case 'settingsDestination.timestampsRelative': return 'Относительные'; + case 'settingsDestination.sheetbotLikeGraphs': return 'Графики-радары как у sheetBot'; + case 'settingsDestination.sheetbotLikeGraphsDescription': return 'Хоть и несмотря на то, что я считаю поведение графиков sheetBot-а не совсем корректным, некоторые пользователи были в замешательстве от того, что -0,5 страйд не выглядит так, как на графике sheetBot-а. Поэтому вот моё решение: если тумблер включен, точки графика могут появляться на противоположенной стороне графика если значение со знаком минус.'; + case 'settingsDestination.oskKagariGimmick': return '"Оск Кагари" прикол'; + case 'settingsDestination.oskKagariGimmickDescription': return 'Если включено, вместо настоящего ранга оска будет рендерится :kagari:.'; + case 'settingsDestination.bytesOfDataStored': return 'данных сохранено'; + case 'settingsDestination.TLrecordsSaved': return 'записей о матчах Тетра Лиги сохранено'; + case 'settingsDestination.TLplayerstatesSaved': return 'состояний Тетра Лиги сохранено'; + case 'settingsDestination.fixButton': return 'Исправить'; + case 'settingsDestination.compressButton': return 'Сжать'; + case 'settingsDestination.exportDB': return 'Экспортировать локальную базу данных'; + case 'settingsDestination.desktopExportAlertTitle': return 'Экспорт на десктопе'; + case 'settingsDestination.desktopExportText': return 'Похоже, вы используете десктопную версию. Проверьте папку "Документы", там вы должны найти файл "TetraStats.db". Скопируйте его куда-нибудь'; + case 'settingsDestination.androidExportAlertTitle': return 'Экспорт на Android'; + case 'settingsDestination.androidExportText': return ({required Object exportedDB}) => 'Экспортировано.\n${exportedDB}'; + case 'settingsDestination.importDB': return 'Импортировать локальную базу данных'; + case 'settingsDestination.importDBDescription': return 'Восстановите свою резеврную копию. Обратите внимание, что текущая база данных будет перезаписана.'; + case 'settingsDestination.importWrongFileType': return 'Неверный тип файла'; + case 'homeNavigation.overview': return 'Обзор'; + case 'homeNavigation.standing': return 'Положение'; + case 'homeNavigation.seasons': return 'Сезоны'; + case 'homeNavigation.mathces': return 'Матчи'; + case 'homeNavigation.pb': return 'Рекорд'; + case 'homeNavigation.normal': return 'Обычный'; + case 'homeNavigation.expert': return 'Эксперт'; + case 'homeNavigation.expertRecords': return 'Записи EX'; + case 'graphsNavigation.history': return 'История игрока'; + case 'graphsNavigation.league': return 'Состояние Лиги'; + case 'graphsNavigation.cutoffs': return 'История рангов'; + case 'calcNavigation.stats': return 'Калькулятор статистики'; + case 'calcNavigation.damage': return 'Калькулятор урона'; + case 'firstTimeView.welcome': return 'Добро пожаловать в Tetra Stats'; + case 'firstTimeView.description': return 'Сервис, который позволяет просматривать статистику в TETR.IO'; + case 'firstTimeView.nicknameQuestion': return 'Введите свой ник'; + case 'firstTimeView.inpuntHint': return '(3-16 символов)'; + case 'firstTimeView.emptyInputError': return 'Строка пуста'; + case 'firstTimeView.niceToSeeYou': return ({required Object n}) => 'Приятно познакомиться, ${n}'; + case 'firstTimeView.letsTakeALook': return 'Давайте же посмотрим на ваши статы...'; + case 'firstTimeView.skip': return 'Пропустить'; + case 'aboutView.title': return 'О Tetra Stats'; + case 'aboutView.about': return 'Tetra Stats — это сервис, который работает с TETR.IO Tetra Channel API, показывает данные оттуда и считает дополнительную статистику, основанную на этих данных. Сервис позволяет отслеживать прогресс в Тетра Лиге с помощью функции "Отслеживать", которая записывает каждое изменение в Лиге в локальную базу данных (не автоматически, вы должны вручную посещать свой профиль), что позволяет потом просматривать изменения с помощью графиков.\n\nBeanserver blaster — серверная часть Tetra Stats. Она собирает полную таблицу игроков Тетра Лиги, благодаря чему сортировать эту таблицу по любой метрике и строить точечную диаграмму, что позволяет анализировать тренды Лиги. Также она предоставляет историю требований рангов, которую тоже можно посмотреть на графике.\n\nВ будущем планируется добавить анализ повторов и историю турниров, так что оставайтесь на связи.\n\nСервис ни коим образом не ассоциируется с TETR.IO или osk.'; + case 'aboutView.appVersion': return 'Версия приложения'; + case 'aboutView.build': return ({required Object build}) => 'Сборка ${build}'; + case 'aboutView.GHrepo': return 'Репозиторий на GitHub'; + case 'aboutView.submitAnIssue': return 'Сообщить об ошибке'; + case 'aboutView.credits': return 'Благодарности'; + case 'aboutView.authorAndDeveloper': return 'Автор и разработчик'; + case 'aboutView.providedFormulas': return 'Предоставил формулы'; + case 'aboutView.providedS1history': return 'Предоставляет историю первого сезона лиги'; + case 'aboutView.inoue': return 'Inoue (достаёт повторы)'; + case 'aboutView.zhCNlocale': return 'Перевёл на упрощённый китайский'; + case 'aboutView.supportHim': return 'Поддержите его!'; + case 'stats.registrationDate': return 'Дата регистрации'; + case 'stats.gametime': return 'Время в игре'; + case 'stats.ogp': return 'Онлайн игр'; + case 'stats.ogw': return 'Онлайн побед'; + case 'stats.followers': return 'Подписчиков'; + case 'stats.xp.short': return 'Опыт'; + case 'stats.xp.full': return 'Очки опыта'; + case 'stats.tr.short': return 'TR'; + case 'stats.tr.full': return 'Тетра Рейтинг'; + case 'stats.glicko.short': return 'Glicko'; + case 'stats.glicko.full': return 'Glicko'; + case 'stats.rd.short': return 'RD'; + case 'stats.rd.full': return 'Отклонение Рейтинга'; + case 'stats.glixare.short': return 'GXE'; + case 'stats.glixare.full': return 'GLIXARE'; + case 'stats.s1tr.short': return 'S1 TR'; + case 'stats.s1tr.full': return 'TR как в первом сезоне'; + case 'stats.gp.short': return 'GP'; + case 'stats.gp.full': return 'Матчей'; + case 'stats.gw.short': return 'GW'; + case 'stats.gw.full': return 'Побед'; + case 'stats.winrate.short': return 'WR%'; + case 'stats.winrate.full': return 'Процент побед'; + case 'stats.apm.short': return 'APM'; + case 'stats.apm.full': return 'Атаки в Минуту'; + case 'stats.pps.short': return 'PPS'; + case 'stats.pps.full': return 'Фигур в Секунду'; + case 'stats.vs.short': return 'VS'; + case 'stats.vs.full': return 'Показатель Versus'; + case 'stats.app.short': return 'APP'; + case 'stats.app.full': return 'Атаки на Фигуру'; + case 'stats.vsapm.short': return 'VS/APM'; + case 'stats.vsapm.full': return 'VS / APM'; + case 'stats.dss.short': return 'DS/S'; + case 'stats.dss.full': return 'Спуск в секунду'; + case 'stats.dsp.short': return 'DS/P'; + case 'stats.dsp.full': return 'Спуск на фигуру'; + case 'stats.appdsp.short': return 'APP+DSP'; + case 'stats.appdsp.full': return 'APP + DSP'; + case 'stats.cheese.short': return 'Cheese'; + case 'stats.cheese.full': return 'Индекс Сыра'; + case 'stats.gbe.short': return 'GbE'; + case 'stats.gbe.full': return 'Эффективность Мусора'; + case 'stats.nyaapp.short': return 'wAPP'; + case 'stats.nyaapp.full': return 'Weighted APP'; + case 'stats.area.short': return 'Area'; + case 'stats.area.full': return 'Area'; + case 'stats.etr.short': return 'eTR'; + case 'stats.etr.full': return 'Расчётный TR'; + case 'stats.etracc.short': return '±eTR'; + case 'stats.etracc.full': return 'Точность расчёта'; + case 'stats.opener.short': return 'Opener'; + case 'stats.opener.full': return 'Опенер'; + case 'stats.plonk.short': return 'Plonk'; + case 'stats.plonk.full': return 'Плонк'; + case 'stats.stride.short': return 'Stride'; + case 'stats.stride.full': return 'Страйд'; + case 'stats.infds.short': return 'Inf. DS'; + case 'stats.infds.full': return 'Бесконечный спуск'; + case 'stats.altitude.short': return 'м'; + case 'stats.altitude.full': return 'Высота'; + case 'stats.climbSpeed.short': return 'CSP'; + case 'stats.climbSpeed.full': return 'Скорость подъёма'; + case 'stats.climbSpeed.gaugetTitle': return 'Скорость\nПодъёма'; + case 'stats.peakClimbSpeed.short': return 'Пик CSP'; + case 'stats.peakClimbSpeed.full': return 'Пиковая скорость подъёма'; + case 'stats.peakClimbSpeed.gaugetTitle': return 'Пик'; + case 'stats.kos.short': return 'KO\'s'; + case 'stats.kos.full': return 'Выбил'; + case 'stats.b2b.short': return 'B2B'; + case 'stats.b2b.full': return 'Back-To-Back'; + case 'stats.finesse.short': return 'F'; + case 'stats.finesse.full': return 'Техника'; + case 'stats.finesse.widgetTitle': return 'Техника'; + case 'stats.finesseFaults.short': return 'FF'; + case 'stats.finesseFaults.full': return 'Ошибок техники'; + case 'stats.totalTime.short': return 'Время'; + case 'stats.totalTime.full': return 'Общее время'; + case 'stats.totalTime.widgetTitle': return 'Общее время'; + case 'stats.level.short': return 'Лвл'; + case 'stats.level.full': return 'Уровень'; + case 'stats.pieces.short': return 'P'; + case 'stats.pieces.full': return 'Фигур'; + case 'stats.spp.short': return 'SPP'; + case 'stats.spp.full': return 'Очков на Фигуру'; + case 'stats.kp.short': return 'KP'; + case 'stats.kp.full': return 'Нажатий клавиш'; + case 'stats.kpp.short': return 'KPP'; + case 'stats.kpp.full': return 'Нажатий клавиш на Фигуру'; + case 'stats.kps.short': return 'KPS'; + case 'stats.kps.full': return 'Нажатий клавиш в Секунду'; + case 'stats.blitzScore': return ({required Object p}) => '${p} очков'; + case 'stats.levelUpRequirement': return ({required Object p}) => 'Очков для повышения уровня: ${p}'; + case 'stats.piecesTotal': return 'Всего фигур установлено'; + case 'stats.piecesWithPerfectFinesse': return 'Установлено с идеальной техникой'; + case 'stats.score': return 'Счёт'; + case 'stats.lines': return 'Линий'; + case 'stats.linesShort': return 'L'; + case 'stats.pcs': return 'Perfect Clears'; + case 'stats.holds': return 'Holds'; + case 'stats.spike': return 'Top Spike'; + case 'stats.top': return ({required Object percentage}) => 'Топ ${percentage}'; + case 'stats.topRank': return ({required Object rank}) => 'Топ ранг: ${rank}'; + case 'stats.floor': return 'Этаж'; + case 'stats.split': return 'Сектор'; + case 'stats.total': return 'Всего'; + case 'stats.sent': return 'Отправлено'; + case 'stats.received': return 'Получено'; + case 'stats.placement': return 'Положение'; + case 'stats.peak': return 'Пик'; + case 'stats.qpWithMods': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + one: 'С 1 модом', + two: 'С ${n} модами', + few: 'С ${n} модами', + many: 'С ${n} модами', + other: 'С ${n} модами', + ); + case 'stats.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} нажатий клавиш', + one: '${n} нажатие клавиш', + two: '${n} нажатия клавиш', + few: '${n} нажатия клавиш', + many: '${n} нажатий клавиш', + other: '${n} нажатий клавиш', + ); + case 'stats.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, zero: '${n} T-спинов всего', one: 'Всего ${n} T-спин', two: '${n} T-спина всего', @@ -2502,7 +6051,7 @@ extension on _StringsRu { many: '${n} T-спинов всего', other: '${n} T-спинов всего', ); - case 'numOfGameActions.lineClears': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + case 'stats.linesCleared': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, zero: '${n} линий очищено', one: '${n} линия очищена', two: '${n} линии очищено', @@ -2510,36 +6059,59 @@ extension on _StringsRu { many: '${n} линий очищено', other: '${n} линий очищено', ); - case 'popupActions.cancel': return 'Отменить'; - case 'popupActions.submit': return 'Подтвердить'; - case 'popupActions.ok': return 'OK'; - case 'errors.connection': return ({required Object code, required Object message}) => 'Проблема с подключением: ${code} ${message}'; - case 'errors.noSuchUser': return 'Нет такого пользователя'; - case 'errors.noSuchUserSub': return 'Либо вы ошиблись при вводе, либо аккаунта больше не существует'; - case 'errors.discordNotAssigned': return 'К данному Discord ID не привязан аккаунт'; - case 'errors.discordNotAssignedSub': return 'Убедитесь в том, что вы вставили правильный ID'; - case 'errors.history': return 'История данного игрока отсутствует'; - case 'errors.actionSuggestion': return 'Возможно, вы хотите'; - case 'errors.p1nkl0bst3rTLmatches': return 'Старых матчей Тетра Лиги не было найдено'; - case 'errors.clientException': return 'Нет соединения с интернетом'; - case 'errors.forbidden': return 'Ваш IP адрес заблокирован'; - case 'errors.forbiddenSub': return ({required Object nickname}) => 'Если у вас работает VPN или прокси, выключите его. Если это не помогло, свяжитесь с ${nickname}'; - case 'errors.tooManyRequests': return 'Слишком много запросов'; - case 'errors.tooManyRequestsSub': return 'Подождите немного и попробуйте снова'; - case 'errors.internal': return 'Что-то случилось на стороне tetr.io'; - case 'errors.internalSub': return 'Скорее всего, osk уже в курсе об этом'; - case 'errors.internalWebVersion': return 'Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)'; - case 'errors.internalWebVersionSub': return 'Если статус страница osk-а говорит, что всё ок - свяжитесь с dan63047'; - case 'errors.oskwareBridge': return 'Что-то случилось с oskware_bridge'; - case 'errors.oskwareBridgeSub': return 'Дайте dan63047 знать'; - case 'errors.p1nkl0bst3rForbidden': return 'Стороннее API заблокировало ваш IP адрес'; - case 'errors.p1nkl0bst3rTooManyRequests': return 'Слишком много запросов к стороннему API. Попробуйте позже'; - case 'errors.p1nkl0bst3rinternal': return 'Что-то случилось на стороне p1nkl0bst3r-а'; - case 'errors.p1nkl0bst3rinternalWebVersion': return 'Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)'; - case 'errors.replayAlreadySaved': return 'Повтор уже сохранён'; - case 'errors.replayExpired': return 'Повтор истёк и больше недоступен'; - case 'errors.replayRejected': return 'Стороннее API заблокировало ваш IP адрес'; - case 'countries.': return 'Не выбрана'; + case 'stats.graphs.attack': return 'Атака'; + case 'stats.graphs.speed': return 'Скорость'; + case 'stats.graphs.defense': return 'Оборона'; + case 'stats.graphs.cheese': return 'Сыр'; + case 'stats.players': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} игроков', + one: '${n} игрок', + two: '${n} игрока', + few: '${n} игрока', + many: '${n} игроков', + other: '${n} игроков', + ); + case 'stats.games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))(n, + zero: '${n} игр', + one: '${n} игра', + two: '${n} игры', + few: '${n} игры', + many: '${n} игр', + other: '${n} игр', + ); + case 'stats.lineClear.single': return 'Single'; + case 'stats.lineClear.double': return 'Double'; + case 'stats.lineClear.triple': return 'Triple'; + case 'stats.lineClear.quad': return 'Quad'; + case 'stats.lineClear.penta': return 'Penta'; + case 'stats.lineClear.hexa': return 'Hexa'; + case 'stats.lineClear.hepta': return 'Hepta'; + case 'stats.lineClear.octa': return 'Octa'; + case 'stats.lineClear.ennea': return 'Ennea'; + case 'stats.lineClear.deca': return 'Deca'; + case 'stats.lineClear.hendeca': return 'Hendeca'; + case 'stats.lineClear.dodeca': return 'Dodeca'; + case 'stats.lineClear.triadeca': return 'Triadeca'; + case 'stats.lineClear.tessaradeca': return 'Tessaradeca'; + case 'stats.lineClear.pentedeca': return 'Pentedeca'; + case 'stats.lineClear.hexadeca': return 'Hexadeca'; + case 'stats.lineClear.heptadeca': return 'Heptadeca'; + case 'stats.lineClear.octadeca': return 'Octadeca'; + case 'stats.lineClear.enneadeca': return 'Enneadeca'; + case 'stats.lineClear.eicosa': return 'Eicosa'; + case 'stats.lineClear.kagaris': return 'Kagaris'; + case 'stats.lineClears.zero': return 'Zeros'; + case 'stats.lineClears.single': return 'Singles'; + case 'stats.lineClears.double': return 'Doubles'; + case 'stats.lineClears.triple': return 'Triples'; + case 'stats.lineClears.quad': return 'Quads'; + case 'stats.lineClears.penta': return 'Pentas'; + case 'stats.mini': return 'Mini'; + case 'stats.tSpin': return 'T-spin'; + case 'stats.tSpins': return 'T-spins'; + case 'stats.spin': return 'Spin'; + case 'stats.spins': return 'Spins'; + case 'countries.': return 'Во всем мире'; case 'countries.AF': return 'Афганистан'; case 'countries.AX': return 'Аландские острова'; case 'countries.AL': return 'Албания'; @@ -2770,13 +6342,13 @@ extension on _StringsRu { case 'countries.TL': return 'Тимор-Лешти'; case 'countries.TG': return 'Того'; case 'countries.TK': return 'Токелау'; - case 'countries.TO': return 'Tonga'; + case 'countries.TO': return 'Тонга'; case 'countries.TT': return 'Тринидад и Тобаго'; case 'countries.TN': return 'Тунис'; case 'countries.TR': return 'Турция'; case 'countries.TM': return 'Туркменистан'; case 'countries.TC': return 'Острова Теркс и Кайкос'; - case 'countries.ТВ': return 'Тувалу'; + case 'countries.TV': return 'Тувалу'; case 'countries.UG': return 'Уганда'; case 'countries.UA': return 'Украина'; case 'countries.AE': return 'Объединенные Арабские Эмираты'; @@ -2802,3 +6374,816 @@ extension on _StringsRu { } } } + +extension on _StringsZhCn { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'locales.en': return '英语 (English)'; + case 'locales.ru-RU': return '俄语 (Русский)'; + case 'locales.zh-CN': return '简体中文'; + case 'gamemodes.league': return 'Tetra 联赛'; + case 'gamemodes.zenith': return '快速游戏'; + case 'gamemodes.zenithex': return '快速游戏 · 专家模式'; + case 'gamemodes.40l': return '40行竞速'; + case 'gamemodes.blitz': return '闪电战'; + case 'gamemodes.5mblast': return '5,000,000 Blast'; + case 'gamemodes.zen': return '禅意模式'; + case 'destinations.home': return '主页'; + case 'destinations.graphs': return '图表'; + case 'destinations.leaderboards': return '排行榜'; + case 'destinations.cutoffs': return '段位分界线'; + case 'destinations.calc': return '计算器'; + case 'destinations.info': return '信息中心'; + case 'destinations.data': return '已保存的数据'; + case 'destinations.settings': return '设置'; + case 'playerRole.user': return '用户'; + case 'playerRole.banned': return '已封禁'; + case 'playerRole.bot': return '机器人'; + case 'playerRole.sysop': return '系统操作员'; + case 'playerRole.admin': return '管理员'; + case 'playerRole.mod': return '管理员'; + case 'playerRole.halfmod': return '社区管理员'; + case 'playerRole.anon': return '匿名用户'; + case 'goBackButton': return '返回'; + case 'nanow': return '目前不可用...'; + case 'seasonEnds': return ({required Object countdown}) => '当前赛季还有${countdown}结束'; + case 'seasonEnded': return '赛季已结束'; + case 'overallPB': return ({required Object pb}) => '生涯最佳:${pb} m'; + case 'gamesUntilRanked': return ({required Object left}) => '还有${left}局才可获得段位'; + case 'numOfVictories': return ({required Object wins}) => '~${wins}次胜局'; + case 'promotionOnNextWin': return '下一场胜局即可升段'; + case 'numOfdefeats': return ({required Object losses}) => '~${losses}次负局'; + case 'demotionOnNextLoss': return '下一场负局即可掉段'; + case 'records': return '记录'; + case 'nerdStats': return '详细信息'; + case 'playstyles': return '游戏方式'; + case 'horoscopes': return '散点图'; + case 'relatedAchievements': return '相关成就'; + case 'season': return '赛季'; + case 'smooth': return '平滑'; + case 'dateAndTime': return '日期和时间:'; + case 'TLfullLBnote': return '很大,但允许你通过玩家的数据对玩家进行排序,还可以按段位筛选玩家'; + case 'rank': return '段位'; + case 'verdictGeneral': return ({required Object rank, required Object n, required Object verdict}) => '比 ${rank} 段平均数据${n} ${verdict}'; + case 'verdictBetter': return '好'; + case 'verdictWorse': return '差'; + case 'localStanding': return '本地'; + case 'xp.title': return '经验等级'; + case 'xp.progressToNextLevel': return ({required Object percentage}) => '到下一等级的进度:${percentage}'; + case 'xp.progressTowardsGoal': return ({required Object goal, required Object percentage, required Object left}) => '从0级到${goal}级的进度:${percentage} (还差 ${left} 点经验值)'; + case 'gametime.title': return '精确游戏时长'; + case 'gametime.gametimeAday': return ({required Object gametime}) => '平均每天${gametime}'; + case 'gametime.breakdown': return ({required Object years, required Object months, required Object days, required Object minutes, required Object seconds}) => '相当于 ${years} 年,\n${months} 月,\n${days} 天,\n${minutes} 分钟,\n${seconds} 秒'; + case 'track': return '跟踪'; + case 'stopTracking': return '停止跟踪'; + case 'supporter': return ({required Object tier}) => '${tier}级会员'; + case 'comparingWith': return ({required Object newDate, required Object oldDate}) => '${newDate}的数据与${oldDate}相比'; + case 'compare': return '比较'; + case 'comparison': return '比较'; + case 'enterUsername': return '输入用户名或者\$avgX (X是一个段位)'; + case 'general': return '常规'; + case 'badges': return '勋章'; + case 'obtainDate': return ({required Object date}) => '于${date}获得'; + case 'assignedManualy': return '此徽章由TETR.IO管理员手动颁发'; + case 'distinguishment': return '区别'; + case 'banned': return '已封禁'; + case 'bannedSubtext': return '由于 TETR.IO 规则或服务条款被违反 而被封禁'; + case 'badStanding': return '信誉不佳'; + case 'badStandingSubtext': return '近期有一次或多次违禁行为'; + case 'botAccount': return '机器人账号'; + case 'botAccountSubtext': return ({required Object botMaintainers}) => '由${botMaintainers}管理'; + case 'copiedToClipboard': return '已复制到剪贴板!'; + case 'bio': return '个性签名'; + case 'news': return '新闻'; + case 'matchResult.victory': return '胜利'; + case 'matchResult.defeat': return '失败'; + case 'matchResult.tie': return '平局'; + case 'matchResult.dqvictory': return '对手被取消资格'; + case 'matchResult.dqdefeat': return '被取消资格'; + case 'matchResult.nocontest': return '无竞赛记录'; + case 'matchResult.nullified': return '竞赛记录已取消'; + case 'distinguishments.noHeader': return '缺少标题'; + case 'distinguishments.noFooter': return '缺少标题'; + case 'distinguishments.twc': return 'TETR.IO 世界冠军'; + case 'distinguishments.twcYear': return ({required Object year}) => '${year} TETR.IO 世界杯'; + case 'newsEntries.leaderboard': return ({required InlineSpan gametype, required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: '在'), + gametype, + const TextSpan(text: '中荣获第'), + rank, + const TextSpan(text: '名'), + ]); + case 'newsEntries.personalbest': return ({required InlineSpan gametype, required InlineSpan pb}) => TextSpan(children: [ + const TextSpan(text: '在'), + gametype, + const TextSpan(text: '中取得新纪录:'), + pb, + ]); + case 'newsEntries.badge': return ({required InlineSpan badge}) => TextSpan(children: [ + const TextSpan(text: '获得勋章 '), + badge, + ]); + case 'newsEntries.rankup': return ({required InlineSpan rank}) => TextSpan(children: [ + const TextSpan(text: '升 '), + rank, + ]); + case 'newsEntries.supporter': return ({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: '成为'), + s('TETR.IO supporter'), + ]); + case 'newsEntries.supporter_gift': return ({required InlineSpanBuilder s}) => TextSpan(children: [ + const TextSpan(text: '被赠送'), + s('TETR.IO supporter'), + ]); + case 'newsEntries.unknown': return ({required InlineSpan type}) => TextSpan(children: [ + const TextSpan(text: '未知新闻类型 '), + type, + ]); + case 'rankupMiddle': return ({required Object r}) => '${r} 段'; + case 'copyUserID': return '点击以复制用户 ID'; + case 'searchHint': return '用户名或 ID'; + case 'navMenu': return '导航菜单'; + case 'navMenuTooltip': return '打开导航菜单'; + case 'refresh': return '刷新数据'; + case 'searchButton': return '搜索'; + case 'trackedPlayers': return '跟踪的玩家'; + case 'standing': return '名次'; + case 'previousSeasons': return '上赛季'; + case 'recent': return '最近'; + case 'top': return '前'; + case 'noRecord': return '暂无记录'; + case 'sprintAndBlitsRelevance': return ({required Object date}) => '${date}'; + case 'snackBarMessages.stateRemoved': return ({required Object date}) => '成功移除${date}时的状态!'; + case 'snackBarMessages.matchRemoved': return ({required Object date}) => '成功移除${date}时的一局!'; + case 'snackBarMessages.notForWeb': return '此功能在网络版本中不可用'; + case 'snackBarMessages.importSuccess': return '导入成功'; + case 'snackBarMessages.importCancelled': return '导入已取消'; + case 'errors.noRecords': return '暂无记录'; + case 'errors.notEnoughData': return '数据不足'; + case 'errors.noHistorySaved': return '没有保存历史记录'; + case 'errors.connection': return ({required Object code, required Object message}) => '连接错误:${code} ${message}'; + case 'errors.noSuchUser': return '用户不存在'; + case 'errors.noSuchUserSub': return '您输入的内容有误,或者用户不存在'; + case 'errors.discordNotAssigned': return '没有指定Discord ID的用户'; + case 'errors.discordNotAssignedSub': return '请确保您提供了有效的 ID'; + case 'errors.history': return '缺少该玩家的历史'; + case 'errors.actionSuggestion': return '也许,您想要'; + case 'errors.p1nkl0bst3rTLmatches': return '没有找到Tetra联赛比赛'; + case 'errors.clientException': return '你尚未连接'; + case 'errors.forbidden': return '您的 IP 地址已被封禁'; + case 'errors.forbiddenSub': return ({required Object nickname}) => '如果你在使用VPN,请关闭。如果仍然不可以,请联系${nickname}'; + case 'errors.tooManyRequests': return '您的评分已经被限制'; + case 'errors.tooManyRequestsSub': return '请稍后重试'; + case 'errors.internal': return 'TETR.IO 出现了问题!'; + case 'errors.internalSub': return 'osk,或许,已经知道了'; + case 'errors.internalWebVersion': return 'TETR.IO (也许是oskware_bridge,我不知道到底是哪儿) 出现了问题!'; + case 'errors.internalWebVersionSub': return '如果 osk status 页面显示一切都很正常,请联系dan63047'; + case 'errors.oskwareBridge': return 'oskware_bridge 出现了问题!'; + case 'errors.oskwareBridgeSub': return '请联系dan63047'; + case 'errors.p1nkl0bst3rForbidden': return '第三方API屏蔽了您的 IP 地址'; + case 'errors.p1nkl0bst3rTooManyRequests': return '第三方API请求过多,请稍后再试'; + case 'errors.p1nkl0bst3rinternal': return 'p1nkl0bst3r 那边出现了问题!'; + case 'errors.p1nkl0bst3rinternalWebVersion': return 'p1nkl0bst3r 那边(也许是oskware_bridge,我不知道到底是哪儿)出现了问题!'; + case 'errors.replayAlreadySaved': return '回放已保存'; + case 'errors.replayExpired': return '回放已过期或不再可用'; + case 'errors.replayRejected': return '第三方API屏蔽了您的 IP 地址'; + case 'actions.cancel': return '取消'; + case 'actions.submit': return '确定'; + case 'actions.ok': return '确定'; + case 'actions.apply': return '应用'; + case 'actions.refresh': return '刷新'; + case 'graphsDestination.fetchAndsaveTLHistory': return '获取玩家历史'; + case 'graphsDestination.fetchAndSaveOldTLmatches': return '获取 Tetra 联赛历史记录'; + case 'graphsDestination.fetchAndsaveTLHistoryResult': return ({required Object number}) => '找到 ${number} 个状态'; + case 'graphsDestination.fetchAndSaveOldTLmatchesResult': return ({required Object number}) => '找到 ${number} 场比赛'; + case 'graphsDestination.gamesPlayed': return ({required Object games}) => '游玩次数:${games}'; + case 'graphsDestination.dateAndTime': return '日期和时间'; + case 'graphsDestination.filterModaleTitle': return '在图表上筛选等级'; + case 'filterModale.all': return '全部'; + case 'cutoffsDestination.title': return 'Tetra 联赛 状态'; + case 'cutoffsDestination.relevance': return ({required Object timestamp}) => '${timestamp}'; + case 'cutoffsDestination.actual': return '实际'; + case 'cutoffsDestination.target': return '目标'; + case 'cutoffsDestination.cutoffTR': return '分段 TR'; + case 'cutoffsDestination.targetTR': return '目标 TR'; + case 'cutoffsDestination.state': return '状态'; + case 'cutoffsDestination.advanced': return '高级选项'; + case 'cutoffsDestination.players': return ({required Object n}) => '玩家(${n})'; + case 'cutoffsDestination.moreInfo': return '更多信息'; + case 'cutoffsDestination.NumberOne': return ({required Object tr}) => '№ 1 is ${tr} TR'; + case 'cutoffsDestination.inflated': return ({required Object tr}) => '高于目标 ${tr}'; + case 'cutoffsDestination.notInflated': return '不偏高'; + case 'cutoffsDestination.deflated': return ({required Object tr}) => '低于目标 ${tr}'; + case 'cutoffsDestination.notDeflated': return '不偏低'; + case 'cutoffsDestination.wellDotDotDot': return '嗯…'; + case 'cutoffsDestination.fromPlace': return ({required Object n}) => '自 № ${n}'; + case 'cutoffsDestination.viewButton': return '查看'; + case 'rankView.rankTitle': return ({required Object rank}) => '${rank} 段数据'; + case 'rankView.everyoneTitle': return '全部排行榜'; + case 'rankView.trRange': return 'TR 范围'; + case 'rankView.supposedToBe': return '应为'; + case 'rankView.gap': return ({required Object value}) => '相差 ${value}'; + case 'rankView.trGap': return ({required Object value}) => '相差 ${value} TR'; + case 'rankView.deflationGap': return '偏低量'; + case 'rankView.inflationGap': return '偏高量'; + case 'rankView.LBposRange': return '排行榜位置范围'; + case 'rankView.overpopulated': return ({required Object players}) => '比期望的多 ${players}'; + case 'rankView.underpopulated': return ({required Object players}) => '比期望的少 ${players}'; + case 'rankView.PlayersEqualSupposedToBe': return '符合'; + case 'rankView.avgStats': return '平均数据'; + case 'rankView.avgForRank': return ({required Object rank}) => '${rank} 段平均数据'; + case 'rankView.avgNerdStats': return '平均详细信息'; + case 'rankView.minimums': return '最小值'; + case 'rankView.maximums': return '最大值'; + case 'stateView.title': return ({required Object date}) => '${date}的状态'; + case 'tlMatchView.match': return '匹配'; + case 'tlMatchView.vs': return 'vs'; + case 'tlMatchView.winner': return '获胜者'; + case 'tlMatchView.roundNumber': return ({required Object n}) => '第${n}回合'; + case 'tlMatchView.statsFor': return '状态'; + case 'tlMatchView.numberOfRounds': return '回合数'; + case 'tlMatchView.matchLength': return '比赛时长'; + case 'tlMatchView.roundLength': return '回合时长'; + case 'tlMatchView.matchStats': return '比赛数据'; + case 'tlMatchView.downloadReplay': return '下载 .ttrm 回放'; + case 'tlMatchView.openReplay': return '在 TETR.IO 中打开回放'; + case 'calcDestination.placeholders': return ({required Object stat}) => '输入你的${stat}'; + case 'calcDestination.tip': return '输入值并按 "计算" 来查看TA的详细信息'; + case 'calcDestination.statsCalcButton': return '计算'; + case 'calcDestination.damageCalcTip': return '点击左侧的操作在此添加'; + case 'calcDestination.actions': return '操作'; + case 'calcDestination.results': return '结果'; + case 'calcDestination.rules': return '规则'; + case 'calcDestination.noSpinClears': return '非 Spin 清除'; + case 'calcDestination.spins': return 'Spin'; + case 'calcDestination.miniSpins': return 'Mini spin'; + case 'calcDestination.noLineclear': return '无清除(连消结束)'; + case 'calcDestination.custom': return '自定义'; + case 'calcDestination.multiplier': return '倍增'; + case 'calcDestination.pcDamage': return '全消伤害'; + case 'calcDestination.comboTable': return '连击'; + case 'calcDestination.b2bChaining': return 'B2B增伤'; + case 'calcDestination.surgeStartAtB2B': return '开始于B2B'; + case 'calcDestination.surgeStartAmount': return '初始值'; + case 'calcDestination.totalDamage': return '累计伤害'; + case 'calcDestination.lineclears': return '清除行数'; + case 'calcDestination.combo': return '连击'; + case 'calcDestination.surge': return 'B2B充能'; + case 'calcDestination.pcs': return '全消'; + case 'infoDestination.title': return '信息中心'; + case 'infoDestination.sprintAndBlitzAverages': return '40 行 & 闪电战平均数据'; + case 'infoDestination.sprintAndBlitzAveragesDescription': return '计算40 行 & 闪电战平均数据是个很繁琐的过程,所以很久才会更新一次。 点击标题查看完整的 40 行 & 闪电战平均数据表'; + case 'infoDestination.tetraStatsWiki': return 'Tetra Stats Wiki'; + case 'infoDestination.tetraStatsWikiDescription': return '查看更多关于Tetra Stats提供的功能和数据'; + case 'infoDestination.about': return '关于 Tetra Stats'; + case 'infoDestination.aboutDescription': return '由 dan63 开发'; + case 'leaderboardsDestination.title': return '排行榜'; + case 'leaderboardsDestination.tl': return 'Tetra 联赛(当前赛季)'; + case 'leaderboardsDestination.fullTL': return 'Tetra 联赛(当前赛季,完整)'; + case 'leaderboardsDestination.ar': return '成就点'; + case 'savedDataDestination.title': return '已保存的数据'; + case 'savedDataDestination.tip': return '选择左边的昵称以查看与之相关的数据'; + case 'savedDataDestination.seasonTLstates': return ({required Object s}) => '第${s}赛季状态'; + case 'savedDataDestination.TLrecords': return '联赛记录'; + case 'settingsDestination.title': return '设置'; + case 'settingsDestination.general': return '常规'; + case 'settingsDestination.customization': return '自定义设置'; + case 'settingsDestination.database': return '本地数据库'; + case 'settingsDestination.checking': return '正在检查...'; + case 'settingsDestination.enterToSubmit': return '按回车键提交'; + case 'settingsDestination.account': return '您的 TETR.IO 账号'; + case 'settingsDestination.accountDescription': return '该玩家的状态将在启动此应用后立即加载。 默认情况下,它会加载我的数据。如要更改,请在此输入您的昵称。'; + case 'settingsDestination.done': return '完成!'; + case 'settingsDestination.noSuchAccount': return '账号不存在'; + case 'settingsDestination.language': return '语言'; + case 'settingsDestination.languageDescription': return ({required Object languages}) => 'Tetra Stats 有${languages}。默认情况下,应用程序将选择您的系统语言,如果您的系统区域设置不可用,则选择英语。'; + case 'settingsDestination.languages': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '0种语言', + one: '${n}种语言', + two: '${n}种语言', + few: '${n}种语言', + many: '${n}种语言', + other: '${n}种语言', + ); + case 'settingsDestination.updateInTheBackground': return '后台更新数据'; + case 'settingsDestination.updateInTheBackgroundDescription': return '如果开启,Tetra Stats将尝试在缓存过期后查询新信息。通常一次/5分钟。'; + case 'settingsDestination.compareStats': return '将TL数据与段位平均水平作比较'; + case 'settingsDestination.compareStatsDescription': return '如果开启,Tetra Stats将提供额外的量度,使您能够将自己与普通玩家的等级相比较。 你看到它的方式——统计信息将以相应的颜色高亮,用光标悬停在它们上面以获取更多信息。'; + case 'settingsDestination.showPosition': return '显示排行榜中的位置'; + case 'settingsDestination.showPositionDescription': return '这可能需要一些时间(和流量),但您可以看到您在排行榜上的位置,按数据排序'; + case 'settingsDestination.accentColor': return '主题色'; + case 'settingsDestination.accentColorDescription': return '这种颜色会在这个应用上可见,而且通常会高亮显示交互界面元素。'; + case 'settingsDestination.accentColorModale': return '选取主题色'; + case 'settingsDestination.timestamps': return '时间戳格式'; + case 'settingsDestination.timestampsDescriptionPart1': return ({required Object d}) => '您可以选择时间戳显示时间的方式。默认情况下,它们以 GMT 时区显示时间,并根据所选区域设置进行格式设置,例如:${d}。'; + case 'settingsDestination.timestampsDescriptionPart2': return ({required Object y, required Object r}) => '这里还有:\n• 以您的时区设置的区域设置:${y}\n• 相对时间戳:${r}'; + case 'settingsDestination.timestampsAbsoluteGMT': return 'GMT'; + case 'settingsDestination.timestampsAbsoluteLocalTime': return '您的时区'; + case 'settingsDestination.timestampsRelative': return '相对'; + case 'settingsDestination.sheetbotLikeGraphs': return 'Sheetbot 型雷达图'; + case 'settingsDestination.sheetbotLikeGraphsDescription': return '尽管我认为,图表在 SheetBot 中的工作方式不是很正确,有些人感到困惑,那 -0.5 Stride 看起来不像它在 SheetBot 图表上那样。因此,我们这里有:如果开启,则如果数值为负,则图形上的点可以出现在图形的另一半。'; + case 'settingsDestination.oskKagariGimmick': return 'Osk-Kagari'; + case 'settingsDestination.oskKagariGimmickDescription': return '如果开启,osk的段位会显示为:kagari:'; + case 'settingsDestination.bytesOfDataStored': return '存储数据'; + case 'settingsDestination.TLrecordsSaved': return '已保存 Tetra 联赛记录'; + case 'settingsDestination.TLplayerstatesSaved': return '已保存 Tetra 联赛玩家状态'; + case 'settingsDestination.fixButton': return '修复'; + case 'settingsDestination.compressButton': return '压缩'; + case 'settingsDestination.exportDB': return '导出本地数据库'; + case 'settingsDestination.desktopExportAlertTitle': return '桌面导出'; + case 'settingsDestination.desktopExportText': return '看起来您在桌面上使用了这个应用程序。请检查您的文档文件夹,您应该找到"TetraStats.db"。请将其复制到某处'; + case 'settingsDestination.androidExportAlertTitle': return 'Android 导出'; + case 'settingsDestination.androidExportText': return ({required Object exportedDB}) => '已导出。\n${exportedDB}'; + case 'settingsDestination.importDB': return '导入本地数据库'; + case 'settingsDestination.importDBDescription': return '还原您的备份。请注意已存储的数据库将被覆盖。'; + case 'settingsDestination.importWrongFileType': return '文件类型错误!'; + case 'homeNavigation.overview': return '概览'; + case 'homeNavigation.standing': return '名次'; + case 'homeNavigation.seasons': return '赛季'; + case 'homeNavigation.mathces': return '比赛场次'; + case 'homeNavigation.pb': return '个人最佳'; + case 'homeNavigation.normal': return '普通模式'; + case 'homeNavigation.expert': return '专家模式'; + case 'homeNavigation.expertRecords': return '专家模式记录'; + case 'graphsNavigation.history': return '玩家历史记录'; + case 'graphsNavigation.league': return '联赛状态'; + case 'graphsNavigation.cutoffs': return '分段线历史'; + case 'calcNavigation.stats': return '数据计算器'; + case 'calcNavigation.damage': return '伤害计算器'; + case 'firstTimeView.welcome': return '欢迎使用 Tetra Stats'; + case 'firstTimeView.description': return '服务,允许您跟踪TETR.IO的各种数据'; + case 'firstTimeView.nicknameQuestion': return '您的昵称是?'; + case 'firstTimeView.inpuntHint': return '在此处输入... (3-16个符号)'; + case 'firstTimeView.emptyInputError': return '不能提交空字符串'; + case 'firstTimeView.niceToSeeYou': return ({required Object n}) => '很高兴见到你,${n}'; + case 'firstTimeView.letsTakeALook': return '让我们看看您的统计资料...'; + case 'firstTimeView.skip': return '跳过'; + case 'aboutView.title': return '关于 Tetra Stats'; + case 'aboutView.about': return 'Tetra Stats是一种服务,与TETR.IO Tetra Channel API共用,提供数据并根据这种数据计算一些附加度量。 服务允许用户用"Track"功能跟踪他们在Tetra League中的进度,这个功能记录每个Tetra Leage更改到本地数据库(非自动) 您必须不时地访问服务。这样,这些更改可以通过图表来查看。\n\nBeanserver blaster 是Tetra Stats的一部分,它被拆解成服务器侧脚本。 它提供完整的Tetra League排行榜,允许Tetra Stats通过任何公式对排行榜进行排序并生成散点图,这允许用户分析Tetra联赛趋势。 它还提供了Tetra League 的评分历史,用户也可以通过图表看到。\n\n我们有一个添加回放分析和锦标赛历史记录的计划,所以随时关注!\n\n服务没有与TETR.IO与osk以任何身份关联。'; + case 'aboutView.appVersion': return '版本'; + case 'aboutView.build': return ({required Object build}) => '${build}'; + case 'aboutView.GHrepo': return 'GitHub Repository'; + case 'aboutView.submitAnIssue': return '提交问题'; + case 'aboutView.credits': return '鸣谢'; + case 'aboutView.authorAndDeveloper': return '作者 & 开发者'; + case 'aboutView.providedFormulas': return '提供的公式'; + case 'aboutView.providedS1history': return '提供的 S1 历史'; + case 'aboutView.inoue': return 'Inoue (回放抓取器)'; + case 'aboutView.zhCNlocale': return '简中翻译员'; + case 'aboutView.supportHim': return '为他提供支持!'; + case 'stats.registrationDate': return '注册时间'; + case 'stats.gametime': return '游玩时长'; + case 'stats.ogp': return '在线游戏次数'; + case 'stats.ogw': return '在线游戏胜利次数'; + case 'stats.followers': return '粉丝'; + case 'stats.xp.short': return '经验值'; + case 'stats.xp.full': return '经验点'; + case 'stats.tr.short': return 'TR'; + case 'stats.tr.full': return 'Tetra 评分'; + case 'stats.glicko.short': return 'Glicko'; + case 'stats.glicko.full': return 'Glicko'; + case 'stats.rd.short': return 'RD'; + case 'stats.rd.full': return '评分偏差'; + case 'stats.glixare.short': return 'GXE'; + case 'stats.glixare.full': return 'GLIXARE'; + case 'stats.s1tr.short': return 'S1 TR'; + case 'stats.s1tr.full': return '第 1 赛季式 TR'; + case 'stats.gp.short': return 'GP'; + case 'stats.gp.full': return '总场数'; + case 'stats.gw.short': return 'GW'; + case 'stats.gw.full': return '胜场数'; + case 'stats.winrate.short': return 'WR%'; + case 'stats.winrate.full': return '胜率'; + case 'stats.apm.short': return 'APM'; + case 'stats.apm.full': return '每分钟攻击数'; + case 'stats.pps.short': return 'PPS'; + case 'stats.pps.full': return '每秒块数'; + case 'stats.vs.short': return 'VS'; + case 'stats.vs.full': return 'VS 分数'; + case 'stats.app.short': return 'APP'; + case 'stats.app.full': return '每块攻击数'; + case 'stats.vsapm.short': return 'VS/APM'; + case 'stats.vsapm.full': return 'VS / APM'; + case 'stats.dss.short': return 'DS/S'; + case 'stats.dss.full': return '每秒挖掘数'; + case 'stats.dsp.short': return 'DS/P'; + case 'stats.dsp.full': return '每块挖掘数'; + case 'stats.appdsp.short': return 'APP+DSP'; + case 'stats.appdsp.full': return 'APP + DSP'; + case 'stats.cheese.short': return 'CI'; + case 'stats.cheese.full': return '垃圾行混乱指数'; + case 'stats.gbe.short': return 'GbE'; + case 'stats.gbe.full': return '垃圾行效率'; + case 'stats.nyaapp.short': return 'wAPP'; + case 'stats.nyaapp.full': return '加权APP'; + case 'stats.area.short': return '面积'; + case 'stats.area.full': return '面积'; + case 'stats.etr.short': return 'eTR'; + case 'stats.etr.full': return '预测 TR'; + case 'stats.etracc.short': return '±eTR'; + case 'stats.etracc.full': return '预测实际差量'; + case 'stats.opener.short': return '定式'; + case 'stats.opener.full': return '定式'; + case 'stats.plonk.short': return '太极'; + case 'stats.plonk.full': return '太极'; + case 'stats.stride.short': return '速度'; + case 'stats.stride.full': return '速度'; + case 'stats.infds.short': return '挖掘'; + case 'stats.infds.full': return '挖掘'; + case 'stats.altitude.short': return 'm'; + case 'stats.altitude.full': return '高度'; + case 'stats.climbSpeed.short': return 'CSP'; + case 'stats.climbSpeed.full': return '爬行速度'; + case 'stats.climbSpeed.gaugetTitle': return '爬行速度'; + case 'stats.peakClimbSpeed.short': return '最高CSP'; + case 'stats.peakClimbSpeed.full': return '最高爬行速度'; + case 'stats.peakClimbSpeed.gaugetTitle': return '最高'; + case 'stats.kos.short': return 'KO\'s'; + case 'stats.kos.full': return '击杀'; + case 'stats.b2b.short': return 'B2B'; + case 'stats.b2b.full': return '背靠背/满贯'; + case 'stats.finesse.short': return '极'; + case 'stats.finesse.full': return '极简率'; + case 'stats.finesse.widgetTitle': return '简率'; + case 'stats.finesseFaults.short': return '非极简'; + case 'stats.finesseFaults.full': return '非极简操作数'; + case 'stats.totalTime.short': return '时长'; + case 'stats.totalTime.full': return '总时长'; + case 'stats.totalTime.widgetTitle': return '总时长'; + case 'stats.level.short': return 'Lvl'; + case 'stats.level.full': return '等级'; + case 'stats.pieces.short': return 'P'; + case 'stats.pieces.full': return '块'; + case 'stats.spp.short': return 'SPP'; + case 'stats.spp.full': return '每块得分'; + case 'stats.kp.short': return 'KP'; + case 'stats.kp.full': return '按键'; + case 'stats.kpp.short': return 'KPP'; + case 'stats.kpp.full': return '每块按键数'; + case 'stats.kps.short': return 'KPS'; + case 'stats.kps.full': return '每秒按键数'; + case 'stats.blitzScore': return ({required Object p}) => '${p} 分'; + case 'stats.levelUpRequirement': return ({required Object p}) => '还需 ${p} 升到下一级'; + case 'stats.piecesTotal': return '放块总数'; + case 'stats.piecesWithPerfectFinesse': return '极简块数'; + case 'stats.score': return '分数'; + case 'stats.lines': return '行数'; + case 'stats.linesShort': return '行'; + case 'stats.pcs': return '全消数'; + case 'stats.holds': return '暂存数'; + case 'stats.spike': return '最高暴击'; + case 'stats.top': return ({required Object percentage}) => '前 ${percentage}'; + case 'stats.topRank': return ({required Object rank}) => '最高段位:${rank}'; + case 'stats.floor': return '层'; + case 'stats.split': return '拆分'; + case 'stats.total': return '总计'; + case 'stats.sent': return '已发送'; + case 'stats.received': return '已接收'; + case 'stats.placement': return '排名'; + case 'stats.peak': return '最高'; + case 'stats.qpWithMods': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + one: '使用 1 个模组', + two: '使用 ${n} 个模组', + few: '使用 ${n} 个模组', + many: '使用 ${n} 个模组', + other: '使用 ${n} 个模组', + ); + case 'stats.inputs': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 按键', + one: '${n} 按键', + two: '${n} 按键', + few: '${n} 按键', + many: '${n} 按键', + other: '${n} 按键', + ); + case 'stats.tspinsTotal': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '总共 ${n} 次T旋', + one: '总共 ${n} 次T旋', + two: '总共 ${n} 次T旋', + few: '总共 ${n} 次T旋', + many: '总共 ${n} 次T旋', + other: '总共 ${n} 次T旋', + ); + case 'stats.linesCleared': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '总共消除 ${n} 行', + one: '总共消除 ${n} 行', + two: '总共消除 ${n} 行', + few: '总共消除 ${n} 行', + many: '总共消除 ${n} 行', + other: '总共消除 ${n} 行', + ); + case 'stats.graphs.attack': return '攻击'; + case 'stats.graphs.speed': return '速度'; + case 'stats.graphs.defense': return '防御'; + case 'stats.graphs.cheese': return '奶酪层'; + case 'stats.players': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 名玩家', + one: '${n} 名玩家', + two: '${n} 名玩家', + few: '${n} 名玩家', + many: '${n} 名玩家', + other: '${n} 名玩家', + ); + case 'stats.games': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, + zero: '${n} 次游戏', + one: '${n} 次游戏', + two: '${n} 次游戏', + few: '${n} 次游戏', + many: '${n} 次游戏', + other: '${n} 次游戏', + ); + case 'stats.lineClear.single': return 'Single'; + case 'stats.lineClear.double': return 'Double'; + case 'stats.lineClear.triple': return 'Triple'; + case 'stats.lineClear.quad': return 'Quad'; + case 'stats.lineClear.penta': return 'Penta'; + case 'stats.lineClear.hexa': return 'Hexa'; + case 'stats.lineClear.hepta': return 'Hepta'; + case 'stats.lineClear.octa': return 'Octa'; + case 'stats.lineClear.ennea': return 'Ennea'; + case 'stats.lineClear.deca': return 'Deca'; + case 'stats.lineClear.hendeca': return 'Hendeca'; + case 'stats.lineClear.dodeca': return 'Dodeca'; + case 'stats.lineClear.triadeca': return 'Triadeca'; + case 'stats.lineClear.tessaradeca': return 'Tessaradeca'; + case 'stats.lineClear.pentedeca': return 'Pentedeca'; + case 'stats.lineClear.hexadeca': return 'Hexadeca'; + case 'stats.lineClear.heptadeca': return 'Heptadeca'; + case 'stats.lineClear.octadeca': return 'Octadeca'; + case 'stats.lineClear.enneadeca': return 'Enneadeca'; + case 'stats.lineClear.eicosa': return 'Eicosa'; + case 'stats.lineClear.kagaris': return 'Kagaris'; + case 'stats.lineClears.zero': return 'Zeros'; + case 'stats.lineClears.single': return 'Singles'; + case 'stats.lineClears.double': return 'Doubles'; + case 'stats.lineClears.triple': return 'Triples'; + case 'stats.lineClears.quad': return 'Quads'; + case 'stats.lineClears.penta': return 'Pentas'; + case 'stats.mini': return 'Mini'; + case 'stats.tSpin': return 'T-spin'; + case 'stats.tSpins': return 'T-spins'; + case 'stats.spin': return 'Spin'; + case 'stats.spins': return 'Spins'; + case 'countries.': return '全球'; + case 'countries.AF': return '阿富汗'; + case 'countries.AX': return '奥兰群岛'; + case 'countries.AL': return '阿尔巴尼亚'; + case 'countries.DZ': return '阿尔及利亚'; + case 'countries.AS': return '美属萨摩亚'; + case 'countries.AD': return '安道尔'; + case 'countries.AO': return '安哥拉'; + case 'countries.AI': return '安圭拉'; + case 'countries.AQ': return '南极洲'; + case 'countries.AG': return '安提瓜和巴布达'; + case 'countries.AR': return '阿根廷'; + case 'countries.AM': return '亚美尼亚'; + case 'countries.AW': return '阿鲁巴'; + case 'countries.AU': return '澳大利亚'; + case 'countries.AT': return '奥地利'; + case 'countries.AZ': return '阿塞拜疆'; + case 'countries.BS': return '巴哈马'; + case 'countries.BH': return '巴林'; + case 'countries.BD': return '孟加拉国'; + case 'countries.BB': return '巴巴多斯'; + case 'countries.BY': return '白俄罗斯'; + case 'countries.BE': return '比利时'; + case 'countries.BZ': return '伯利兹'; + case 'countries.BJ': return '贝宁'; + case 'countries.BM': return '百慕大'; + case 'countries.BT': return '不丹'; + case 'countries.BO': return '玻利维亚'; + case 'countries.BA': return '波黑'; + case 'countries.BW': return '博茨瓦纳'; + case 'countries.BV': return '布韦岛'; + case 'countries.BR': return '巴西'; + case 'countries.IO': return '英属印度洋领地'; + case 'countries.BN': return '文莱'; + case 'countries.BG': return '保加利亚'; + case 'countries.BF': return '布基纳法索'; + case 'countries.BI': return '布隆迪'; + case 'countries.KH': return '柬埔寨'; + case 'countries.CM': return '喀麦隆'; + case 'countries.CA': return '加拿大'; + case 'countries.CV': return '佛得角'; + case 'countries.BQ': return '荷兰加勒比区'; + case 'countries.KY': return '开曼群岛'; + case 'countries.CF': return '中非'; + case 'countries.TD': return '乍得'; + case 'countries.CL': return '智利'; + case 'countries.CN': return '中国'; + case 'countries.CX': return '圣诞岛'; + case 'countries.CC': return '科科斯群岛'; + case 'countries.CO': return '哥伦比亚'; + case 'countries.KM': return '科摩罗'; + case 'countries.CG': return '刚果(布)'; + case 'countries.CD': return '刚果(金)'; + case 'countries.CK': return '库克群岛'; + case 'countries.CR': return '哥斯达黎加'; + case 'countries.CI': return '科特迪瓦'; + case 'countries.HR': return '克罗地亚'; + case 'countries.CU': return '古巴'; + case 'countries.CW': return '库拉索'; + case 'countries.CY': return '塞浦路斯'; + case 'countries.CZ': return '捷克'; + case 'countries.DK': return '丹麦'; + case 'countries.DJ': return '吉布提'; + case 'countries.DM': return '多米尼克'; + case 'countries.DO': return '多米尼加共和国'; + case 'countries.EC': return '厄瓜多尔'; + case 'countries.EG': return '埃及'; + case 'countries.SV': return '萨尔瓦多'; + case 'countries.GB-ENG': return '英格兰'; + case 'countries.GQ': return '赤道几内亚'; + case 'countries.ER': return '厄立特里亚'; + case 'countries.EE': return '爱沙尼亚'; + case 'countries.ET': return '埃塞俄比亚'; + case 'countries.EU': return '欧洲'; + case 'countries.FK': return '福克兰群岛 (马尔维纳斯)'; + case 'countries.FO': return '法罗群岛'; + case 'countries.FJ': return '斐济'; + case 'countries.FI': return '芬兰'; + case 'countries.FR': return '法国'; + case 'countries.GF': return '法属圭亚那'; + case 'countries.PF': return '法属波利尼西亚'; + case 'countries.TF': return '法属南部领地'; + case 'countries.GA': return '加蓬'; + case 'countries.GM': return '冈比亚'; + case 'countries.GE': return '格鲁吉亚'; + case 'countries.DE': return '德国'; + case 'countries.GH': return '加纳'; + case 'countries.GI': return '直布罗陀'; + case 'countries.GR': return '希腊'; + case 'countries.GL': return '格陵兰'; + case 'countries.GD': return '格林纳达'; + case 'countries.GP': return '瓜德罗普'; + case 'countries.GU': return '关岛'; + case 'countries.GT': return '危地马拉'; + case 'countries.GG': return '根西'; + case 'countries.GN': return '几内亚'; + case 'countries.GW': return '几内亚比绍'; + case 'countries.GY': return '圭亚那'; + case 'countries.HT': return '海地'; + case 'countries.HM': return '赫德岛和麦克唐纳群岛'; + case 'countries.VA': return '梵蒂冈'; + case 'countries.HN': return '洪都拉斯'; + case 'countries.HK': return '中国香港'; + case 'countries.HU': return '匈牙利'; + case 'countries.IS': return '冰岛'; + case 'countries.IN': return '印度'; + case 'countries.ID': return '印尼'; + case 'countries.IR': return '伊朗'; + case 'countries.IQ': return '伊拉克'; + case 'countries.IE': return '爱尔兰'; + case 'countries.IM': return '马恩岛'; + case 'countries.IL': return '以色列'; + case 'countries.IT': return '意大利'; + case 'countries.JM': return '牙买加'; + case 'countries.JP': return '日本'; + case 'countries.JE': return '泽西'; + case 'countries.JO': return '约旦'; + case 'countries.KZ': return '哈萨克斯坦'; + case 'countries.KE': return '肯尼亚'; + case 'countries.KI': return '基里巴斯'; + case 'countries.KP': return '朝鲜'; + case 'countries.KR': return '韩国'; + case 'countries.XK': return '科索沃'; + case 'countries.KW': return '科威特'; + case 'countries.KG': return '吉尔吉斯斯坦'; + case 'countries.LA': return '老挝'; + case 'countries.LV': return '拉托维亚'; + case 'countries.LB': return '黎巴嫩'; + case 'countries.LS': return '莱索托'; + case 'countries.LR': return '利比里亚'; + case 'countries.LY': return '利比亚'; + case 'countries.LI': return '列支敦士登'; + case 'countries.LT': return '立陶宛'; + case 'countries.LU': return '卢森堡'; + case 'countries.MO': return '中国澳门'; + case 'countries.MK': return '马其顿'; + case 'countries.MG': return '马达加斯加'; + case 'countries.MW': return '马拉维'; + case 'countries.MY': return '马来西亚'; + case 'countries.MV': return '马尔代夫'; + case 'countries.ML': return '马里'; + case 'countries.MT': return '马耳他'; + case 'countries.MH': return '马绍尔群岛'; + case 'countries.MQ': return '马提尼克'; + case 'countries.MR': return '毛里塔尼亚'; + case 'countries.MU': return '毛里求斯'; + case 'countries.YT': return '马约特岛'; + case 'countries.MX': return '墨西哥'; + case 'countries.FM': return '密克罗尼西亚'; + case 'countries.MD': return '摩尔多瓦'; + case 'countries.MC': return '摩纳哥'; + case 'countries.ME': return '黑山'; + case 'countries.MA': return '摩洛哥'; + case 'countries.MN': return '蒙古'; + case 'countries.MS': return '蒙特塞拉特'; + case 'countries.MZ': return '莫桑比克'; + case 'countries.MM': return '缅甸'; + case 'countries.NA': return '纳米比亚'; + case 'countries.NR': return '瑙鲁'; + case 'countries.NP': return '尼泊尔'; + case 'countries.NL': return '荷兰'; + case 'countries.AN': return '荷属安地列斯'; + case 'countries.NC': return '新喀里多尼亚'; + case 'countries.NZ': return '新西兰'; + case 'countries.NI': return '尼加拉瓜'; + case 'countries.NE': return '尼日尔'; + case 'countries.NG': return '尼日利亚'; + case 'countries.NU': return '纽埃'; + case 'countries.NF': return '诺福克岛'; + case 'countries.GB-NIR': return '北爱尔兰'; + case 'countries.MP': return '北马里亚纳群岛'; + case 'countries.NO': return '挪威'; + case 'countries.OM': return '阿曼'; + case 'countries.PK': return '巴基斯坦'; + case 'countries.PW': return '帕劳'; + case 'countries.PS': return '巴勒斯坦'; + case 'countries.PA': return '巴拿马'; + case 'countries.PG': return '巴布亚新几内亚'; + case 'countries.PY': return '巴拉圭'; + case 'countries.PE': return '秘鲁'; + case 'countries.PH': return '菲律宾'; + case 'countries.PN': return '皮特凯恩'; + case 'countries.PL': return '波兰'; + case 'countries.PT': return '葡萄牙'; + case 'countries.PR': return '波多黎哥'; + case 'countries.QA': return '卡塔尔'; + case 'countries.RE': return '留尼汪'; + case 'countries.RO': return '罗马尼亚'; + case 'countries.RU': return '俄罗斯'; + case 'countries.RW': return '卢旺达'; + case 'countries.BL': return '圣巴泰勒米'; + case 'countries.SH': return '圣赫勒拿-阿森松-特里斯坦达库尼亚'; + case 'countries.KN': return '圣基茨和尼维斯'; + case 'countries.LC': return '圣卢西亚'; + case 'countries.MF': return '圣马丁'; + case 'countries.PM': return '圣皮埃尔和密克隆'; + case 'countries.VC': return '圣文森特和格林纳丁斯'; + case 'countries.WS': return '萨摩亚'; + case 'countries.SM': return '圣马利诺'; + case 'countries.ST': return '圣多美和普林西比'; + case 'countries.SA': return '沙特阿拉伯'; + case 'countries.GB-SCT': return '苏格兰'; + case 'countries.SN': return '塞内加尔'; + case 'countries.RS': return '塞尔维亚'; + case 'countries.SC': return '塞舌尔'; + case 'countries.SL': return '塞拉利昂'; + case 'countries.SG': return '新加坡'; + case 'countries.SX': return '荷属圣马丁'; + case 'countries.SK': return '斯洛伐克'; + case 'countries.SI': return '斯洛文尼亚'; + case 'countries.SB': return '所罗门'; + case 'countries.SO': return '索马里'; + case 'countries.ZA': return '南非'; + case 'countries.GS': return '南乔治亚和南桑威奇'; + case 'countries.SS': return '南苏丹'; + case 'countries.ES': return '西班牙'; + case 'countries.LK': return '斯里兰卡'; + case 'countries.SD': return '苏丹'; + case 'countries.SR': return '苏里南'; + case 'countries.SJ': return '斯瓦尔巴和扬马延'; + case 'countries.SZ': return '斯威士兰'; + case 'countries.SE': return '瑞典'; + case 'countries.CH': return '瑞士'; + case 'countries.SY': return '叙利亚'; + case 'countries.TW': return '中国台湾'; + case 'countries.TJ': return '塔吉克斯坦'; + case 'countries.TZ': return '坦桑尼亚'; + case 'countries.TH': return '泰国'; + case 'countries.TL': return '东帝汶'; + case 'countries.TG': return '多哥'; + case 'countries.TK': return '托克劳'; + case 'countries.TO': return '汤加'; + case 'countries.TT': return '特立尼达和多巴哥'; + case 'countries.TN': return '突尼斯'; + case 'countries.TR': return '土耳其'; + case 'countries.TM': return '土库曼斯坦'; + case 'countries.TC': return '特克斯河凯科斯群岛'; + case 'countries.TV': return '图瓦卢'; + case 'countries.UG': return '乌干达'; + case 'countries.UA': return '乌克兰'; + case 'countries.AE': return '阿联酋'; + case 'countries.GB': return '英国'; + case 'countries.US': return '美国'; + case 'countries.UY': return '乌拉圭'; + case 'countries.UM': return '美国本土外小岛屿'; + case 'countries.UZ': return '乌兹别克斯坦'; + case 'countries.VU': return '瓦努阿图'; + case 'countries.VE': return '委内瑞拉'; + case 'countries.VN': return '越南'; + case 'countries.VG': return '英属维京群岛'; + case 'countries.VI': return '美属维京群岛'; + case 'countries.GB-WLS': return '威尔士'; + case 'countries.WF': return '瓦利斯群岛和富图纳群岛'; + case 'countries.EH': return '西撒哈拉'; + case 'countries.YE': return '也门'; + case 'countries.ZM': return '赞比亚'; + case 'countries.ZW': return '津巴布韦'; + case 'countries.XX': return '未知'; + case 'countries.XM': return '月球'; + default: return null; + } + } +} diff --git a/lib/main.dart b/lib/main.dart index bbba219..bef51b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,24 +7,20 @@ import 'dart:developer' as developer; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; -import 'package:tetra_stats/views/customization_view.dart'; -import 'package:tetra_stats/views/ranks_averages_view.dart'; -import 'package:tetra_stats/views/sprint_and_blitz_averages.dart'; -import 'package:tetra_stats/views/tl_leaderboard_view.dart'; +import 'package:tetra_stats/views/first_time_view.dart'; import 'package:window_manager/window_manager.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:tetra_stats/views/main_view.dart'; -import 'package:tetra_stats/views/settings_view.dart'; -import 'package:tetra_stats/views/tracked_players_view.dart'; -import 'package:tetra_stats/views/calc_view.dart'; import 'package:go_router/go_router.dart'; late final PackageInfo packageInfo; late SharedPreferences prefs; late TetrioService teto; +late GoRouter router; + ThemeData theme = ThemeData( fontFamily: 'Eurostile Round', colorScheme: const ColorScheme.dark( @@ -32,6 +28,12 @@ ThemeData theme = ThemeData( surface: Color.fromARGB(255, 10, 10, 10), secondary: Color(0xFF00838F), ), + textTheme: TextTheme( + titleLarge: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), + titleSmall: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9, fontWeight: FontWeight.w200), + headlineMedium: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36), + displayLarge: TextStyle(fontSize: 18), + ), cardTheme: const CardTheme(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), drawerTheme: const DrawerThemeData(surfaceTintColor: Color.fromARGB(255, 10, 10, 10)), searchBarTheme: const SearchBarThemeData( @@ -44,65 +46,31 @@ ThemeData theme = ThemeData( ), segmentedButtonTheme: SegmentedButtonThemeData( style: ButtonStyle( + visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0), side: const WidgetStatePropertyAll(BorderSide(color: Colors.transparent)), surfaceTintColor: const WidgetStatePropertyAll(Colors.cyanAccent), iconColor: const WidgetStatePropertyAll(Colors.cyanAccent), shadowColor: WidgetStatePropertyAll(Colors.cyanAccent.shade200), ) ), - scaffoldBackgroundColor: Colors.black -); - -final router = GoRouter( - initialLocation: "/", - routes: [ - GoRoute( - path: "/", - builder: (_, __) => const MainView(), - routes: [ - GoRoute( - path: 'settings', - builder: (_, __) => const SettingsView(), - routes: [ - GoRoute( - path: 'customization', - builder: (_, __) => const CustomizationView(), - ), - ] - ), - 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']) + dividerColor: Color.fromARGB(50, 158, 158, 158), + dividerTheme: DividerThemeData(color: Color.fromARGB(50, 158, 158, 158)), + expansionTileTheme: ExpansionTileThemeData( + expansionAnimationStyle: AnimationStyle(curve: Easing.standard, reverseCurve: Easing.standard), + expandedAlignment: Alignment.bottomCenter, + ), + dropdownMenuTheme: DropdownMenuThemeData(textStyle: TextStyle(fontFamily: "Eurostile Round", fontSize: 18)), + scaffoldBackgroundColor: Colors.black, + tooltipTheme: TooltipThemeData( + textStyle: TextStyle(color: Colors.white), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + border: Border.all( + color: Colors.white + ), + color: Colors.black, ) - ], + ) ); void main() async { @@ -128,6 +96,24 @@ void main() async { prefs = await SharedPreferences.getInstance(); teto = TetrioService(); + router = GoRouter( + initialLocation: prefs.getBool("notFirstTime") == true ? "/" : "/hihello", + routes: [ + GoRoute( + path: "/", + builder: (_, __) => const MainView(), + ), + GoRoute( // that one intended for Android users, that can open https://ch.tetr.io/u/ links + path: "/u/:userId", + builder: (_, __) => MainView(player: __.pathParameters['userId']) + ), + GoRoute( + path: "/hihello", + builder: (_, __) => const FirstTimeView(), + ) + ], + ); + // Choosing the locale String? locale = prefs.getString("locale"); if (locale == null){ diff --git a/lib/services/sqlite_db_controller.dart b/lib/services/sqlite_db_controller.dart index b1bb348..2d2a87a 100644 --- a/lib/services/sqlite_db_controller.dart +++ b/lib/services/sqlite_db_controller.dart @@ -86,4 +86,24 @@ class DB { var newDBStats = await dbFile.stat(); return dbStats.size - newDBStats.size; } + + Future checkImportingDB(File db) async { + final newDB = await openDatabase(db.path); + var usersTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioUsersTable}`);"); + List usersTableRows = [for (Map row in usersTable) row["name"] as String]; + if (!listEquals(usersTableRows, tetrioUsersTableRows)) return false; + var usersToTrackTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioUsersToTrackTable}`);"); + List usersToTrackTableRows = [for (Map row in usersToTrackTable) row["name"] as String]; + if (!listEquals(usersToTrackTableRows, tetrioUsersToTrackTableRows)) return false; + var leagueMatchesTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetraLeagueMatchesTable}`);"); + List leagueMatchesTableRows = [for (Map row in leagueMatchesTable) row["name"] as String]; + if (!listEquals(leagueMatchesTableRows, tetraLeagueMatchesTableRows)) return false; + var tlReplayStatsTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioTLReplayStatsTable}`);"); + List TLReplayStatsTableRows = [for (Map row in tlReplayStatsTable) row["name"] as String]; + if (!listEquals(TLReplayStatsTableRows, tetrioTLReplayStatsTableRows)) return false; + var leagueTable = await newDB.rawQuery("PRAGMA table_xinfo(`${tetrioLeagueTable}`);"); + List leagueTableRows = [for (Map row in leagueTable) row["name"] as String]; + if (!listEquals(leagueTableRows, tetrioLeagueTableRows)) return false; + return true; + } } diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 102dc89..1d2f170 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -4,21 +4,38 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; +import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:sqflite/sql.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.dart'; +import 'package:tetra_stats/data_objects/end_context_multi.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/p1nkl0bst3r.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_record.dart'; +import 'package:tetra_stats/data_objects/tetra_league_alpha_stream.dart'; +import 'package:tetra_stats/data_objects/tetra_league_beta_stream.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/data_objects/tetrio_player_from_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; +import 'package:tetra_stats/data_objects/user_records.dart'; import 'package:tetra_stats/main.dart' show packageInfo; import 'package:flutter/foundation.dart'; import 'package:tetra_stats/services/custom_http_client.dart'; import 'package:http/http.dart' as http; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/sqlite_db_controller.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:csv/csv.dart'; const String dbName = "TetraStats.db"; +const String webVersionDomain = "ts.dan63.by"; const String tetrioUsersTable = "tetrioUsers"; const String tetrioUsersToTrackTable = "tetrioUsersToTrack"; const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces"; @@ -33,6 +50,11 @@ const String endContext2 = "endContext2"; const String statesCol = "jsonStates"; const String player1id = "player1id"; const String player2id = "player2id"; +const List tetrioUsersTableRows = [idCol, nickCol, "jsonStates"]; +const List tetrioUsersToTrackTableRows = [idCol]; +const List tetraLeagueMatchesTableRows = [idCol, replayID, player1id, player2id, timestamp, endContext1, endContext2]; +const List tetrioTLReplayStatsTableRows = [idCol, "data", "freyhoe"]; +const List tetrioLeagueTableRows = [idCol, "gamesplayed", "gameswon", "tr", "glicko", "rd", "gxe", "rank", "bestrank", "apm", "pps", "vs", "decaying", "standing", "standing_local", "percentile", "prev_rank", "prev_at", "next_rank", "next_at", "percentile_rank", "season"]; /// Table, that store players data, their stats at some moments of time const String createTetrioUsersTable = ''' CREATE TABLE IF NOT EXISTS "tetrioUsers" ( @@ -268,7 +290,7 @@ class TetrioService extends DB { // If failed, actually trying to retrieve Uri url; if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID}); } else { // Actually going to hit inoue url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); } @@ -337,6 +359,25 @@ class TetrioService extends DB { return data; } + + /// Returns three integers, representing size of the database in bytes, amount of TL records in it and amount of TL states in it + Future<(int, int, int)> getDatabaseData() async { + await ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + String dbPath; + if (kIsWeb) { + dbPath = dbName; + } else { + final docsPath = await getApplicationDocumentsDirectory(); + dbPath = join(docsPath.path, dbName); + } + var dbFile = File(dbPath); + var dbSize = (await dbFile.stat()).size; + var dbTLRecordsQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetraLeagueMatchesTable}`')).first['COUNT(*)']! as int; + var dbTLStatesQuery = (await db.rawQuery('SELECT COUNT(*) FROM `${tetrioLeagueTable}`')).first['COUNT(*)']! as int; + return (dbSize, dbTLRecordsQuery, dbTLStatesQuery); + } + /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream). /// Throws an exception if fails to retrieve. Future fetchStream(String userID, String stream) async { @@ -345,7 +386,7 @@ class TetrioService extends DB { Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "singleplayerStream", "user": userID.toLowerCase().trim(), "stream": stream}); } else { url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/$stream'); } @@ -393,7 +434,7 @@ class TetrioService extends DB { Uri url; if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "PeakTR", "user": id}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "PeakTR", "user": id}); } else { // Actually going to hit p1nkl0bst3r api url = Uri.https('api.p1nkl0bst3r.xyz', 'toptr/$id'); } @@ -444,7 +485,7 @@ class TetrioService extends DB { Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "cutoffs"}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "cutoffs"}); } else { url = Uri.https('ch.tetr.io', 'api/labs/league_ranks'); } @@ -485,10 +526,10 @@ class TetrioService extends DB { } Future fetchCutoffsBeanserver() async { - Cutoffs? cached = _cache.get("", Cutoffs); + Cutoffs? cached = _cache.get("CutoffsTetrioleague_ranks", Cutoffs); if (cached != null) return cached; - Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/cutoffs.json'); + Uri url = Uri.https(webVersionDomain, 'beanserver_blaster/cutoffs.json'); try{ final response = await client.get(url); @@ -531,13 +572,68 @@ class TetrioService extends DB { } } + Future> fetchCutoffsHistory() async { + Uri url = Uri.https(webVersionDomain, 'beanserver_blaster/history.csv'); + + try{ + final response = await client.get(url); + + switch (response.statusCode) { + case 200: + List> csv = const CsvToListConverter().convert(response.body, eol: "\n")..removeAt(0); + List history = []; + for (List entry in csv){ + Map tr = {}; + Map glicko = {}; + Map 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 fetchTopOneFromTheLeaderboard() async { TetrioPlayerFromLeaderboard? cached = _cache.get("topone", TetrioPlayerFromLeaderboard); if (cached != null) return cached; Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLTopOne"}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLTopOne"}); } else { url = Uri.https('ch.tetr.io', 'api/users/by/league', {"after": "25000:0:0", "limit": "1"}); } @@ -577,10 +673,11 @@ class TetrioService extends DB { /// Retrieves Tetra League history from p1nkl0bst3r api for a player with given [id]. Returns a list of states /// (state = instance of [TetrioPlayer] at some point of time). Can throw an exception if fails to retrieve data. - Future> fetchAndsaveTLHistory(String id) async { + Future> fetchAndsaveTLHistory(String id, int season) async { + // TODO: find le way to get season 2 history Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); } else { url = Uri.https('api.p1nkl0bst3r.xyz', 'tlhist/$id'); } @@ -652,7 +749,7 @@ class TetrioService extends DB { Future fetchAndSaveOldTLmatches(String userID) async { Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLMatches", "user": userID}); } else { url = Uri.https('api.p1nkl0bst3r.xyz', 'tlmatches/$userID', {"before": "0", "count": "9000"}); } @@ -694,7 +791,7 @@ class TetrioService extends DB { TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); if (cached != null) return cached; - Uri url = Uri.https('ts.dan63.by', 'beanserver_blaster/leaderboard.json'); + Uri url = Uri.https(webVersionDomain, 'beanserver_blaster/leaderboard.json'); try{ final response = await client.get(url); @@ -728,34 +825,121 @@ class TetrioService extends DB { } } - // Stream fetchFullLeaderboard() async* { - // late double after; - // int lbLength = 100; - // TetrioPlayersLeaderboard leaderboard = await fetchTLLeaderboard(); - // after = leaderboard.leaderboard.last.tr; - // while (lbLength == 100){ - // TetrioPlayersLeaderboard pseudoLb = await fetchTLLeaderboard(after: after); - // leaderboard.addPlayers(pseudoLb.leaderboard); - // lbLength = pseudoLb.leaderboard.length; - // after = pseudoLb.leaderboard.last.tr; - // yield leaderboard; - // } - // } - - // i want to know progress, so i trying to figure out this thing: - // Stream fetchTLLeaderboardAsStream() async { - // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); - // if (cached != null) return cached; + Future> fetchTetrioLeaderboard({String? prisecter, String? lb, String? country}) async { + // TetrioPlayersLeaderboard? cached = _cache.get("league", TetrioPlayersLeaderboard); + // if (cached != null) return cached; - // Uri url; - // if (kIsWeb) { - // url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "TLLeaderboard"}); - // } else { - // url = Uri.https('ch.tetr.io', 'api/users/lists/league/all'); - // } + Uri url; + if (kIsWeb) { + url = Uri.https(webVersionDomain, 'oskware_bridge.php', { + "endpoint": "leaderboard", + "lb": lb??"league", + if (prisecter != null) "after": prisecter, + if (country != null) "country": country + }); + } else { + url = Uri.https('ch.tetr.io', 'api/users/by/${lb??"league"}', { + "limit": "100", + if (prisecter != null) "after": prisecter, + if (country != null) "country": country + }); + } + try{ + final response = await client.get(url); - // Stream stream = http.StreamedRequest("GET", url); - // } + switch (response.statusCode) { + case 200: + _lbPositions.clear(); + var rawJson = jsonDecode(response.body); + if (rawJson['success']) { // if api confirmed that everything ok + List leaderboard = []; + for (Map entry in rawJson['data']['entries']) { + leaderboard.add(TetrioPlayerFromLeaderboard.fromJson(entry, DateTime.fromMillisecondsSinceEpoch(rawJson['cache']['cached_at']))); + } + developer.log("fetchTLLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + //_cache.store(leaderboard, rawJson['cache']['cached_until']); + return leaderboard; + } else { // idk how to hit that one + developer.log("fetchTLLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); + throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side? + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchTLLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); + } + } + + Future> 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 leaderboard = []; + for (Map entry in rawJson['data']['entries']) { + leaderboard.add(RecordSingle.fromJson(entry, -1, -1)); + } + developer.log("fetchTetrioRecordsLeaderboard: Leaderboard retrieved and cached", name: "services/tetrio_crud"); + //_leaderboardsCache[rawJson['cache']['cached_until'].toString()] = leaderboard; + //_cache.store(leaderboard, rawJson['cache']['cached_until']); + return leaderboard; + } else { // idk how to hit that one + developer.log("fetchTetrioRecordsLeaderboard: Bruh", name: "services/tetrio_crud", error: rawJson); + throw Exception("Failed to get leaderboard (problems on the tetr.io side)"); // will it be on tetr.io side? + } + case 403: + throw TetrioForbidden(); + case 429: + throw TetrioTooManyRequests(); + case 418: + throw TetrioOskwareBridgeProblem(); + case 500: + case 502: + case 503: + case 504: + throw TetrioInternalProblem(); + default: + developer.log("fetchTetrioRecordsLeaderboard: Failed to fetch leaderboard", name: "services/tetrio_crud", error: response.statusCode); + throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); + } + } on http.ClientException catch (e, s) { + developer.log("$e, $s"); + throw http.ClientException(e.message, e.uri); + } + } TetrioPlayersLeaderboard? getCachedLeaderboard(){ return _cache.get("league", TetrioPlayersLeaderboard); @@ -768,7 +952,7 @@ class TetrioService extends DB { Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioNews", "user": userID.toLowerCase().trim(), "limit": "100"}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioNews", "user": userID.toLowerCase().trim(), "limit": "100"}); } else { url = Uri.https('ch.tetr.io', 'api/news/user_${userID.toLowerCase().trim()}', {"limit": "100"}); } @@ -810,15 +994,22 @@ class TetrioService extends DB { /// Retrieves avaliable Tetra League matches from Tetra Channel api. Returns stream object (fake stream). /// Throws an exception if fails to retrieve. - Future fetchTLStream(String userID) async { - TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream); - if (cached != null) return cached; + Future fetchTLStream(String userID, {String? prisecter}) async { + // TetraLeagueBetaStream? cached = _cache.get(userID, TetraLeagueBetaStream); + // if (cached != null) return cached; Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserTL", "user": userID.toLowerCase().trim()}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', { + "endpoint": "tetrioUserTL", + "user": userID.toLowerCase().trim(), + if (prisecter != null) "after": prisecter + }); } else { - url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/league/recent'); + url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records/league/recent', { + "limit": "100", + if (prisecter != null) "after": prisecter + }); } try { final response = await client.get(url); @@ -946,7 +1137,7 @@ class TetrioService extends DB { Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserRecords", "user": userID.toLowerCase().trim()}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUserRecords", "user": userID.toLowerCase().trim()}); } else { url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records'); } @@ -999,7 +1190,7 @@ class TetrioService extends DB { Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "Summaries", "id": id}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "Summaries", "id": id}); } else { url = Uri.https('ch.tetr.io', 'api/users/$id/summaries'); } @@ -1137,9 +1328,9 @@ class TetrioService extends DB { // trying to find player with given discord id Uri dUrl; if (kIsWeb) { - dUrl = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()}); + dUrl = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUserByDiscordID", "user": user.toLowerCase().trim()}); } else { - dUrl = Uri.https('ch.tetr.io', 'api/users/search/${user.toLowerCase().trim()}'); + dUrl = Uri.https('ch.tetr.io', 'api/users/search/discord:${user.toLowerCase().trim()}'); } try{ final response = await client.get(dUrl); @@ -1182,7 +1373,7 @@ class TetrioService extends DB { // finally going to obtain Uri url; if (kIsWeb) { - url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUser", "user": user.toLowerCase().trim()}); } else { url = Uri.https('ch.tetr.io', 'api/users/${user.toLowerCase().trim()}'); } @@ -1191,7 +1382,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: - var json = jsonDecode(response.body); + var json = jsonDecode(utf8.decode(response.bodyBytes)); if (json['success']) { // parse and count stats TetrioPlayer player = TetrioPlayer.fromJson(json['data'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_at'], isUtc: true), json['data']['_id'], json['data']['username'], DateTime.fromMillisecondsSinceEpoch(json['cache']['cached_until'], isUtc: true)); diff --git a/lib/utils/colors_functions.dart b/lib/utils/colors_functions.dart index 70277d9..3340437 100644 --- a/lib/utils/colors_functions.dart +++ b/lib/utils/colors_functions.dart @@ -1,10 +1,25 @@ import 'package:flutter/material.dart'; Color getColorOfRank(int rank){ + if (rank < 1) return Colors.grey; if (rank == 1) return Colors.yellowAccent; if (rank == 2) return Colors.blueGrey; if (rank == 3) return Colors.brown[400]!; if (rank <= 9) return Colors.blueAccent; if (rank <= 99) return Colors.greenAccent; return Colors.grey; +} + +Color? getStatColor(num value, num? avgValue, bool higherIsBetter){ + if (avgValue == null) return null; + num percentile = (higherIsBetter ? value / avgValue : avgValue / value).abs(); + if (percentile > 1.50) return Colors.purpleAccent; + if (percentile > 1.20) return Colors.blueAccent; + if (percentile > 0.90) return Colors.greenAccent; + if (percentile > 0.70) return Colors.yellowAccent; + return Colors.redAccent; + } + +Color getDifferenceColor(num diff){ + return diff.isNegative ? Colors.redAccent : Colors.greenAccent; } \ No newline at end of file diff --git a/lib/utils/copy_to_clipboard.dart b/lib/utils/copy_to_clipboard.dart new file mode 100644 index 0000000..727a6c8 --- /dev/null +++ b/lib/utils/copy_to_clipboard.dart @@ -0,0 +1,5 @@ +import 'package:flutter/services.dart'; + +Future copyToClipboard(String text) async { + await Clipboard.setData(ClipboardData(text: text)); +} \ No newline at end of file diff --git a/lib/utils/numers_formats.dart b/lib/utils/numers_formats.dart index 097b705..005f44a 100644 --- a/lib/utils/numers_formats.dart +++ b/lib/utils/numers_formats.dart @@ -1,6 +1,8 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +final NumberFormat compareIntf = NumberFormat("+#,###;-#,###")..maximumFractionDigits = 0; +final NumberFormat fDiff = NumberFormat("+#,###.####;-#,###.####"); final NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = 3; final NumberFormat comparef2 = NumberFormat("+#,###.##;-#,###.##")..maximumFractionDigits = 2; final NumberFormat intf = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0); @@ -11,6 +13,7 @@ final NumberFormat f2l = NumberFormat.decimalPatternDigits(locale: LocaleSetting final NumberFormat f1 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 1); final NumberFormat f0 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode); final NumberFormat percentage = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2; +final NumberFormat percentagef4 = NumberFormat.percentPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 4; /// Readable [a] - [b], without sign String readableIntDifference(int a, int b){ diff --git a/lib/utils/relative_timestamps.dart b/lib/utils/relative_timestamps.dart index 5176221..4096018 100644 --- a/lib/utils/relative_timestamps.dart +++ b/lib/utils/relative_timestamps.dart @@ -73,6 +73,10 @@ String get40lTime(int microseconds){ return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); } +String getALittleBitMoreNormalTime(Duration time){ + return "${intf.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}"; +} + String getMoreNormalTime(Duration time){ return "${nonsecs.format(time.inMinutes)}:${(fixedSecs.format(time.inMilliseconds/1000%60))}"; } diff --git a/lib/views/about_view.dart b/lib/views/about_view.dart new file mode 100644 index 0000000..aa45181 --- /dev/null +++ b/lib/views/about_view.dart @@ -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 createState() => AboutState(); +} + +class AboutCard extends StatelessWidget{ + final String title; + final String value; + final String? undervalue; //what? + final List 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 { + + @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"));}) + ])), + ], + ), + ], + ) + ], + ), + )), + ); + } +} diff --git a/lib/views/calc_view.dart b/lib/views/calc_view.dart deleted file mode 100644 index 45c7749..0000000 --- a/lib/views/calc_view.dart +++ /dev/null @@ -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 createState() => CalcState(); -} - -class CalcState extends State { - 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))); - } -} diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart deleted file mode 100644 index 333df80..0000000 --- a/lib/views/compare_view.dart +++ /dev/null @@ -1,1868 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:io'; -import 'dart:math'; -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/relative_timestamps.dart'; -import 'package:tetra_stats/widgets/vs_graphs.dart'; -import 'package:window_manager/window_manager.dart'; - -enum Mode{ - player, - stats, - averages -} -Mode greenSideMode = Mode.player; -List theGreenSide = [null, null, null]; // TetrioPlayer?, List>?, Summary -Mode redSideMode = Mode.player; -List theRedSide = [null, null, null]; -final DateFormat dateFormat = DateFormat.yMd(LocaleSettings.currentLocale.languageCode).add_Hm(); -var numbersReg = RegExp(r'\d+(\.\d*)*'); -late String oldWindowTitle; - -class CompareView extends StatefulWidget { - final List greenSide; - final List redSide; - final Mode greenMode; - final Mode redMode; - const CompareView({super.key, required this.greenSide, required this.redSide, required this.greenMode, required this.redMode}); - - @override - State createState() => CompareState(); -} - -class CompareState extends State { - late ScrollController _scrollController; - - @override - void initState() { - theGreenSide = widget.greenSide; - fetchGreenSide(widget.greenSide[0].userId); - if (widget.redSide[0] != null) fetchRedSide(widget.redSide[0].userId); - _scrollController = ScrollController(); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - } - super.initState(); - } - - @override - void dispose(){ - theGreenSide = [null, null, null]; - greenSideMode = Mode.player; - theRedSide = [null, null, null]; - redSideMode = Mode.player; - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); - super.dispose(); - } - - void fetchRedSide(String user) async { - try { - if (user.startsWith("\$avg")){ - try{ - var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; - Summaries summary = Summaries("avg${user.substring(4).toLowerCase()}", average, TetrioZen(level: 0, score: 0)); - redSideMode = Mode.averages; - theRedSide = [null, null, summary]; - return setState(() {}); - }on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); - return; - } - } - var tearDownToNumbers = numbersReg.allMatches(user); - if (tearDownToNumbers.length == 3) { - redSideMode = Mode.stats; - var threeNumbers = tearDownToNumbers.toList(); - double apm = double.parse(threeNumbers[0][0]!); - double pps = double.parse(threeNumbers[1][0]!); - double vs = double.parse(threeNumbers[2][0]!); - theRedSide = [null, - null, - Summaries(user, TetraLeague( - id: "", - timestamp: DateTime.now(), - apm: apm, - pps: pps, - vs: vs, - rd: noTrRd, - gamesPlayed: -1, - gamesWon: -1, - bestRank: "z", - decaying: true, - tr: -1, - gxe: -1, - rank: "z", - percentileRank: "z", - percentile: 1, - standing: -1, - standingLocal: -1, - nextAt: -1, - prevAt: -1, season: currentSeason), TetrioZen(level: 0, score: 0))]; - return setState(() {}); - } - var player = await teto.fetchPlayer(user); - Summaries summary = await teto.fetchSummaries(player.userId); - redSideMode = Mode.player; - //late List states; - // List>? dStates = >[]; - // try{ - // states = await teto.getPlayer(player.userId); - // for (final TetrioPlayer state in states) { - // dStates.add(DropdownMenuItem( - // value: state, child: Text(dateFormat.format(state.state)))); - // } - // dStates.firstWhere((element) => element.value == player, orElse: () { - // dStates?.add(DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne))); - // return DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne)); - // },); - // }on Exception { - // dStates = null; - // } - theRedSide = [player, null, summary]; - } on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.compareViewWrongValue(value: user)))); - } - _justUpdate(); - } - - void changeRedSide(TetraLeague user) { - setState(() { - //theRedSide[0] = user; - theRedSide[2].league = user; - }); - } - - void fetchGreenSide(String user) async { - try { - if (user.startsWith("\$avg")){ - try{ - var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; - Summaries summary = Summaries("avg${user.substring(4).toLowerCase()}", average, TetrioZen(level: 0, score: 0)); - greenSideMode = Mode.averages; - theGreenSide = [null, null, summary]; - return setState(() {}); - }on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); - return; - } - } - var tearDownToNumbers = numbersReg.allMatches(user); - if (tearDownToNumbers.length == 3) { - greenSideMode = Mode.stats; - var threeNumbers = tearDownToNumbers.toList(); - double apm = double.parse(threeNumbers[0][0]!); - double pps = double.parse(threeNumbers[1][0]!); - double vs = double.parse(threeNumbers[2][0]!); - theGreenSide = [null, - null, - Summaries(user, TetraLeague( - id: "", - timestamp: DateTime.now(), - apm: apm, - pps: pps, - vs: vs, - rd: noTrRd, - gamesPlayed: -1, - gamesWon: -1, - bestRank: "z", - decaying: true, - tr: -1, - gxe: -1, - rank: "z", - percentileRank: "z", - percentile: 1, - standing: -1, - standingLocal: -1, - nextAt: -1, - prevAt: -1, season: currentSeason), TetrioZen(level: 0, score: 0))]; - return setState(() {}); - } - var player = await teto.fetchPlayer(user); - Summaries summary = await teto.fetchSummaries(player.userId); - greenSideMode = Mode.player; - // late List states; - // List>? dStates = >[]; - // try{ - // states = await teto.getPlayer(player.userId); - // for (final TetrioPlayer state in states) { - // dStates.add(DropdownMenuItem( - // value: state, child: Text(dateFormat.format(state.state)))); - // } - // dStates.firstWhere((element) => element.value == player, orElse: () { - // dStates?.add(DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne))); - // return DropdownMenuItem( - // value: player, child: Text(t.mostRecentOne)); - // },); - // }on Exception { - // dStates = null; - // } - theGreenSide = [player, null, summary]; - } on Exception { - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Falied to assign $user"))); - } - _justUpdate(); - } - - void changeGreenSide(TetraLeague user) { - setState(() { - //theGreenSide[0] = user; - theGreenSide[2].league = user; - }); - } - - double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { - return ((1 / - (1 + pow(10, - (notyourGlicko - yourGlicko) / - (400 * sqrt(1 + (3 * pow(0.0057564273, 2) * - (pow(yourRD, 2) + pow(notyourRD, 2)) / pow(pi, 2) - ))) - ) - ) - )); - } - - void _justUpdate() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - bool bigScreen = MediaQuery.of(context).size.width > 768; - String titleGreenSide; - String titleRedSide; - switch (greenSideMode){ - case Mode.player: - titleGreenSide = theGreenSide[0] != null ? theGreenSide[0].username.toUpperCase() : "???"; - break; - case Mode.stats: - titleGreenSide = "${theGreenSide[2].league.apm} APM, ${theGreenSide[2].league.pps} PPS, ${theGreenSide[2].league.vs} VS"; - break; - case Mode.averages: - titleGreenSide = t.averageXrank(rankLetter: theGreenSide[2].league.rank.toUpperCase()); - break; - } - switch (redSideMode){ - case Mode.player: - titleRedSide = theRedSide[0] != null ? theRedSide[0].username.toUpperCase() : "???"; - break; - case Mode.stats: - titleRedSide = "${theRedSide[2].league.apm} APM, ${theRedSide[2].league.pps} PPS, ${theRedSide[2].league.vs} VS"; - break; - case Mode.averages: - titleRedSide = t.averageXrank(rankLetter: theRedSide[2].league.rank.toUpperCase()); - break; - } - windowManager.setTitle("Tetra Stats: $titleGreenSide ${t.vs} $titleRedSide"); - return Scaffold( - appBar: AppBar(title: Text("$titleGreenSide ${t.vs} $titleRedSide")), - backgroundColor: Colors.black, - body: SingleChildScrollView( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 768), - child: Column(children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.green, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, 0.4], - )), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: PlayerSelector( - data: theGreenSide, - mode: greenSideMode, - fetch: fetchGreenSide, - change: changeGreenSide, - updateState: _justUpdate, - ), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(top: 16), - child: Text("VS"), - ), - Expanded( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.red, Colors.transparent], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - stops: [0.0, 0.4], - )), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: PlayerSelector( - data: theRedSide, - mode: redSideMode, - fetch: fetchRedSide, - change: changeRedSide, - updateState: _justUpdate, - ), - ), - ), - ), - ], - ), - ), - const Divider(), - if (!listEquals(theGreenSide, [null, null, null]) && !listEquals(theRedSide, [null, null, null])) Column( - children: [ - if (theGreenSide[0] != null && theRedSide[0] != null && theGreenSide[0]!.role != "banned" && theRedSide[0]!.role != "banned") - Column( - children: [ - CompareRegTimeThingy( - greenSide: theGreenSide[0].registrationTime, - redSide: theRedSide[0].registrationTime, - label: t.registred), - CompareThingy( - label: t.statCellNum.level, - greenSide: theGreenSide[0].level, - redSide: theRedSide[0].level, - higherIsBetter: true, - fractionDigits: 2, - ), - if (!theGreenSide[0].gameTime.isNegative && !theRedSide[0].gameTime.isNegative) - CompareThingy( - greenSide: theGreenSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - redSide: theRedSide[0].gameTime.inMicroseconds / - 1000000 / - 60 / - 60, - label: t.statCellNum.hoursPlayed.replaceAll(RegExp(r'\n'), " "), - higherIsBetter: true, - fractionDigits: 2, - ), - if (theGreenSide[0].gamesPlayed >= 0 && theRedSide[0].gamesPlayed >= 0) - CompareThingy( - label: t.statCellNum.onlineGames.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesPlayed, - redSide: theRedSide[0].gamesPlayed, - higherIsBetter: true, - ), - if (theGreenSide[0].gamesWon >= 0 && theRedSide[0].gamesWon >= 0) - CompareThingy( - label: t.statCellNum.gamesWon.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[0].gamesWon, - redSide: theRedSide[0].gamesWon, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.friends, - greenSide: theGreenSide[0].friendCount, - redSide: theRedSide[0].friendCount, - higherIsBetter: true, - ), - const Divider(), - ], - ), - if (theGreenSide[0] != null && theRedSide[0] != null && (theGreenSide[0]!.role == "banned" || theRedSide[0]!.role == "banned")) - CompareBoolThingy( - greenSide: theGreenSide[0].role == "banned", - redSide: theRedSide[0].role == "banned", - label: t.normalBanned, - trueIsBetter: false - ), - (theGreenSide[2].league.gamesPlayed > 0 || greenSideMode == Mode.stats) && (theRedSide[2].league.gamesPlayed > 0 || redSideMode == Mode.stats) - ? Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.tetraLeague, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theRedSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "TR", - greenSide: theGreenSide[2].league.tr, - redSide: theRedSide[2].league.tr, - fractionDigits: 2, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesPlayed.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].league.gamesPlayed, - redSide: theRedSide[2].league.gamesPlayed, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.gamesWonTL.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].league.gamesWon, - redSide: theRedSide[2].league.gamesWon, - higherIsBetter: true, - ), - if (greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "WR %", - greenSide: - theGreenSide[2].league.winrate * 100, - redSide: theRedSide[2].league.winrate * 100, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theRedSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "Glicko", - greenSide: theGreenSide[2].league.glicko!, - redSide: theRedSide[2].league.glicko!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theRedSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: "RD", - greenSide: theGreenSide[2].league.rd!, - redSide: theRedSide[2].league.rd!, - fractionDigits: 3, - higherIsBetter: false, - ), - if (theGreenSide[2].league.standing > 0 && - theRedSide[2].league.standing > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpShort, - greenSide: theGreenSide[2].league.standing, - redSide: theRedSide[2].league.standing, - higherIsBetter: false, - ), - if (theGreenSide[2].league.standingLocal > 0 && - theRedSide[2].league.standingLocal > 0 && - greenSideMode == Mode.player && - redSideMode == Mode.player) - CompareThingy( - label: t.statCellNum.lbpcShort, - greenSide: - theGreenSide[2].league.standingLocal, - redSide: theRedSide[2].league.standingLocal, - higherIsBetter: false, - ), - if (theGreenSide[2].league.apm != null && - theRedSide[2].league.apm != null) - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].league.apm!, - redSide: theRedSide[2].league.apm!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.pps != null && - theRedSide[2].league.pps != null) - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].league.pps!, - redSide: theRedSide[2].league.pps!, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.vs != null && - theRedSide[2].league.vs != null) - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].league.vs!, - redSide: theRedSide[2].league.vs!, - fractionDigits: 2, - higherIsBetter: true, - ), - ], - ) - : CompareBoolThingy( - greenSide: theGreenSide[2].league.gamesPlayed > 0, - redSide: theRedSide[2].league.gamesPlayed > 0, - label: t.playedTL, - trueIsBetter: false), - if (theGreenSide[2].league.nerdStats != null && - theRedSide[2].league.nerdStats != null) - Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.nerdStats, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].league.nerdStats!.app, - redSide: theRedSide[2].league.nerdStats!.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].league.nerdStats!.vsapm, - redSide: theRedSide[2].league.nerdStats!.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].league.nerdStats!.dss, - redSide: theRedSide[2].league.nerdStats!.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].league.nerdStats!.dsp, - redSide: theRedSide[2].league.nerdStats!.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].league.nerdStats!.appdsp, - redSide: theRedSide[2].league.nerdStats!.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].league.nerdStats!.cheese, - redSide: theRedSide[2].league.nerdStats!.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].league.nerdStats!.gbe, - redSide: theRedSide[2].league.nerdStats!.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].league.nerdStats!.nyaapp, - redSide: theRedSide[2].league.nerdStats!.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].league.nerdStats!.area, - redSide: theRedSide[2].league.nerdStats!.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: theGreenSide[2].league.estTr!.esttr, - redSide: theRedSide[2].league.estTr!.esttr, - fractionDigits: 2, - higherIsBetter: true, - ), - if (theGreenSide[2].league.gamesPlayed > 9 && - theGreenSide[2].league.gamesPlayed > 9 && - greenSideMode != Mode.stats && - redSideMode != Mode.stats) - CompareThingy( - label: t.statCellNum.accOfEstShort, - greenSide: theGreenSide[2].league.esttracc!, - redSide: theRedSide[2].league.esttracc!, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].league.playstyle!.opener, - redSide: theRedSide[2].league.playstyle!.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].league.playstyle!.plonk, - redSide: theRedSide[2].league.playstyle!.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].league.playstyle!.stride, - redSide: theRedSide[2].league.playstyle!.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].league.playstyle!.infds, - redSide: theRedSide[2].league.playstyle!.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].league.apm!, theGreenSide[2].league.pps!, theGreenSide[2].league.vs!, theGreenSide[2].league.nerdStats!, theGreenSide[2].league.playstyle!, theRedSide[2].league.apm!, theRedSide[2].league.pps!, theRedSide[2].league.vs!, theRedSide[2].league.nerdStats!, theRedSide[2].league.playstyle!), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.winChance, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28)), - ), - if (greenSideMode != Mode.stats && redSideMode != Mode.stats && - theGreenSide[2].league.gamesPlayed > 9 && theRedSide[2].league.gamesPlayed > 9) - CompareThingy( - label: t.byGlicko, - greenSide: getWinrateByTR( - theGreenSide[2].league.glicko!, - theGreenSide[2].league.rd!, - theRedSide[2].league.glicko!, - theRedSide[2].league.rd!) * - 100, - redSide: getWinrateByTR( - theRedSide[2].league.glicko!, - theRedSide[2].league.rd!, - theGreenSide[2].league.glicko!, - theGreenSide[2].league.rd!) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - CompareThingy( - label: t.byEstTR, - greenSide: getWinrateByTR( - theGreenSide[2].league.estTr!.estglicko, - theGreenSide[2].league.rd ?? noTrRd, - theRedSide[2].league.estTr!.estglicko, - theRedSide[2].league.rd ?? noTrRd) * - 100, - redSide: getWinrateByTR( - theRedSide[2].league.estTr!.estglicko, - theRedSide[2].league.rd ?? noTrRd, - theGreenSide[2].league.estTr!.estglicko, - theGreenSide[2].league.rd ?? noTrRd) * - 100, - fractionDigits: 2, - higherIsBetter: true, - postfix: "%", - ), - ], - ), - if (theGreenSide[2].zenith != null && theRedSide[2].zenith != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.quickPlay, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Height", - greenSide: theGreenSide[2].zenith.stats.zenith!.altitude, - redSide: theRedSide[2].zenith.stats.zenith!.altitude, - fractionDigits: 2, - higherIsBetter: true, - postfix: "m", - ), - CompareThingy( - label: "Position", - greenSide: theGreenSide[2].zenith.rank, - redSide: theRedSide[2].zenith.rank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "Position (Country)", - greenSide: theGreenSide[2].zenith.countryRank, - redSide: theRedSide[2].zenith.countryRank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].zenith.aggregateStats.apm, - redSide: theRedSide[2].zenith.aggregateStats.apm, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].zenith.aggregateStats.pps, - redSide: theRedSide[2].zenith.aggregateStats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].zenith.aggregateStats.vs, - redSide: theRedSide[2].zenith.aggregateStats.vs, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KO's", - greenSide: theGreenSide[2].zenith.stats.kills, - redSide: theRedSide[2].zenith.stats.kills, - higherIsBetter: true, - ), - CompareThingy( - label: "CPS", - greenSide: theGreenSide[2].zenith.stats.cps, - redSide: theRedSide[2].zenith.stats.cps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Peak CPS", - greenSide: theGreenSide[2].zenith.stats.zenith!.peakrank, - redSide: theRedSide[2].zenith.stats.zenith!.peakrank, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareDurationThingy( - label: "Time", - greenSide: theGreenSide[2].zenith.stats.finalTime, - redSide: theRedSide[2].zenith.stats.finalTime, - higherIsBetter: false, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].zenith.stats.finessePercentage * 100, - redSide: theRedSide[2].zenith.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("${t.quickPlay} ${t.nerdStats}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.app, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.vsapm, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.dss, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.dsp, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].zenith.aggregateStats.nerdStats.appdsp, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].zenith.aggregateStats.nerdStats.cheese, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.gbe, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].zenith.aggregateStats.nerdStats.nyaapp, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].zenith.aggregateStats.nerdStats.area, - redSide: theRedSide[2].zenith.aggregateStats.nerdStats.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.opener, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.plonk, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.stride, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].zenith.aggregateStats.playstyle.infds, - redSide: theRedSide[2].zenith.aggregateStats.playstyle.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].zenith.aggregateStats.apm, theGreenSide[2].zenith.aggregateStats.pps, theGreenSide[2].zenith.aggregateStats.vs, theGreenSide[2].zenith.aggregateStats.nerdStats, theGreenSide[2].zenith.aggregateStats.playstyle, theRedSide[2].zenith.aggregateStats.apm, theRedSide[2].zenith.aggregateStats.pps, theRedSide[2].zenith.aggregateStats.vs, theRedSide[2].zenith.aggregateStats.nerdStats, theRedSide[2].zenith.aggregateStats.playstyle), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].zenith != null, redSide: theRedSide[2].zenith != null, label: "Played QP", trueIsBetter: true), - if (theGreenSide[2].zenithEx != null && theRedSide[2].zenithEx != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("${t.quickPlay} ${t.expert}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Height", - greenSide: theGreenSide[2].zenithEx.stats.zenith!.altitude, - redSide: theRedSide[2].zenithEx.stats.zenith!.altitude, - fractionDigits: 2, - higherIsBetter: true, - postfix: "m", - ), - CompareThingy( - label: "Position", - greenSide: theGreenSide[2].zenithEx.rank, - redSide: theRedSide[2].zenithEx.rank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "Position (Country)", - greenSide: theGreenSide[2].zenithEx.countryRank, - redSide: theRedSide[2].zenithEx.countryRank, - higherIsBetter: false, - prefix: "№ ", - ), - CompareThingy( - label: "APM", - greenSide: theGreenSide[2].zenithEx.aggregateStats.apm, - redSide: theRedSide[2].zenithEx.aggregateStats.apm, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].zenithEx.aggregateStats.pps, - redSide: theRedSide[2].zenithEx.aggregateStats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "VS", - greenSide: theGreenSide[2].zenithEx.aggregateStats.vs, - redSide: theRedSide[2].zenithEx.aggregateStats.vs, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KO's", - greenSide: theGreenSide[2].zenithEx.stats.kills, - redSide: theRedSide[2].zenithEx.stats.kills, - higherIsBetter: true, - ), - CompareThingy( - label: "CPS", - greenSide: theGreenSide[2].zenithEx.stats.cps, - redSide: theRedSide[2].zenithEx.stats.cps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Peak CPS", - greenSide: theGreenSide[2].zenithEx.stats.zenith!.peakrank, - redSide: theRedSide[2].zenithEx.stats.zenith!.peakrank, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareDurationThingy( - label: "Time", - greenSide: theGreenSide[2].zenithEx.stats.finalTime, - redSide: theRedSide[2].zenithEx.stats.finalTime, - higherIsBetter: false, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].zenithEx.stats.finessePercentage * 100, - redSide: theRedSide[2].zenithEx.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text("${t.quickPlay} ${t.expert} ${t.nerdStats}", style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)), - ), - CompareThingy( - label: "APP", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.app, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.app, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "VS/APM", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.vsapm, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.vsapm, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/S", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.dss, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.dss, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "DS/P", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.dsp, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.dsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "APP + DS/P", - greenSide: - theGreenSide[2].zenithEx.aggregateStats.nerdStats.appdsp, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.appdsp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: - theGreenSide[2].zenithEx.aggregateStats.nerdStats.cheese, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.cheese, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Gb Eff.", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.gbe, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.gbe, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "wAPP", - greenSide: - theGreenSide[2].zenithEx.aggregateStats.nerdStats.nyaapp, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.nyaapp, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Area", - greenSide: theGreenSide[2].zenithEx.aggregateStats.nerdStats.area, - redSide: theRedSide[2].zenithEx.aggregateStats.nerdStats.area, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Opener", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.opener, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.opener, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Plonk", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.plonk, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.plonk, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Stride", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.stride, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.stride, - fractionDigits: 3, - higherIsBetter: true, - ), - CompareThingy( - label: "Inf. DS", - greenSide: theGreenSide[2].zenithEx.aggregateStats.playstyle.infds, - redSide: theRedSide[2].zenithEx.aggregateStats.playstyle.infds, - fractionDigits: 3, - higherIsBetter: true, - ), - VsGraphs(theGreenSide[2].zenithEx.aggregateStats.apm, theGreenSide[2].zenithEx.aggregateStats.pps, theGreenSide[2].zenithEx.aggregateStats.vs, theGreenSide[2].zenithEx.aggregateStats.nerdStats, theGreenSide[2].zenithEx.aggregateStats.playstyle, theRedSide[2].zenithEx.aggregateStats.apm, theRedSide[2].zenithEx.aggregateStats.pps, theRedSide[2].zenithEx.aggregateStats.vs, theRedSide[2].zenithEx.aggregateStats.nerdStats, theRedSide[2].zenithEx.aggregateStats.playstyle), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].zenithEx != null, redSide: theRedSide[2].zenithEx != null, label: "Played QP Expert", trueIsBetter: true), - if (theGreenSide[2].sprint != null && theRedSide[2].sprint != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.sprint, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareDurationThingy( - label: "Time", - greenSide: theGreenSide[2].sprint.stats.finalTime, - redSide: theRedSide[2].sprint.stats.finalTime, - higherIsBetter: false, - ), - CompareThingy( - label: "Lines", - greenSide: theGreenSide[2].sprint.stats.lines, - redSide: theRedSide[2].sprint.stats.lines, - higherIsBetter: false, - ), - CompareThingy( - label: t.statCellNum.pieces.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].sprint.stats.piecesPlaced, - redSide: theRedSide[2].sprint.stats.piecesPlaced, - higherIsBetter: false, - ), - CompareThingy( - label: t.statCellNum.keys.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].sprint.stats.inputs, - redSide: theRedSide[2].sprint.stats.inputs, - higherIsBetter: false, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].sprint.stats.pps, - redSide: theRedSide[2].sprint.stats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KPP", - greenSide: theGreenSide[2].sprint.stats.kpp, - redSide: theRedSide[2].sprint.stats.kpp, - fractionDigits: 2, - higherIsBetter: false, - ), - CompareThingy( - label: "KPS", - greenSide: theGreenSide[2].sprint.stats.kps, - redSide: theRedSide[2].sprint.stats.kps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].sprint.stats.finessePercentage * 100, - redSide: theRedSide[2].sprint.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - CompareThingy( - label: "Holds", - greenSide: theGreenSide[2].sprint.stats.holds, - redSide: theRedSide[2].sprint.stats.holds, - higherIsBetter: false, - ), - CompareThingy( - label: "T-spins", - greenSide: theGreenSide[2].sprint.stats.tSpins, - redSide: theRedSide[2].sprint.stats.tSpins, - higherIsBetter: false, - ), - CompareThingy( - label: "Quads", - greenSide: theGreenSide[2].sprint.stats.clears.quads, - redSide: theRedSide[2].sprint.stats.clears.quads, - higherIsBetter: true, - ), - CompareThingy( - label: "PC's", - greenSide: theGreenSide[2].sprint.stats.clears.allClears, - redSide: theRedSide[2].sprint.stats.clears.allClears, - higherIsBetter: true, - ), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].sprint != null, redSide: theRedSide[2].sprint != null, label: "Played 40 Lines", trueIsBetter: true), - if (theGreenSide[2].blitz != null && theRedSide[2].blitz != null && greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.blitz, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Score", - greenSide: theGreenSide[2].blitz.stats.score, - redSide: theRedSide[2].blitz.stats.score, - higherIsBetter: true, - ), - CompareThingy( - label: "SPP", - greenSide: theGreenSide[2].blitz.stats.spp, - redSide: theRedSide[2].blitz.stats.spp, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Level", - greenSide: theGreenSide[2].blitz.stats.level, - redSide: theRedSide[2].blitz.stats.level, - higherIsBetter: true, - ), - CompareThingy( - label: "Lines", - greenSide: theGreenSide[2].blitz.stats.lines, - redSide: theRedSide[2].blitz.stats.lines, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.pieces.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].blitz.stats.piecesPlaced, - redSide: theRedSide[2].blitz.stats.piecesPlaced, - higherIsBetter: true, - ), - CompareThingy( - label: t.statCellNum.keys.replaceAll(RegExp(r'\n'), " "), - greenSide: theGreenSide[2].blitz.stats.inputs, - redSide: theRedSide[2].blitz.stats.inputs, - higherIsBetter: true, - ), - CompareThingy( - label: "PPS", - greenSide: theGreenSide[2].blitz.stats.pps, - redSide: theRedSide[2].blitz.stats.pps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "KPP", - greenSide: theGreenSide[2].blitz.stats.kpp, - redSide: theRedSide[2].blitz.stats.kpp, - fractionDigits: 2, - higherIsBetter: false, - ), - CompareThingy( - label: "KPS", - greenSide: theGreenSide[2].blitz.stats.kps, - redSide: theRedSide[2].blitz.stats.kps, - fractionDigits: 2, - higherIsBetter: true, - ), - CompareThingy( - label: "Finesse", - greenSide: theGreenSide[2].blitz.stats.finessePercentage * 100, - redSide: theRedSide[2].blitz.stats.finessePercentage * 100, - fractionDigits: 2, - postfix: "%", - higherIsBetter: true, - ), - CompareThingy( - label: "Holds", - greenSide: theGreenSide[2].blitz.stats.holds, - redSide: theRedSide[2].blitz.stats.holds, - higherIsBetter: false, - ), - CompareThingy( - label: "T-spins", - greenSide: theGreenSide[2].blitz.stats.tSpins, - redSide: theRedSide[2].blitz.stats.tSpins, - higherIsBetter: false, - ), - CompareThingy( - label: "Quads", - greenSide: theGreenSide[2].blitz.stats.clears.quads, - redSide: theRedSide[2].blitz.stats.clears.quads, - higherIsBetter: true, - ), - CompareThingy( - label: "PC's", - greenSide: theGreenSide[2].blitz.stats.clears.allClears, - redSide: theRedSide[2].blitz.stats.clears.allClears, - higherIsBetter: true, - ), - ], - ) - else if (greenSideMode == Mode.player && redSideMode == Mode.player) CompareBoolThingy(greenSide: theGreenSide[2].blitz != null, redSide: theRedSide[2].blitz != null, label: "Played Blitz", trueIsBetter: true), - if (greenSideMode == Mode.player && redSideMode == Mode.player) Column( - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ), - CompareThingy( - label: "Level", - greenSide: theGreenSide[2].zen.level, - redSide: theRedSide[2].zen.level, - higherIsBetter: true, - ), - CompareThingy( - label: "Score", - greenSide: theGreenSide[2].zen.score, - redSide: theRedSide[2].zen.score, - higherIsBetter: true, - ), - ], - ) - ], - ) - else Padding( - padding: const EdgeInsets.all(8.0), - child: Text(t.compareViewNoValues(avgR: "\$avgR"), textAlign: TextAlign.center), - ) - ], - ), - ), - ), - ), - ); - } -} - -class PlayerSelector extends StatelessWidget { - final List data; - final Mode mode; - final Function fetch; - final Function change; - final Function updateState; - const PlayerSelector( - {super.key, - required this.data, - required this.mode, - required this.updateState, - required this.fetch, - required this.change}); - - @override - Widget build(BuildContext context) { - final TextEditingController playerController = TextEditingController(); - String underFieldString = ""; - if (!listEquals(data, [null, null, null])){ - switch (mode){ - case Mode.player: - playerController.text = data[0] != null ? data[0].username : ""; - break; - case Mode.stats: - playerController.text = "${data[2].league.apm} ${data[2].league.pps} ${data[2].league.vs}"; - break; - case Mode.averages: - playerController.text = "\$avg${data[2].league.rank.toUpperCase()}"; - break; - } - } - if (!listEquals(data, [null, null, null])){ - switch (mode){ - case Mode.player: - underFieldString = data[0] != null ? data[0].toString() : "???"; - break; - case Mode.stats: - underFieldString = "${data[2].league.apm} APM, ${data[2].league.pps} PPS, ${data[2].league.vs} VS"; - break; - case Mode.averages: - underFieldString = t.averageXrank(rankLetter: data[2].league.rank.toUpperCase()); - break; - } - } - return Column( - children: [ - TextField( - autocorrect: false, - enableSuggestions: false, - maxLength: 25, - controller: playerController, - decoration: const InputDecoration(counter: Offstage()), - onSubmitted: (String value) { - underFieldString = "Fetching..."; - fetch(value); - }), - if (data[0] != null && data[1] != null) - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: DropdownButton( - items: data[1], - value: data[0], - onChanged: (value) => change(value!), - ), - ) - else Text( - underFieldString, - style: const TextStyle( - shadows: [ - 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, - ), - ], - ), - ), - ], - ); - } -} - -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( - 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( - 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, - ), - )), - ], - ), - ); - } -} - -class CompareBoolThingy extends StatelessWidget { - final bool greenSide; - final bool redSide; - final String label; - final bool trueIsBetter; - const CompareBoolThingy( - {super.key, - required this.greenSide, - required this.redSide, - required this.label, - required this.trueIsBetter}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 2, 16, 2), - child: Row(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, - stops: [ - 0.0, - trueIsBetter - ? greenSide - ? 0.6 - : 0 - : !greenSide - ? 0.6 - : 0 - ], - )), - child: Text( - greenSide ? t.yes : t.no, - style: const TextStyle( - fontSize: 22, - shadows: [ - 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.start, - ), - )), - Column( - children: [ - Text( - label, - style: const TextStyle(fontSize: 22), - textAlign: TextAlign.center, - ), - const Text("---", 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, - stops: [ - 0.0, - trueIsBetter - ? redSide - ? 0.6 - : 0 - : !redSide - ? 0.6 - : 0 - ], - )), - child: Text( - redSide ? t.yes : t.no, - style: const TextStyle( - fontSize: 22, - shadows: [ - 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, - ), - )), - ]), - ); - } -} - -class CompareDurationThingy extends StatelessWidget { - final Duration greenSide; - final Duration redSide; - final String label; - final bool higherIsBetter; - const CompareDurationThingy( - {super.key, - required this.greenSide, - required this.redSide, - required this.label, - required this.higherIsBetter}); - - Duration verdict(Duration greenSide, Duration redSide) { - return greenSide - redSide; - } - - @override - Widget build(BuildContext context) { - 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(get40lTime(greenSide.inMicroseconds), style: const TextStyle( - fontSize: 22, - shadows: [ - 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.start) - )), - Column( - children: [ - Text( - label, - style: const TextStyle( - fontSize: 22, - shadows: [ - 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.center, - ), - Text( - verdict(greenSide, redSide).toString(), 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(get40lTime(redSide.inMicroseconds), style: const TextStyle( - fontSize: 22, - shadows: [ - 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) - )), - ], - ), - ); - } -} - -class CompareRegTimeThingy extends StatelessWidget { - final DateTime? greenSide; - final DateTime? redSide; - final String label; - final int? fractionDigits; - const CompareRegTimeThingy( - {super.key, - required this.greenSide, - required this.redSide, - required this.label, - this.fractionDigits}); - - String verdict(DateTime? greenSide, DateTime? redSide) { - var f = NumberFormat("#,### ${t.daysLater};#,### ${t.dayseBefore}"); - String result = "---"; - if (greenSide != null && redSide != null) { - result = f.format(greenSide.difference(redSide).inDays); - } - return result; - } - - @override - Widget build(BuildContext context) { - DateFormat f = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode); - 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, - stops: [ - 0.0, - greenSide == null - ? 0.6 - : redSide != null && greenSide!.isBefore(redSide!) - ? 0.6 - : 0 - ], - )), - child: Text( - greenSide != null ? f.format(greenSide!) : t.fromBeginning, - style: const TextStyle( - fontSize: 22, - shadows: [ - 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.start, - ), - )), - Column( - children: [ - Text( - label, - style: const TextStyle(fontSize: 22), - textAlign: TextAlign.center, - ), - Text(verdict(greenSide, redSide), 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, - stops: [ - 0.0, - redSide == null - ? 0.6 - : greenSide != null && redSide!.isBefore(greenSide!) - ? 0.6 - : 0 - ], - )), - child: Text( - redSide != null ? f.format(redSide!) : t.fromBeginning, - style: const TextStyle( - fontSize: 22, - shadows: [ - 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, - ), - )), - ], - ), - ); - } -} diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart new file mode 100644 index 0000000..9f2ed70 --- /dev/null +++ b/lib/views/compare_view_tiles.dart @@ -0,0 +1,1371 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; +import 'dart:math'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'dart:developer' as developer; +import 'package:tetra_stats/data_objects/aggregate_stats.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/summaries.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; +import 'package:tetra_stats/data_objects/tetrio_player.dart'; +import 'package:tetra_stats/data_objects/tetrio_zen.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart' show teto; +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:transparent_image/transparent_image.dart'; +import 'package:window_manager/window_manager.dart'; + +enum Mode{ + player, + stats, + averages +} +final DateFormat dateFormat = DateFormat.yMd(LocaleSettings.currentLocale.languageCode).add_Hm(); +var numbersReg = RegExp(r'\d+(\.\d*)*'); +late String oldWindowTitle; + +class CompareView extends StatefulWidget { + final TetrioPlayer initPlayer; + const CompareView(this.initPlayer); + + @override + State createState() => CompareState(); +} + +class CompareState extends State { + late ScrollController _scrollController; + bool tlOnly = false; + List players = []; + List summaries = []; + List nicknames = []; + Map> TitesForStats = { + t.general: [ + t.stats.registrationDate, + t.stats.xp.short, + t.stats.gametime, + t.stats.ogp, + t.stats.ogw, + t.stats.followers, + ], + t.gamemodes["league"]!: [ + t.stats.tr.full, + t.stats.glicko.full, + t.stats.rd.full, + t.stats.glixare.full, + t.stats.s1tr.full, + t.stats.placement, + t.stats.gp.full, + t.stats.gw.full, + t.stats.winrate.full, + t.stats.apm.full, + t.stats.pps.full, + t.stats.vs.full, + t.nerdStats, + t.stats.app.full, + t.stats.vsapm.full, + t.stats.dss.full, + t.stats.dsp.full, + t.stats.appdsp.full, + t.stats.cheese.full, + t.stats.gbe.full, + t.stats.nyaapp.full, + t.stats.area.full, + t.playstyles, + t.stats.opener.full, + t.stats.plonk.full, + t.stats.stride.full, + t.stats.infds.full + ], + t.gamemodes["zenith"]!:[ + t.stats.altitude.full, + t.stats.placement, + t.stats.apm.full, + t.stats.pps.full, + t.stats.vs.full, + t.stats.kos.full, + t.stats.b2b.full, + t.stats.climbSpeed.full, + t.stats.peakClimbSpeed.full, + t.stats.totalTime.full, + t.stats.finesse.full, + t.nerdStats, + t.stats.app.full, + t.stats.vsapm.full, + t.stats.dss.full, + t.stats.dsp.full, + t.stats.appdsp.full, + t.stats.cheese.full, + t.stats.gbe.full, + t.stats.nyaapp.full, + t.stats.area.full, + t.playstyles, + t.stats.opener.full, + t.stats.plonk.full, + t.stats.stride.full, + t.stats.infds.full + ], + t.gamemodes["zenithex"]!:[ + t.stats.altitude.full, + t.stats.placement, + t.stats.apm.full, + t.stats.pps.full, + t.stats.vs.full, + t.stats.kos.full, + t.stats.b2b.full, + t.stats.climbSpeed.full, + t.stats.peakClimbSpeed.full, + t.stats.totalTime.full, + t.stats.finesse.full, + t.nerdStats, + t.stats.app.full, + t.stats.vsapm.full, + t.stats.dss.full, + t.stats.dsp.full, + t.stats.appdsp.full, + t.stats.cheese.full, + t.stats.gbe.full, + t.stats.nyaapp.full, + t.stats.area.full, + t.playstyles, + t.stats.opener.full, + t.stats.plonk.full, + t.stats.stride.full, + t.stats.infds.full + ], + t.gamemodes["40l"]!: [ + t.stats.totalTime.short, + t.stats.pieces.full, + t.stats.kp.full, + t.stats.kpp.full, + t.stats.pps.full, + t.stats.kps.full, + t.stats.finesse.full, + t.stats.finesseFaults.full, + "", + t.stats.lineClears.quad, + t.stats.lineClears.triple, + t.stats.lineClears.double, + t.stats.lineClears.single, + "", + "${t.stats.tSpins} ${t.stats.lineClears.triple}", + "${t.stats.tSpins} ${t.stats.lineClears.double}", + "${t.stats.tSpins} ${t.stats.lineClears.single}", + "${t.stats.tSpins} ${t.stats.lineClears.zero}", + "${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.double}", + "${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.single}", + "${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.zero}" + ], + t.gamemodes["blitz"]!: [ + t.stats.score, + t.stats.pieces.full, + t.stats.lines, + t.stats.level.full, + t.stats.kp.full, + t.stats.kpp.full, + t.stats.pps.full, + t.stats.kps.full, + t.stats.finesse.full, + t.stats.finesseFaults.full, + "", + t.stats.lineClears.quad, + t.stats.lineClears.triple, + t.stats.lineClears.double, + t.stats.lineClears.single, + "", + "${t.stats.tSpins} ${t.stats.lineClears.triple}", + "${t.stats.tSpins} ${t.stats.lineClears.double}", + "${t.stats.tSpins} ${t.stats.lineClears.single}", + "${t.stats.tSpins} ${t.stats.lineClears.zero}", + "${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.double}", + "${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.single}", + "${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.zero}" + ], + t.gamemodes["zen"]!: [ + t.stats.score, + t.stats.level.full + ] + }; + List>> rawValues = [[],[],[],[],[],[],[]]; + List>> formattedValues = [[],[],[],[],[],[],[]]; //formattedValues[category][player][stat] + List> best = []; + TextStyle _expansionTileTitleTextStyle = TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24.0); + + @override + void initState() { + _scrollController = ScrollController(); + players = [widget.initPlayer]; + getSummariesForInit(); + 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(); + } + + addvaluesEntrys(TetrioPlayer p, Summaries s){ + RecordSingle? zenithRun = s.zenith != null ? s.zenith : s.zenithCareerBest; + bool oldZenithRun = s.zenith == null; + RecordSingle? zenithExRun = s.zenithEx != null ? s.zenithEx : s.zenithExCareerBest; + bool oldZenithExRun = s.zenithEx == null; + rawValues[0].add([ + p.registrationTime, + p.xp, + p.gameTime, + p.gamesPlayed, + p.gamesWon, + p.friendCount + ]); + rawValues[1].add( + [ + s.league.tr, + s.league.glicko, + s.league.rd, + s.league.gxe, + s.league.s1tr, + s.league.standing, + s.league.gamesPlayed, + s.league.gamesWon, + s.league.winrate, + s.league.apm, + s.league.pps, + s.league.vs, + "", + s.league.nerdStats?.app, + s.league.nerdStats?.vsapm, + s.league.nerdStats?.dss, + s.league.nerdStats?.dsp, + s.league.nerdStats?.appdsp, + s.league.nerdStats?.cheese, + s.league.nerdStats?.gbe, + s.league.nerdStats?.nyaapp, + s.league.nerdStats?.area, + "", + s.league.playstyle?.opener, + s.league.playstyle?.plonk, + s.league.playstyle?.stride, + s.league.playstyle?.infds, + ] + ); + rawValues[2].add([ + zenithRun?.stats.zenith?.altitude, + zenithRun?.rank, + zenithRun?.aggregateStats.apm, + zenithRun?.aggregateStats.pps, + zenithRun?.aggregateStats.vs, + zenithRun?.stats.kills, + zenithRun?.stats.topBtB, + zenithRun?.stats.cps, + zenithRun?.stats.zenith?.peakrank, + zenithRun?.stats.finalTime, + zenithRun?.stats.finessePercentage, + "", + zenithRun?.aggregateStats.nerdStats.app, + zenithRun?.aggregateStats.nerdStats.vsapm, + zenithRun?.aggregateStats.nerdStats.dss, + zenithRun?.aggregateStats.nerdStats.dsp, + zenithRun?.aggregateStats.nerdStats.appdsp, + zenithRun?.aggregateStats.nerdStats.cheese, + zenithRun?.aggregateStats.nerdStats.gbe, + zenithRun?.aggregateStats.nerdStats.nyaapp, + zenithRun?.aggregateStats.nerdStats.area, + "", + zenithRun?.aggregateStats.playstyle.opener, + zenithRun?.aggregateStats.playstyle.plonk, + zenithRun?.aggregateStats.playstyle.stride, + zenithRun?.aggregateStats.playstyle.infds, + ]); + rawValues[3].add([ + zenithExRun?.stats.zenith?.altitude, + zenithExRun?.rank, + zenithExRun?.aggregateStats.apm, + zenithExRun?.aggregateStats.pps, + zenithExRun?.aggregateStats.vs, + zenithExRun?.stats.kills, + zenithExRun?.stats.topBtB, + zenithExRun?.stats.cps, + zenithExRun?.stats.zenith?.peakrank, + zenithExRun?.stats.finalTime, + zenithExRun?.stats.finessePercentage, + "", + zenithExRun?.aggregateStats.nerdStats.app, + zenithExRun?.aggregateStats.nerdStats.vsapm, + zenithExRun?.aggregateStats.nerdStats.dss, + zenithExRun?.aggregateStats.nerdStats.dsp, + zenithExRun?.aggregateStats.nerdStats.appdsp, + zenithExRun?.aggregateStats.nerdStats.cheese, + zenithExRun?.aggregateStats.nerdStats.gbe, + zenithExRun?.aggregateStats.nerdStats.nyaapp, + zenithExRun?.aggregateStats.nerdStats.area, + "", + zenithExRun?.aggregateStats.playstyle.opener, + zenithExRun?.aggregateStats.playstyle.plonk, + zenithExRun?.aggregateStats.playstyle.stride, + zenithExRun?.aggregateStats.playstyle.infds, + ]);; + rawValues[4].add([ + s.sprint?.stats.finalTime, + s.sprint?.stats.piecesPlaced, + s.sprint?.stats.inputs, + s.sprint?.stats.kpp, + s.sprint?.stats.pps, + s.sprint?.stats.kps, + s.sprint?.stats.finessePercentage, + s.sprint?.stats.finesse?.faults, + "", + s.sprint?.stats.clears.quads, + s.sprint?.stats.clears.triples, + s.sprint?.stats.clears.doubles, + s.sprint?.stats.clears.singles, + "", + s.sprint?.stats.clears.tSpinTriples, + s.sprint?.stats.clears.tSpinDoubles, + s.sprint?.stats.clears.tSpinSingles, + s.sprint?.stats.clears.tSpinZeros, + s.sprint?.stats.clears.tSpinMiniDoubles, + s.sprint?.stats.clears.tSpinMiniSingles, + s.sprint?.stats.clears.tSpinMiniZeros + ]); + rawValues[5].add( + [ + s.blitz?.stats.score, + s.blitz?.stats.piecesPlaced, + s.blitz?.stats.lines, + s.blitz?.stats.level, + s.blitz?.stats.inputs, + s.blitz?.stats.kpp, + s.blitz?.stats.pps, + s.blitz?.stats.kps, + s.blitz?.stats.finessePercentage, + s.blitz?.stats.finesse?.faults, + "", + s.blitz?.stats.clears.quads, + s.blitz?.stats.clears.triples, + s.blitz?.stats.clears.doubles, + s.blitz?.stats.clears.singles, + "", + s.blitz?.stats.clears.tSpinTriples, + s.blitz?.stats.clears.tSpinDoubles, + s.blitz?.stats.clears.tSpinSingles, + s.blitz?.stats.clears.tSpinZeros, + s.blitz?.stats.clears.tSpinMiniDoubles, + s.blitz?.stats.clears.tSpinMiniSingles, + s.blitz?.stats.clears.tSpinMiniZeros + ] + ); + rawValues[6].add([ + s.zen.score, + s.zen.level + ]); + formattedValues[0].add([ + Text(timestamp(p.registrationTime)), + RichText(text: p.xp.isNegative ? TextSpan(text: "hidden", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.grey)) : TextSpan(text: intf.format(p.xp), style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white), children: [TextSpan(text: " (lvl ${intf.format(p.level.floor())})", style: TextStyle(color: Colors.grey))])), + Text(p.gameTime.isNegative ? "hidden" : playtime(p.gameTime), style: TextStyle(color: p.gameTime.isNegative ? Colors.grey : Colors.white)), + Text(p.gamesPlayed.isNegative ? "hidden" : intf.format(p.gamesPlayed), style: TextStyle(color: p.gamesPlayed.isNegative ? Colors.grey : Colors.white)), + Text(p.gamesWon.isNegative ? "hidden" : intf.format(p.gamesWon), style: TextStyle(color: p.gamesWon.isNegative ? Colors.grey : Colors.white)), + Text(intf.format(p.friendCount)) + ]); + formattedValues[1].add([ + Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr)), + Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko)), + Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd), style: TextStyle(color: s.league.rd!.isNegative ? Colors.grey : Colors.white)), + Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe)), + Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr)), + Text(s.league.standing.isNegative ? "---" : "№ "+intf.format(s.league.standing)), + Text(intf.format(s.league.gamesPlayed)), + Text(intf.format(s.league.gamesWon)), + Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%"), + Text(s.league.apm != null ? f2.format(s.league.apm) : "---"), + Text(s.league.pps != null ? f2.format(s.league.pps) : "---"), + Text(s.league.vs != null ? f2.format(s.league.vs) : "---"), + Text(""), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---"), + Text(""), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.opener) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.plonk) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.stride) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.infds) : "---"), + ]); + formattedValues[2].add([ + RichText(text: TextSpan(text: zenithRun != null ? "${f2.format(zenithRun.stats.zenith!.altitude)} m" : "---", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white), children: [if (zenithRun != null && oldZenithRun) TextSpan(text: " (${zenithRun.revolution})", style: TextStyle(color: Colors.grey))])), + Text(zenithRun != null ? "№ "+intf.format(zenithRun.rank) : "---"), + Text(zenithRun != null ? f2.format(zenithRun.aggregateStats.apm) : "---"), + Text(zenithRun != null ? f2.format(zenithRun.aggregateStats.pps) : "---"), + Text(zenithRun != null ? f2.format(zenithRun.aggregateStats.vs) : "---"), + Text(zenithRun != null ? intf.format(zenithRun.stats.kills) : "---"), + Text(zenithRun != null ? intf.format(zenithRun.stats.topBtB) : "---"), + Text(zenithRun != null ? f4.format(zenithRun.stats.cps) : "---"), + Text(zenithRun != null ? f4.format(zenithRun.stats.zenith!.peakrank) : "---"), + Text(zenithRun != null ? getMoreNormalTime(zenithRun.stats.finalTime) : "---"), + Text(zenithRun != null ? f2.format(zenithRun.stats.finessePercentage*100)+"%" : "---"), + Text(""), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.app) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.vsapm) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.dss) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.dsp) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.appdsp) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.cheese) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.gbe) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.nyaapp) : "---"), + Text(zenithRun?.aggregateStats.nerdStats != null ? f4.format(zenithRun!.aggregateStats.nerdStats.area) : "---"), + Text(""), + Text(zenithRun?.aggregateStats.playstyle != null ? f4.format(zenithRun!.aggregateStats.playstyle.opener) : "---"), + Text(zenithRun?.aggregateStats.playstyle != null ? f4.format(zenithRun!.aggregateStats.playstyle.plonk) : "---"), + Text(zenithRun?.aggregateStats.playstyle != null ? f4.format(zenithRun!.aggregateStats.playstyle.stride) : "---"), + Text(zenithRun?.aggregateStats.playstyle != null ? f4.format(zenithRun!.aggregateStats.playstyle.infds) : "---"), + ]); + formattedValues[3].add([ + RichText(text: TextSpan(text: zenithExRun != null ? "${f2.format(zenithExRun.stats.zenith!.altitude)} m" : "---", style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white), children: [if (zenithExRun != null && oldZenithExRun) TextSpan(text: " (${zenithExRun.revolution})", style: TextStyle(color: Colors.grey))])), + Text(zenithExRun != null ? "№ "+intf.format(zenithExRun.rank) : "---"), + Text(zenithExRun != null ? f2.format(zenithExRun.aggregateStats.apm) : "---"), + Text(zenithExRun != null ? f2.format(zenithExRun.aggregateStats.pps) : "---"), + Text(zenithExRun != null ? f2.format(zenithExRun.aggregateStats.vs) : "---"), + Text(zenithExRun != null ? intf.format(zenithExRun.stats.kills) : "---"), + Text(zenithExRun != null ? intf.format(zenithExRun.stats.topBtB) : "---"), + Text(zenithExRun != null ? f4.format(zenithExRun.stats.cps) : "---"), + Text(zenithExRun != null ? f4.format(zenithExRun.stats.zenith!.peakrank) : "---"), + Text(zenithExRun != null ? getMoreNormalTime(zenithExRun.stats.finalTime) : "---"), + Text(zenithExRun != null ? f2.format(zenithExRun.stats.finessePercentage*100)+"%" : "---"), + Text(""), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.app) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.vsapm) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.dss) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.dsp) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.appdsp) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.cheese) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.gbe) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.nyaapp) : "---"), + Text(zenithExRun?.aggregateStats.nerdStats != null ? f4.format(zenithExRun!.aggregateStats.nerdStats.area) : "---"), + Text(""), + Text(zenithExRun?.aggregateStats.playstyle != null ? f4.format(zenithExRun!.aggregateStats.playstyle.opener) : "---"), + Text(zenithExRun?.aggregateStats.playstyle != null ? f4.format(zenithExRun!.aggregateStats.playstyle.plonk) : "---"), + Text(zenithExRun?.aggregateStats.playstyle != null ? f4.format(zenithExRun!.aggregateStats.playstyle.stride) : "---"), + Text(zenithExRun?.aggregateStats.playstyle != null ? f4.format(zenithExRun!.aggregateStats.playstyle.infds) : "---"), + ]); + formattedValues[4].add([ + Text(s.sprint != null ? getMoreNormalTime(s.sprint!.stats.finalTime) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.piecesPlaced) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.inputs) : "---"), + Text(s.sprint != null ? f4.format(s.sprint!.stats.kpp) : "---"), + Text(s.sprint != null ? f4.format(s.sprint!.stats.pps) : "---"), + Text(s.sprint != null ? f4.format(s.sprint!.stats.kps) : "---"), + Text(s.sprint != null ? percentage.format(s.sprint!.stats.finessePercentage) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.finesse?.faults) : "---"), + Text(""), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.quads) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.triples) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.doubles) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.singles) : "---"), + Text(""), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.tSpinTriples) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.tSpinDoubles) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.tSpinSingles) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.tSpinZeros) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.tSpinMiniDoubles) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.tSpinMiniSingles) : "---"), + Text(s.sprint != null ? intf.format(s.sprint!.stats.clears.tSpinMiniZeros) : "---"), + ]); + formattedValues[5].add([ + Text(s.blitz != null ? intf.format(s.blitz!.stats.score) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.piecesPlaced) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.lines) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.level) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.inputs) : "---"), + Text(s.blitz != null ? f4.format(s.blitz!.stats.kpp) : "---"), + Text(s.blitz != null ? f4.format(s.blitz!.stats.pps) : "---"), + Text(s.blitz != null ? f4.format(s.blitz!.stats.kps) : "---"), + Text(s.blitz != null ? percentage.format(s.blitz!.stats.finessePercentage) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.finesse?.faults) : "---"), + Text(""), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.quads) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.triples) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.doubles) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.singles) : "---"), + Text(""), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.tSpinTriples) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.tSpinDoubles) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.tSpinSingles) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.tSpinZeros) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.tSpinMiniDoubles) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.tSpinMiniSingles) : "---"), + Text(s.blitz != null ? intf.format(s.blitz!.stats.clears.tSpinMiniZeros) : "---"), + ]); + formattedValues[6].add([ + Text(intf.format(s.zen.score)), + Text(intf.format(s.zen.level)) + ]); + } + + addAverages(Summaries s){ + rawValues[1].add( + [ + s.league.tr, + s.league.glicko, + s.league.rd, + s.league.gxe, + s.league.s1tr, + s.league.standing, + s.league.gamesPlayed, + s.league.gamesWon, + s.league.winrate, + s.league.apm, + s.league.pps, + s.league.vs, + "", + s.league.nerdStats?.app, + s.league.nerdStats?.vsapm, + s.league.nerdStats?.dss, + s.league.nerdStats?.dsp, + s.league.nerdStats?.appdsp, + s.league.nerdStats?.cheese, + s.league.nerdStats?.gbe, + s.league.nerdStats?.nyaapp, + s.league.nerdStats?.area, + "", + s.league.playstyle?.opener, + s.league.playstyle?.plonk, + s.league.playstyle?.stride, + s.league.playstyle?.infds, + ] + ); + formattedValues[1].add([ + Text(s.league.tr.isNegative ? "---" : f4.format(s.league.tr)), + Text(s.league.glicko!.isNegative ? "---" : f4.format(s.league.glicko)), + Text(s.league.rd!.isNegative ? "---" : f4.format(s.league.rd), style: TextStyle(color: s.league.rd!.isNegative ? Colors.grey : Colors.white)), + Text(s.league.gxe.isNegative ? "---" : f4.format(s.league.gxe)), + Text(s.league.s1tr.isNegative ? "---" : f4.format(s.league.s1tr)), + Text(s.league.standing.isNegative ? "---" : "№ "+intf.format(s.league.standing)), + Text(intf.format(s.league.gamesPlayed)), + Text(intf.format(s.league.gamesWon)), + Text(s.league.winrate.isNaN ? "---" : f4.format(s.league.winrate*100)+"%"), + Text(s.league.apm != null ? f2.format(s.league.apm) : "---"), + Text(s.league.pps != null ? f2.format(s.league.pps) : "---"), + Text(s.league.vs != null ? f2.format(s.league.vs) : "---"), + Text(""), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.app) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.vsapm) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dss) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.dsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.appdsp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.cheese) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.gbe) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.nyaapp) : "---"), + Text(s.league.nerdStats != null ? f4.format(s.league.nerdStats!.area) : "---"), + Text(""), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.opener) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.plonk) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.stride) : "---"), + Text(s.league.playstyle != null ? f4.format(s.league.playstyle!.infds) : "---"), + ]); + } + + List> recalculateBestEntries(){ + return [ + [ + players.reduce((curr, next) => curr.registrationTime.isBefore(next.registrationTime) ? curr : next).registrationTime, + players.reduce((curr, next) => curr.xp > next.xp ? curr : next).xp, + players.reduce((curr, next) => curr.gameTime.compareTo(next.gameTime) > 0 ? curr : next).gameTime, + players.reduce((curr, next) => curr.gamesPlayed > next.gamesPlayed ? curr : next).gamesPlayed, + players.reduce((curr, next) => curr.gamesWon > next.gamesWon ? curr : next).gamesWon, + players.reduce((curr, next) => curr.friendCount > next.friendCount ? curr : next).friendCount, + ], + [ + summaries.reduce((curr, next) => curr.league.tr > next.league.tr ? curr : next).league.tr, + summaries.reduce((curr, next) => (curr.league.glicko??-1) > (next.league.glicko??-1) ? curr : next).league.glicko, + summaries.reduce((curr, next) => (curr.league.rd??-1) < (next.league.rd??-1) ? curr : next).league.rd, + summaries.reduce((curr, next) => curr.league.gxe > next.league.gxe ? curr : next).league.gxe, + summaries.reduce((curr, next) => curr.league.s1tr > next.league.s1tr ? curr : next).league.s1tr, + summaries.reduce((curr, next) => curr.league.standing > next.league.standing ? curr : next).league.standing, + summaries.reduce((curr, next) => curr.league.gamesPlayed > next.league.gamesPlayed ? curr : next).league.gamesPlayed, + summaries.reduce((curr, next) => curr.league.gamesWon > next.league.gamesWon ? curr : next).league.gamesWon, + summaries.reduce((curr, next) => curr.league.winrate > next.league.winrate ? curr : next).league.winrate, + summaries.reduce((curr, next) => (curr.league.apm??0) > (next.league.apm??0) ? curr : next).league.apm, + summaries.reduce((curr, next) => (curr.league.pps??0) > (next.league.pps??0) ? curr : next).league.pps, + summaries.reduce((curr, next) => (curr.league.vs??0) > (next.league.vs??0) ? curr : next).league.vs, + null, + summaries.reduce((curr, next) => (curr.league.nerdStats?.app??0) > (next.league.nerdStats?.app??0) ? curr : next).league.nerdStats?.app, + summaries.reduce((curr, next) => (curr.league.nerdStats?.vsapm??0) > (next.league.nerdStats?.vsapm??0) ? curr : next).league.nerdStats?.vsapm, + summaries.reduce((curr, next) => (curr.league.nerdStats?.dss??0) > (next.league.nerdStats?.dss??0) ? curr : next).league.nerdStats?.dss, + summaries.reduce((curr, next) => (curr.league.nerdStats?.dsp??0) > (next.league.nerdStats?.dsp??0) ? curr : next).league.nerdStats?.dsp, + summaries.reduce((curr, next) => (curr.league.nerdStats?.appdsp??0) > (next.league.nerdStats?.appdsp??0) ? curr : next).league.nerdStats?.appdsp, + summaries.reduce((curr, next) => (curr.league.nerdStats?.cheese??double.negativeInfinity) > (next.league.nerdStats?.cheese??double.negativeInfinity) ? curr : next).league.nerdStats?.cheese, + summaries.reduce((curr, next) => (curr.league.nerdStats?.gbe??0) > (next.league.nerdStats?.gbe??0) ? curr : next).league.nerdStats?.gbe, + summaries.reduce((curr, next) => (curr.league.nerdStats?.nyaapp??0) > (next.league.nerdStats?.nyaapp??0) ? curr : next).league.nerdStats?.nyaapp, + summaries.reduce((curr, next) => (curr.league.nerdStats?.area??0) > (next.league.nerdStats?.area??0) ? curr : next).league.nerdStats?.area, + null, + summaries.reduce((curr, next) => (curr.league.playstyle?.opener??double.negativeInfinity) > (next.league.playstyle?.opener??double.negativeInfinity) ? curr : next).league.playstyle?.opener, + summaries.reduce((curr, next) => (curr.league.playstyle?.plonk??double.negativeInfinity) > (next.league.playstyle?.plonk??double.negativeInfinity) ? curr : next).league.playstyle?.plonk, + summaries.reduce((curr, next) => (curr.league.playstyle?.stride??double.negativeInfinity) > (next.league.playstyle?.stride??double.negativeInfinity) ? curr : next).league.playstyle?.stride, + summaries.reduce((curr, next) => (curr.league.playstyle?.infds??double.negativeInfinity) > (next.league.playstyle?.infds??double.negativeInfinity) ? curr : next).league.playstyle?.infds + ], + [ + summaries.reduce((curr, next) => (curr.zenith?.stats.zenith?.altitude??-1) > (next.zenith?.stats.zenith?.altitude??-1) ? curr : next).zenith?.stats.zenith?.altitude??-1, + summaries.reduce((curr, next) => (curr.zenith?.rank??-1) > (next.zenith?.rank??-1) ? curr : next).zenith?.rank, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.apm??-1) > (next.zenith?.aggregateStats.apm??-1) ? curr : next).zenith?.aggregateStats.apm, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.pps??-1) > (next.zenith?.aggregateStats.pps??-1) ? curr : next).zenith?.aggregateStats.pps, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.vs??-1) > (next.zenith?.aggregateStats.vs??-1) ? curr : next).zenith?.aggregateStats.vs, + summaries.reduce((curr, next) => (curr.zenith?.stats.kills??-1) > (next.zenith?.stats.kills??-1) ? curr : next).zenith?.stats.kills, + summaries.reduce((curr, next) => (curr.zenith?.stats.topBtB??-1) > (next.zenith?.stats.topBtB??-1) ? curr : next).zenith?.stats.topBtB, + summaries.reduce((curr, next) => (curr.zenith?.stats.cps??-1) > (next.zenith?.stats.cps??-1) ? curr : next).zenith?.stats.cps, + summaries.reduce((curr, next) => (curr.zenith?.stats.zenith?.peakrank??-1) > (next.zenith?.stats.zenith?.peakrank??-1) ? curr : next).zenith?.stats.zenith?.peakrank, + summaries.reduce((curr, next) => (curr.zenith?.stats.finalTime != null) ? curr.zenith!.stats.finalTime.compareTo(next.zenith?.stats.finalTime??Duration.zero) > 1 ? curr : next : next).zenith?.stats.finalTime, + summaries.reduce((curr, next) => (curr.zenith?.stats.finessePercentage??-1) > (next.zenith?.stats.finessePercentage??-1) ? curr : next).zenith?.stats.finessePercentage, + null, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.app??0) > (next.zenith?.aggregateStats.nerdStats.app??0) ? curr : next).zenith?.aggregateStats.nerdStats.app, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.vsapm??0) > (next.zenith?.aggregateStats.nerdStats.vsapm??0) ? curr : next).zenith?.aggregateStats.nerdStats.vsapm, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.dss??0) > (next.zenith?.aggregateStats.nerdStats.dss??0) ? curr : next).zenith?.aggregateStats.nerdStats.dss, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.dsp??0) > (next.zenith?.aggregateStats.nerdStats.dsp??0) ? curr : next).zenith?.aggregateStats.nerdStats.dsp, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.appdsp??0) > (next.zenith?.aggregateStats.nerdStats.appdsp??0) ? curr : next).zenith?.aggregateStats.nerdStats.appdsp, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.cheese??double.negativeInfinity) > (next.zenith?.aggregateStats.nerdStats.cheese??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.nerdStats.cheese, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.gbe??0) > (next.zenith?.aggregateStats.nerdStats.gbe??0) ? curr : next).zenith?.aggregateStats.nerdStats.gbe, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.nyaapp??0) > (next.zenith?.aggregateStats.nerdStats.nyaapp??0) ? curr : next).zenith?.aggregateStats.nerdStats.nyaapp, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.nerdStats.area??0) > (next.zenith?.aggregateStats.nerdStats.area??0) ? curr : next).zenith?.aggregateStats.nerdStats.area, + null, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.opener??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.opener??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.opener, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.plonk??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.plonk??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.plonk, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.stride??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.stride??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.stride, + summaries.reduce((curr, next) => (curr.zenith?.aggregateStats.playstyle.infds??double.negativeInfinity) > (next.zenith?.aggregateStats.playstyle.infds??double.negativeInfinity) ? curr : next).zenith?.aggregateStats.playstyle.infds + ], + [ + summaries.reduce((curr, next) => (curr.zenithEx?.stats.zenith?.altitude??-1) > (next.zenithEx?.stats.zenith?.altitude??-1) ? curr : next).zenithEx?.stats.zenith?.altitude??-1, + summaries.reduce((curr, next) => (curr.zenithEx?.rank??-1) > (next.zenithEx?.rank??-1) ? curr : next).zenithEx?.rank, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.apm??-1) > (next.zenithEx?.aggregateStats.apm??-1) ? curr : next).zenithEx?.aggregateStats.apm, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.pps??-1) > (next.zenithEx?.aggregateStats.pps??-1) ? curr : next).zenithEx?.aggregateStats.pps, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.vs??-1) > (next.zenithEx?.aggregateStats.vs??-1) ? curr : next).zenithEx?.aggregateStats.vs, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.kills??-1) > (next.zenithEx?.stats.kills??-1) ? curr : next).zenithEx?.stats.kills, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.topBtB??-1) > (next.zenithEx?.stats.topBtB??-1) ? curr : next).zenithEx?.stats.topBtB, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.cps??-1) > (next.zenithEx?.stats.cps??-1) ? curr : next).zenithEx?.stats.cps, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.zenith?.peakrank??-1) > (next.zenithEx?.stats.zenith?.peakrank??-1) ? curr : next).zenithEx?.stats.zenith?.peakrank, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.finalTime != null) ? curr.zenithEx!.stats.finalTime.compareTo(next.zenithEx?.stats.finalTime??Duration.zero) > 1 ? curr : next : next).zenithEx?.stats.finalTime, + summaries.reduce((curr, next) => (curr.zenithEx?.stats.finessePercentage??-1) > (next.zenithEx?.stats.finessePercentage??-1) ? curr : next).zenithEx?.stats.finessePercentage, + null, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.app??0) > (next.zenithEx?.aggregateStats.nerdStats.app??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.app, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.vsapm??0) > (next.zenithEx?.aggregateStats.nerdStats.vsapm??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.vsapm, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.dss??0) > (next.zenithEx?.aggregateStats.nerdStats.dss??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.dss, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.dsp??0) > (next.zenithEx?.aggregateStats.nerdStats.dsp??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.dsp, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.appdsp??0) > (next.zenithEx?.aggregateStats.nerdStats.appdsp??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.appdsp, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.cheese??double.negativeInfinity) > (next.zenithEx?.aggregateStats.nerdStats.cheese??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.nerdStats.cheese, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.gbe??0) > (next.zenithEx?.aggregateStats.nerdStats.gbe??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.gbe, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.nyaapp??0) > (next.zenithEx?.aggregateStats.nerdStats.nyaapp??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.nyaapp, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.nerdStats.area??0) > (next.zenithEx?.aggregateStats.nerdStats.area??0) ? curr : next).zenithEx?.aggregateStats.nerdStats.area, + null, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.opener??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.opener??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.opener, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.plonk??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.plonk??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.plonk, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.stride??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.stride??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.stride, + summaries.reduce((curr, next) => (curr.zenithEx?.aggregateStats.playstyle.infds??double.negativeInfinity) > (next.zenithEx?.aggregateStats.playstyle.infds??double.negativeInfinity) ? curr : next).zenithEx?.aggregateStats.playstyle.infds + ], + [ + summaries.reduce((curr, next) => (curr.sprint?.stats.finalTime != null) ? curr.sprint!.stats.finalTime.compareTo(next.sprint?.stats.finalTime??Duration.zero) < 1 ? curr : next : next).sprint?.stats.finalTime, + summaries.reduce((curr, next) => (curr.sprint?.stats.piecesPlaced??-1) < (next.sprint?.stats.piecesPlaced??-1) ? curr : next).sprint?.stats.piecesPlaced, + summaries.reduce((curr, next) => (curr.sprint?.stats.inputs??-1) < (next.sprint?.stats.inputs??-1) ? curr : next).sprint?.stats.inputs, + summaries.reduce((curr, next) => (curr.sprint?.stats.kpp??-1) < (next.sprint?.stats.kpp??-1) ? curr : next).sprint?.stats.kpp, + summaries.reduce((curr, next) => (curr.sprint?.stats.pps??-1) > (next.sprint?.stats.pps??-1) ? curr : next).sprint?.stats.pps, + summaries.reduce((curr, next) => (curr.sprint?.stats.kps??-1) > (next.sprint?.stats.kps??-1) ? curr : next).sprint?.stats.kps, + summaries.reduce((curr, next) => (curr.sprint?.stats.finessePercentage??-1) > (next.sprint?.stats.finessePercentage??-1) ? curr : next).sprint?.stats.finessePercentage, + summaries.reduce((curr, next) => (curr.sprint?.stats.finesse?.faults??-1) < (next.sprint?.stats.finesse?.faults??-1) ? curr : next).sprint?.stats.finesse?.faults, + null, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.quads??-1) > (next.sprint?.stats.clears.quads??-1) ? curr : next).sprint?.stats.clears.quads, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.triples??-1) > (next.sprint?.stats.clears.triples??-1) ? curr : next).sprint?.stats.clears.triples, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.doubles??-1) > (next.sprint?.stats.clears.doubles??-1) ? curr : next).sprint?.stats.clears.doubles, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.singles??-1) > (next.sprint?.stats.clears.singles??-1) ? curr : next).sprint?.stats.clears.singles, + null, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.tSpinTriples??-1) > (next.sprint?.stats.clears.tSpinTriples??-1) ? curr : next).sprint?.stats.clears.tSpinTriples, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.tSpinDoubles??-1) > (next.sprint?.stats.clears.tSpinDoubles??-1) ? curr : next).sprint?.stats.clears.tSpinDoubles, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.tSpinSingles??-1) > (next.sprint?.stats.clears.tSpinSingles??-1) ? curr : next).sprint?.stats.clears.tSpinSingles, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.tSpinZeros??-1) > (next.sprint?.stats.clears.tSpinZeros??-1) ? curr : next).sprint?.stats.clears.tSpinZeros, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.tSpinMiniDoubles??-1) > (next.sprint?.stats.clears.tSpinMiniDoubles??-1) ? curr : next).sprint?.stats.clears.tSpinMiniDoubles, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.tSpinMiniSingles??-1) > (next.sprint?.stats.clears.tSpinMiniSingles??-1) ? curr : next).sprint?.stats.clears.tSpinMiniSingles, + summaries.reduce((curr, next) => (curr.sprint?.stats.clears.tSpinMiniZeros??-1) > (next.sprint?.stats.clears.tSpinMiniZeros??-1) ? curr : next).sprint?.stats.clears.tSpinMiniZeros, + ], + [ + summaries.reduce((curr, next) => (curr.blitz?.stats.score??-1) > (next.blitz?.stats.score??-1) ? curr : next).blitz?.stats.score, + summaries.reduce((curr, next) => (curr.blitz?.stats.piecesPlaced??-1) < (next.blitz?.stats.piecesPlaced??-1) ? curr : next).blitz?.stats.piecesPlaced, + summaries.reduce((curr, next) => (curr.blitz?.stats.lines??-1) < (next.blitz?.stats.lines??-1) ? curr : next).blitz?.stats.lines, + summaries.reduce((curr, next) => (curr.blitz?.stats.level??-1) < (next.blitz?.stats.level??-1) ? curr : next).blitz?.stats.level, + summaries.reduce((curr, next) => (curr.blitz?.stats.inputs??-1) < (next.blitz?.stats.inputs??-1) ? curr : next).blitz?.stats.inputs, + summaries.reduce((curr, next) => (curr.blitz?.stats.kpp??-1) < (next.blitz?.stats.kpp??-1) ? curr : next).blitz?.stats.kpp, + summaries.reduce((curr, next) => (curr.blitz?.stats.pps??-1) > (next.blitz?.stats.pps??-1) ? curr : next).blitz?.stats.pps, + summaries.reduce((curr, next) => (curr.blitz?.stats.kps??-1) > (next.blitz?.stats.kps??-1) ? curr : next).blitz?.stats.kps, + summaries.reduce((curr, next) => (curr.blitz?.stats.finessePercentage??-1) > (next.blitz?.stats.finessePercentage??-1) ? curr : next).blitz?.stats.finessePercentage, + summaries.reduce((curr, next) => (curr.blitz?.stats.finesse?.faults??-1) < (next.blitz?.stats.finesse?.faults??-1) ? curr : next).blitz?.stats.finesse?.faults, + null, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.quads??-1) > (next.blitz?.stats.clears.quads??-1) ? curr : next).blitz?.stats.clears.quads, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.triples??-1) > (next.blitz?.stats.clears.triples??-1) ? curr : next).blitz?.stats.clears.triples, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.doubles??-1) > (next.blitz?.stats.clears.doubles??-1) ? curr : next).blitz?.stats.clears.doubles, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.singles??-1) > (next.blitz?.stats.clears.singles??-1) ? curr : next).blitz?.stats.clears.singles, + null, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.tSpinTriples??-1) > (next.blitz?.stats.clears.tSpinTriples??-1) ? curr : next).blitz?.stats.clears.tSpinTriples, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.tSpinDoubles??-1) > (next.blitz?.stats.clears.tSpinDoubles??-1) ? curr : next).blitz?.stats.clears.tSpinDoubles, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.tSpinSingles??-1) > (next.blitz?.stats.clears.tSpinSingles??-1) ? curr : next).blitz?.stats.clears.tSpinSingles, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.tSpinZeros??-1) > (next.blitz?.stats.clears.tSpinZeros??-1) ? curr : next).blitz?.stats.clears.tSpinZeros, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.tSpinMiniDoubles??-1) > (next.blitz?.stats.clears.tSpinMiniDoubles??-1) ? curr : next).blitz?.stats.clears.tSpinMiniDoubles, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.tSpinMiniSingles??-1) > (next.blitz?.stats.clears.tSpinMiniSingles??-1) ? curr : next).blitz?.stats.clears.tSpinMiniSingles, + summaries.reduce((curr, next) => (curr.blitz?.stats.clears.tSpinMiniZeros??-1) > (next.blitz?.stats.clears.tSpinMiniZeros??-1) ? curr : next).blitz?.stats.clears.tSpinMiniZeros, + ], + [ + summaries.reduce((curr, next) => curr.zen.score > next.zen.score ? curr : next).zen.score, + summaries.reduce((curr, next) => curr.zen.level > next.zen.level ? curr : next).zen.level, + ] + ]; + } + + void getSummariesForInit() async { + summaries.add(await teto.fetchSummaries(widget.initPlayer.userId)); + nicknames.add(players[0].username); + addvaluesEntrys(players.first, summaries.first); + best = recalculateBestEntries(); + setState(() { + + }); + } + + Future addPlayer(String nickname) async { + try { + if (nickname.startsWith("\$avg")){ + await addRankAverages(nickname.substring(4).toLowerCase()); + return null; + }else{ + TetrioPlayer player = await teto.fetchPlayer(nickname); + Summaries summary = await teto.fetchSummaries(player.userId); + players.add(player); + summaries.add(summary); + addvaluesEntrys(players.last, summaries.last); + nicknames.add(players.last.username); + } + } on Exception catch (e) { + developer.log("Failed to add player:", error: e); + return e; + } + best = recalculateBestEntries(); + setState(() { + + }); + return null; + } + + Future addRankAverages(String rank) async { + try{ + var average = (await teto.fetchTLLeaderboard()).getRankData(rank)[0]; + Summaries summary = Summaries("avg${rank.toUpperCase()}", average, TetrioZen(level: 0, score: 0)); + players.add(TetrioPlayer( + userId: "avg${rank}", + username: "Avg ${rank.toUpperCase()} rank", + role: "rank", + state: summary.league.timestamp, + registrationTime: summary.league.timestamp, + badges: [], + friendCount: -1, + gamesPlayed: -1, + gamesWon: -1, + gameTime: Duration(seconds: -1), + xp: -1, + supporterTier: 0, + verified: false, + connections: null + )); + summaries.add(summary); + nicknames.add("Avg ${rank.toUpperCase()} rank"); + addAverages(summary); + return setState(() {tlOnly = true;}); + }on Exception { + //if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.snackBarMessages.compareViewWrongValue(value: rank)))); + return; + } + } + + void removePlayer(String id) async { + int p = players.indexWhere((e) => e.userId == id); + players.removeAt(p); + summaries.removeAt(p); + nicknames.removeAt(p); + if (id.startsWith("avg")){ + rawValues[1].removeAt(p); + formattedValues[1].removeAt(p); + }else + for (int i = 0; i < 7; i++){ + rawValues[i].removeAt(p); + formattedValues[i].removeAt(p); + } + if (players.isNotEmpty) best = recalculateBestEntries(); + setState(() { + if (players.any((e) => e.userId.startsWith("avg")) == false) tlOnly = false; + }); + } + + double getWinrateByTR(double yourGlicko, double yourRD, double notyourGlicko,double notyourRD) { + return ((1 / + (1 + pow(10, + (notyourGlicko - yourGlicko) / + (400 * sqrt(1 + (3 * pow(0.0057564273, 2) * + (pow(yourRD, 2) + pow(notyourRD, 2)) / pow(pi, 2) + ))) + ) + ) + )); + } + + // void _justUpdate() { + // setState(() {}); + // } + + @override + Widget build(BuildContext context) { + // final t = Translations.of(context); + // bool bigScreen = MediaQuery.of(context).size.width > 768; + return Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + floatingActionButton: Padding( + padding: const EdgeInsets.all(16.0), + child: FloatingActionButton( + onPressed: () => Navigator.pop(context), + tooltip: t.goBackButton, + child: const Icon(Icons.arrow_back), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + height: 175.0, + width: 300.0, + child: Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(18.0, 120.0, 5.0, 0), + child: Text(t.comparison, style: TextStyle(fontSize: 28)), + ), + ), + ), + for (var p in players) SizedBox( + width: 300.0, + child: HeaderCard(p, removePlayer), + ), + SizedBox(width: 300, child: AddNewColumnCard(addPlayer)) + ] + ), + if (tlOnly) SizedBox( + width: 300+300*summaries.length.toDouble(), + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 300.0, + child: Card( + child: Column(children: [ + for (String title in TitesForStats[TitesForStats.keys.elementAt(1)]!) Text(title), + ]), + ), + ), + for (int k = 0; k < formattedValues[1].length; k++) SizedBox( + width: 300.0, + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int l = 0; l < formattedValues[1][k].length; l++) Container(decoration: (rawValues[1].length > 1 && rawValues[1][k][l] != null && best[1][l] == rawValues[1][k][l]) ? BoxDecoration(boxShadow: [BoxShadow(color: Colors.cyanAccent.withAlpha(96), spreadRadius: 0, blurRadius: 4)]) : null, child: formattedValues[1][k][l]), + ], + ), + ), + ), + ] + ), + VsGraphs(stats: [for (var s in summaries) if (s.league.nerdStats != null) AggregateStats.precalculated(s.league.apm!, s.league.pps!, s.league.vs!, s.league.nerdStats!, s.league.playstyle!)], nicknames: [for (int i = 0; i < summaries.length; i++) if (summaries[i].league.nerdStats != null) nicknames[i]]), + ], + ), + ) + else for (int i = 0; i < formattedValues.length; i++) SizedBox( + width: 300+300*summaries.length.toDouble(), + child: ExpansionTile( + title: Text(TitesForStats.keys.elementAt(i), style: _expansionTileTitleTextStyle), + children: [ + Row( + children: [ + SizedBox( + width: 300.0, + child: Card( + child: Column(children: [ + for (String title in TitesForStats[TitesForStats.keys.elementAt(i)]!) Text(title), + ]), + ), + ), + for (int k = 0; k < formattedValues[i].length; k++) SizedBox( + width: 300.0, + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int l = 0; l < formattedValues[i][k].length; l++) Container(decoration: (rawValues[0].length > 1 && rawValues[i][k][l] != null && best[i][l] == rawValues[i][k][l]) ? BoxDecoration(boxShadow: [BoxShadow(color: Colors.cyanAccent.withAlpha(96), spreadRadius: 0, blurRadius: 4)]) : null, child: formattedValues[i][k][l]), + ], + ), + ), + ), + ] + ), + if (i == 1) VsGraphs(stats: [for (var s in summaries) if (s.league.nerdStats != null) AggregateStats.precalculated(s.league.apm!, s.league.pps!, s.league.vs!, s.league.nerdStats!, s.league.playstyle!)], nicknames: [for (int i = 0; i < summaries.length; i++) if (summaries[i].league.nerdStats != null) nicknames[i]]), + if (i == 2) VsGraphs(stats: [for (var s in summaries) if ((s.zenith != null || s.zenithCareerBest != null) && (s.zenith?.aggregateStats??s.zenithCareerBest!.aggregateStats).apm > 0.00) s.zenith?.aggregateStats??s.zenithCareerBest!.aggregateStats], nicknames: [for (int i = 0; i < summaries.length; i++) if ((summaries[i].zenith != null || summaries[i].zenithCareerBest != null) && (summaries[i].zenith?.aggregateStats??summaries[i].zenithCareerBest!.aggregateStats).apm > 0.00) nicknames[i]]), + if (i == 3) VsGraphs(stats: [for (var s in summaries) if ((s.zenithEx != null || s.zenithExCareerBest != null) && (s.zenithEx?.aggregateStats??s.zenithExCareerBest!.aggregateStats).apm > 0.00) s.zenithEx?.aggregateStats??s.zenithExCareerBest!.aggregateStats], nicknames: [for (int i = 0; i < summaries.length; i++) if ((summaries[i].zenithEx != null || summaries[i].zenithExCareerBest != null) && (summaries[i].zenithEx?.aggregateStats??summaries[i].zenithExCareerBest!.aggregateStats).apm > 0.00) nicknames[i]]), + ], + ), + ), + ]), + ), + ), + ], + ), + ), + ), + ); + } +} + +class HeaderCard extends StatelessWidget{ + final TetrioPlayer player; + final Function removePlayer; + + const HeaderCard(this.player, this.removePlayer); + + String fontStyle(int length){ + if (length < 10) return "Eurostile Round Extended"; + else if (length < 13) return "Eurostile Round"; + else return "Eurostile Round Condensed"; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 175, + child: Card( + child: Column( + children: [ + Stack( + alignment: Alignment.topCenter, + clipBehavior: Clip.none, + children: [ + if (player.bannerRevision != null) FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", + placeholder: kTransparentImage, + fit: BoxFit.cover, + height: 120.0, + fadeInCurve: Easing.standard, fadeInDuration: Durations.long4 + ) + else SizedBox(height: 120.0, width: 300.0), + Positioned( + top: 20.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: player.role == "banned" + ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: 128) + : player.avatarRevision != null + ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", + fit: BoxFit.fitHeight, height: 128, placeholder: kTransparentImage, fadeInCurve: Easing.emphasizedDecelerate, fadeInDuration: Durations.long4) + : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: 128), + ) + ), + Positioned( + right: 0, + child: IconButton(onPressed: (){ + removePlayer(player.userId); + }, icon: Icon(Icons.close, shadows: textShadow,)) + ) + ], + ), + RichText( + text: TextSpan(text: player.username, style: TextStyle( + fontFamily: fontStyle(player.username.length), + fontSize: 28, + color: Colors.white, + shadows: textShadow + ), + ) + ), + ], + ), + ), + ); + }} + +class AddNewColumnCard extends StatefulWidget{ + final Future Function(String) addPlayer; + + const AddNewColumnCard(this.addPlayer); + + @override + State createState() => _AddNewColumnCardState(); +} + +class _AddNewColumnCardState extends State with SingleTickerProviderStateMixin { + late AnimationController _animController; + late Animation _anim; + bool showSpinner = false; + + @override + void initState(){ + _animController = AnimationController( + duration: Durations.medium3, + vsync: this, + ); + _anim = new Tween( + begin: 0.0, + end: 1.0, + ).animate(new CurvedAnimation( + parent: _animController, + curve: Easing.standard + )); + super.initState(); + } + + @override + void dispose() { + _animController.dispose(); + super.dispose(); + } + + String getExcepitonText(Exception e){ + return switch(e.runtimeType){ + TetrioPlayerNotExist => t.errors.noSuchUser, + ConnectionIssue => t.errors.connection(code: (e as ConnectionIssue).code, message: e.message), + _ => e.toString() + }; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 175.0, + child: Card( + child: AnimatedBuilder( + animation: _anim, + builder: (context, child) { + return _anim.value > 0.5 ? Opacity( + opacity: _anim.value*2-1, + child: Container( + transform: Matrix4.translationValues(0, 100-(_anim.value as double)*100, 0), + child: Column( + children: [ + Text(t.enterUsername), + TextField( + autofocus: true, + onSubmitted: (value){ + widget.addPlayer(value).then((onValue){ + showSpinner = false; + setState(() { + if (onValue != null) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(getExcepitonText(onValue)))); + }); + }); + setState(() { + showSpinner = true; + }); + }, + onTapOutside: (event) { + setState((){_animController.animateBack(0);}); + }, + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Opacity( + opacity: showSpinner ? 1.0 : 0.0, + child: CircularProgressIndicator( + value: showSpinner ? null : 0.0, + ) + ), + ) + ], + ), + ), + ) : Center( + child: IconButton( + visualDensity: VisualDensity.comfortable, + onPressed: (){setState((){_animController.forward();});}, + icon: Opacity(opacity: 1-(_anim.value as double)*2, child: Transform.translate(offset: Offset.fromDirection(pi*1.5, (_anim.value as double)*50), child: Transform.rotate(angle: (_anim.value as double)*2, child: Icon(Icons.add)))), + constraints: BoxConstraints.expand(), + ), + ); + } + ) + ) + ); + } +} + +class VsGraphs extends StatelessWidget{ + final List stats; + final List nicknames; + const VsGraphs({super.key, required this.stats, required this.nicknames}); + + static const List colorsForGraphs = [ + Colors.cyanAccent, + Colors.redAccent, + Colors.purpleAccent, + Colors.amberAccent, + Colors.pinkAccent, + Colors.tealAccent, + Colors.deepOrangeAccent, + ]; + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Wrap( + direction: Axis.horizontal, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 20, + children: [ + for (int i = 0; i < stats.length; i++) Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 0.0), + child: Container(width: 20.0, height: 10.0, decoration: BoxDecoration(color: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), border: Border.all(color: colorsForGraphs[i%colorsForGraphs.length])),), + ), + Text(nicknames[i], style: TextStyle(fontFamily: "Eurostile Round Extended")) + ], + ) + ], + ), + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle( + text: 'APM', + angle: angle, + positionPercentageOffset: 0.05 + ); + case 1: + return RadarChartTitle( + text: 'PPS', + angle: angle, + positionPercentageOffset: 0.05 + ); + case 2: + return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); + case 4: + return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 5: + return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 6: + return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + case 7: + return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); + case 8: + return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); + case 9: + return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].apm * apmWeight), + RadarEntry(value: stats[i].pps * ppsWeight), + RadarEntry(value: stats[i].vs * vsWeight), + RadarEntry(value: stats[i].nerdStats.app * appWeight), + RadarEntry(value: stats[i].nerdStats.dss * dssWeight), + RadarEntry(value: stats[i].nerdStats.dsp * dspWeight), + RadarEntry(value: stats[i].nerdStats.appdsp * appdspWeight), + RadarEntry(value: stats[i].nerdStats.vsapm * vsapmWeight), + RadarEntry(value: stats[i].nerdStats.cheese * cheeseWeight), + RadarEntry(value: stats[i].nerdStats.gbe * gbeWeight), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), + swapAnimationCurve: Curves.linear, + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: 'Opener',angle: angle, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: 'Stride', angle: angle, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: 'Inf Ds', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: 'Plonk', angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].playstyle.opener), + RadarEntry(value: stats[i].playstyle.stride), + RadarEntry(value: stats[i].playstyle.infds), + RadarEntry(value: stats[i].playstyle.plonk), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ), + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional + ), + ), + ), + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + tickCount: 4, + ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), + radarBorderData: const BorderSide(color: Colors.transparent, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.transparent, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: t.stats.graphs.attack, angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: t.stats.graphs.speed, angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: t.stats.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: t.stats.graphs.cheese, angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + for (int i = 0; i < stats.length; i++) RadarDataSet( + fillColor: colorsForGraphs[i%colorsForGraphs.length].withAlpha(128), + borderColor: colorsForGraphs[i%colorsForGraphs.length], + dataEntries: [ + RadarEntry(value: stats[i].apm / 60 * 0.4), + RadarEntry(value: stats[i].pps / 3.75), + RadarEntry(value: stats[i].nerdStats.dss * 1.15), + RadarEntry(value: stats[i].nerdStats.cheese / 110), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1.2), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ) + ) + ) + ) + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/customization_view.dart b/lib/views/customization_view.dart deleted file mode 100644 index f518c8a..0000000 --- a/lib/views/customization_view.dart +++ /dev/null @@ -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 createState() => CustomizationState(); -} - -class CustomizationState extends State { - 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>? locales = - >[]; - for (var v in AppLocale.values) { - locales.add(DropdownMenuItem( - 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: [ - ElevatedButton( - child: const Text('Set'), - onPressed: () { - setState(() { - context.findAncestorStateOfType()?.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(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: [ - 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; - }); - }),) - ], - )), - ); - } -} diff --git a/lib/views/destination_calculator.dart b/lib/views/destination_calculator.dart new file mode 100644 index 0000000..37b1a6f --- /dev/null +++ b/lib/views/destination_calculator.dart @@ -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 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> 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 { + // 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 rSideWidgets = []; + List 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 clears = []; + Map 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( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: CalcCards.calc, + label: Text(t.calcNavigation.stats), + ), + ButtonSegment( + value: CalcCards.damage, + label: Text(t.calcNavigation.damage), + ), + ], + selected: {calcCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + calcCard = newSelection.first; + });}) + ], + ); + } + +} diff --git a/lib/views/destination_cutoffs.dart b/lib/views/destination_cutoffs.dart new file mode 100644 index 0000000..564c6f5 --- /dev/null +++ b/lib/views/destination_cutoffs.dart @@ -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 createState() => _DestinationCutoffsState(); +} + +class _DestinationCutoffsState extends State { + + Future fetch() async { + TetrioPlayerFromLeaderboard top1; + CutoffsTetrio cutoffs; + List 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( + 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?"); + } + ); + } +} diff --git a/lib/views/destination_graphs.dart b/lib/views/destination_graphs.dart new file mode 100644 index 0000000..1a932e2 --- /dev/null +++ b/lib/views/destination_graphs.dart @@ -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 createState() => _DestinationGraphsState(); +} + +Graph graph = Graph.history; +Stats Ychart = Stats.tr; + +enum Graph{ + history, + leagueState, + leagueCutoffs +} + +class _DestinationGraphsState extends State { + 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> _yAxis = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; + Stats _Xchart = Stats.tr; + int _season = currentSeason-1; + ValueNotifier historyPlayerUsername = ValueNotifier(""); + ValueNotifier historyPlayerAvatarRevizion = ValueNotifier(""); + List excludeRanks = []; + late Future> 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>>> 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> states = await Future.wait>([ + teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), + ]); + Map>> historyData = {}; // [season][metric][spot] + for (int season = 0; season < currentSeason; season++){ + if (states[season].length >= 2){ + Map> 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> 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>>>( + 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: [ + 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( + 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( + isVisible: _smooth, + period: (selectedGraph.length/175).floor(), + type: TrendlineType.movingAverage, + color: Theme.of(context).colorScheme.primary) + ], + ), + ], + ); + }else{ return FutureError(snapshot); } + } + } + ); + } + + Widget getLeagueState (){ + return FutureBuilder>( + 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>( + 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: [ + for (String rank in ranks) StepLineSeries( + 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( + 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( + 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: [ + 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( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: Graph.history, + label: Text(t.graphsNavigation.history)), + ButtonSegment( + value: Graph.leagueState, + label: Text(t.graphsNavigation.league)), + ButtonSegment( + value: Graph.leagueCutoffs, + label: Text(t.graphsNavigation.cutoffs), + ), + ], + selected: {graph}, + onSelectionChanged: (Set 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); +} diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart new file mode 100644 index 0000000..d668c62 --- /dev/null +++ b/lib/views/destination_home.dart @@ -0,0 +1,1420 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_layout_grid/flutter_layout_grid.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/achievement.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.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_extras.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/tetrio_constants.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/colors_functions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/main_view.dart'; +import 'package:tetra_stats/views/singleplayer_record_view.dart'; +import 'package:tetra_stats/widgets/badges_thingy.dart'; +import 'package:tetra_stats/widgets/distinguishment_thingy.dart'; +import 'package:tetra_stats/widgets/error_thingy.dart'; +import 'package:tetra_stats/widgets/fake_distinguishment_thingy.dart'; +import 'package:tetra_stats/widgets/finesse_thingy.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; +import 'package:tetra_stats/widgets/lineclears_thingy.dart'; +import 'package:tetra_stats/widgets/nerd_stats_thingy.dart'; +import 'package:tetra_stats/widgets/news_thingy.dart'; +import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; +import 'package:tetra_stats/widgets/tl_records_thingy.dart'; +import 'package:tetra_stats/widgets/tl_thingy.dart'; +import 'package:tetra_stats/widgets/user_thingy.dart'; +import 'package:tetra_stats/widgets/zenith_thingy.dart'; + +class DestinationHome extends StatefulWidget{ + final String searchFor; + final Future dataFuture; + final BoxConstraints constraints; + final bool noSidebar; + + const DestinationHome({super.key, required this.searchFor, required this.dataFuture, required this.constraints, this.noSidebar = false}); + + @override + State createState() => _DestinationHomeState(); +} + +Cards rightCard = Cards.overview; +CardMod cardMod = CardMod.info; +Map>> modeButtons = { + Cards.overview: [ + ButtonSegment( + value: CardMod.info, + label: Text(t.general), + ), + ], + Cards.tetraLeague: [ + ButtonSegment( + value: CardMod.info, + label: Text(t.homeNavigation.standing), + ), + ButtonSegment( + value: CardMod.ex, // yeah i misusing my own Enum shut the fuck up + label: Text(t.homeNavigation.seasons), + ), + ButtonSegment( + value: CardMod.records, + label: Text(t.homeNavigation.mathces), + ), + ], + Cards.quickPlay: [ + ButtonSegment( + value: CardMod.info, + label: Text(t.homeNavigation.normal), + ), + ButtonSegment( + value: CardMod.records, + label: Text(t.records), + ), + ButtonSegment( + value: CardMod.ex, + label: Text(t.homeNavigation.expert), + ), + ButtonSegment( + value: CardMod.exRecords, + label: Text(t.homeNavigation.expertRecords), + ) + ], + Cards.blitz: [ + ButtonSegment( + value: CardMod.info, + label: Text(t.homeNavigation.pb), + ), + ButtonSegment( + value: CardMod.records, + label: Text(t.recent), + ) + ], + Cards.sprint: [ + ButtonSegment( + value: CardMod.info, + label: Text(t.homeNavigation.pb), + ), + ButtonSegment( + value: CardMod.records, + label: Text(t.recent), + ) + ] +}; + +class ZenithCard extends StatelessWidget { + final RecordSingle? record; + final bool old; + final double width; + final List achievements; + + const ZenithCard(this.record, this.old, this.achievements, {this.width = double.infinity}); + + Widget splitsCard(){ + return Card( + child: Center( + child: SizedBox( + width: 300, + height: 318, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("T", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + Positioned(left: 25, top: 20, child: Text(t.stats.totalTime.widgetTitle, style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text(getMoreNormalTime(record!.stats.finalTime), style: const TextStyle( + shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + SizedBox( + width: 300.0, + child: Table( + columnWidths: const { + 0: FixedColumnWidth(36) + }, + children: [ + TableRow( + children: [ + Text(t.stats.floor), + Text(t.stats.split, textAlign: TextAlign.right), + Text(t.stats.total, textAlign: TextAlign.right), + ] + ), + for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( + children: [ + Text((i+1).toString()), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), + Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), + ] + ) + ], + ), + ), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.gamemodes["zenith"]!, style: width > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall), + //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), + ], + ), + ), + ), + ), + ZenithThingy(zenith: record, old: old, width: width), + if (record != null) width > 600 ? Row( + children: [ + Expanded( + child: Card( + child: Column( + children: [ + FinesseThingy(record!.stats.finesse, record!.stats.finessePercentage), + LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins, showMoreClears: true) + ], + ), + ), + ), + Expanded( + child: splitsCard() + ), + ], + ) : Column( + children: [ + Card( + child: Center( + child: Column( + children: [ + FinesseThingy(record!.stats.finesse, record!.stats.finessePercentage), + LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins, showMoreClears: true) + ], + ), + ), + ), + splitsCard(), + ], + ), + if (record != null) Card(child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text(t.nerdStats, style: width > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + ))), + if (record != null) NerdStatsThingy(nerdStats: record!.aggregateStats.nerdStats, width: width), + if (record != null) Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), + if (achievements.isNotEmpty) Card(child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text(t.relatedAchievements, style: width > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + ))), + if (achievements.isNotEmpty) Wrap( + direction: Axis.horizontal, + children: [ + for (Achievement achievement in achievements) FractionallySizedBox(widthFactor: 1/((width/600).ceil()), child: AchievementSummary(achievement: achievement)), + ], + ), + ], + ); + } +} + +class RecordCard extends StatelessWidget { + final RecordSingle? record; + final List achievements; + final bool? betterThanRankAverage; + final MapEntry? closestAverage; + final bool? betterThanClosestAverage; + final String? rank; + final double width; + + const RecordCard(this.record, this.achievements, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, {this.width = double.infinity}); + + Widget result(){ + TextStyle tableTextStyle = TextStyle(fontSize: width > 768.0 ? 21 : 18); + return Card( + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Tooltip(message: "${t.rankView.avgForRank(rank: closestAverage!.key.toUpperCase())}: ${ + switch(record!.gamemode){ + "40l" => get40lTime(closestAverage!.value.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(closestAverage!.value), + _ => closestAverage!.value.toString() + } + }", child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96)) + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText(text: TextSpan( + text: switch(record!.gamemode){ + "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), + "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), + _ => record!.stats.score.toString() + }, + style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), + ), + ), + RichText(text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), + _ => record!.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + color: betterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), + "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), + _ => record!.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), + if (record!.rank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} ${t.localStanding}", style: TextStyle(color: getColorOfRank(record!.countryRank))), + if (record!.countryRank != -1) TextSpan(text: width > 600.0 ? " • " : "\n"), + TextSpan(text: timestamp(record!.timestamp)), + ] + ), + ), + ], + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(switch(record!.gamemode){ + "40l" => record!.stats.piecesPlaced.toString(), + "blitz" => record!.stats.level.toString(), + "5mblast" => NumberFormat.decimalPattern().format(record!.stats.spp), + _ => "What if " + }, textAlign: TextAlign.right, style: tableTextStyle), + Text(switch(record!.gamemode){ + "40l" => " ${t.stats.pieces.full}", + "blitz" => " ${t.stats.level.full}", + "5mblast" => " ${t.stats.spp.short}", + _ => " i wanted to" + }, textAlign: TextAlign.left, style: tableTextStyle), + ]), + TableRow(children: [ + Text(f2.format(record!.stats.pps), textAlign: TextAlign.right, style: tableTextStyle), + Text(" ${t.stats.pps.short}", textAlign: TextAlign.left, style: tableTextStyle), + ]), + TableRow(children: [ + Text(switch(record!.gamemode){ + "40l" => f2.format(record!.stats.kpp), + "blitz" => f2.format(record!.stats.spp), + "5mblast" => record!.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: tableTextStyle), + Text(switch(record!.gamemode){ + "40l" => " ${t.stats.kpp.short}", + "blitz" => " ${t.stats.spp.short}", + "5mblast" => " ${t.stats.pieces.short}", + _ => " no" + }, textAlign: TextAlign.left, style: tableTextStyle), + ]) + ], + ), + ), + Expanded( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(intf.format(record!.stats.inputs), textAlign: TextAlign.right, style: tableTextStyle), + Text(" ${t.stats.kp.short}", textAlign: TextAlign.left, style: tableTextStyle), + ]), + TableRow(children: [ + Text(f2.format(record!.stats.kps), textAlign: TextAlign.right, style: tableTextStyle), + Text(" ${t.stats.kps.short}", textAlign: TextAlign.left, style: tableTextStyle), + ]), + TableRow(children: [ + Text(switch(record!.gamemode){ + "40l" => " ", + "blitz" => record!.stats.piecesPlaced.toString(), + "5mblast" => record!.stats.piecesPlaced.toString(), + _ => "but god said" + }, textAlign: TextAlign.right, style: tableTextStyle), + Text(switch(record!.gamemode){ + "40l" => " ", + "blitz" => " ${t.stats.pieces.short}", + "5mblast" => " ${t.stats.pieces.short}", + _ => " no" + }, textAlign: TextAlign.left, style: tableTextStyle), + ]) + ], + ), + ), + ], + ) + ], + ), + ); + } + + Widget hjsdj(){ + return Card( + child: Center( + child: Column( + children: [ + FinesseThingy(record!.stats.finesse, record!.stats.finessePercentage), + LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), + if (record!.gamemode == 'blitz') Text("${f2.format(record!.stats.kpp)} ${t.stats.kpp.short}") + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + if (record == null) { + return Card( + child: Center(child: Text(t.noRecord, style: TextStyle(fontSize: 42))), + ); + } + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(switch(record!.gamemode){ + "40l" => t.gamemodes["40l"]!, + "blitz" => t.gamemodes["blitz"]!, + "5mblast" => t.gamemodes["5mblast"]!, + _ => record!.gamemode + }, style: Theme.of(context).textTheme.titleLarge) + ], + ), + ), + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [result(), hjsdj()], + ), + Wrap( + direction: Axis.horizontal, + children: [ + for (Achievement achievement in achievements) FractionallySizedBox(widthFactor: 1/((width/600).ceil()), child: AchievementSummary(achievement: achievement)), + ], + ), + ] + ); + } +} + +class FetchResults{ + bool success; + TetrioPlayer? player; + List states; + Summaries? summaries; + News? news; + Cutoffs? cutoffs; + CutoffsTetrio? averages; + PlayerLeaderboardPosition? playerPos; + bool isTracked; + Exception? exception; + + FetchResults(this.success, this.player, this.states, this.summaries, this.news, this.cutoffs, this.averages, this.playerPos, this.isTracked, this.exception); +} + +class RecordSummary extends StatelessWidget{ + final RecordSingle? record; + final bool hideRank; + final bool old; + final bool? betterThanRankAverage; + final MapEntry? closestAverage; + final bool? betterThanClosestAverage; + final String? rank; + final double width; + + const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.old = false, this.betterThanClosestAverage, this.rank, this.hideRank = false, this.width = double.infinity}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (closestAverage != null && record != null) Padding(padding: const EdgeInsets.only(right: 8.0), + child: Tooltip(message: "${t.rankView.avgForRank(rank: closestAverage!.key.toUpperCase())}: ${ + switch(record!.gamemode){ + "40l" => get40lTime(closestAverage!.value.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(closestAverage!.value), + _ => closestAverage!.value.toString() + } + }", child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96))) + else !hideRank ? Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) : Container(), + if (record != null) Column( + crossAxisAlignment: hideRank ? CrossAxisAlignment.center : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + text: TextSpan( + text: switch(record!.gamemode){ + "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), + "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), + "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(record!.stats.zenith!.altitude)} m", + "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", + _ => record!.stats.score.toString() + }, + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: old ? Colors.grey : Colors.white, height: 0.9), + ), + ), + RichText( + textAlign: hideRank ? TextAlign.center : TextAlign.start, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (rank != null && rank != "z") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), + "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), + _ => record!.stats.score.toString() + }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( + color: betterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent + )) + else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ + "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), + "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), + _ => record!.stats.score.toString() + }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( + color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent + )), + if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), + if (record!.rank != -1 && record!.countryRank != -1) const TextSpan(text: " • "), + if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} ${t.localStanding}", style: TextStyle(color: getColorOfRank(record!.countryRank))), + const TextSpan(text: "\n"), + TextSpan(text: timestamp(record!.timestamp)), + ] + ), + ), + ], + ) else if (hideRank) RichText(text: const TextSpan( + text: "---", + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), + ), + ) + ], + ); + } +} + +class AchievementSummary extends StatelessWidget{ + final Achievement? achievement; + + const AchievementSummary({this.achievement}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(achievement?.name??"---", style: Theme.of(context).textTheme.titleSmall!.copyWith(color: achievement?.v == null ? Colors.grey : Colors.white), textAlign: TextAlign.center), + const Divider(), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Container( + constraints: BoxConstraints( + maxWidth: 512.0, + maxHeight: 512.0, + //minWidth: 256, + minHeight: 64.0, + ), + child: ClipRect( + child: Align( + alignment: Alignment.topLeft.add(Alignment(0.286 * (((achievement?.k??1) - 1) % 8), 0.286 * (((achievement?.k??0) - 1) / 8).floor())), + heightFactor: 0.125, + widthFactor: 0.125, + child: Image.asset("res/icons/achievements.png", width: 2048, height: 2048, scale: 1, color: achievement?.v == null ? Colors.grey : achievementColors[min(achievement!.rank!, 6)]), + ), + ), + ), + ), + //ClipRect(clipper: Rect.fromLTRB(0, 0, 64, 64), child: ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: TextAlign.start, + text: TextSpan( + text: achievement?.v == null ? "---" : switch(achievement!.vt){ + 1 => intf.format(achievement!.v), + 2 => get40lTime((achievement!.v! * 1000).floor()), + 3 => get40lTime((achievement!.v!.abs() * 1000).floor()), + 4 => "${f2.format(achievement!.v!)} m", + 5 => "№ ${intf.format(achievement!.pos!+1)}", + 6 => intf.format(achievement!.v!.abs()), + _ => "lol" + }, + style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: achievement?.v == null ? Colors.grey : Colors.white, height: 0.9), + ), + ), + if (achievement != null) RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (achievement!.object.isNotEmpty) TextSpan(text: "${achievement!.object}\n"), + if (achievement!.vt == 4) TextSpan(text: "${t.stats.floor} ${achievement?.a != null ? achievement!.a! : "-"}"), + if (achievement!.vt == 4) TextSpan(text: " • "), + if (achievement!.vt != 5) TextSpan(text: (achievement?.pos != null && !achievement!.pos!.isNegative) ? "№ ${intf.format(achievement!.pos!+1)}" : "№ ---", style: TextStyle(color: achievement?.pos != null ? getColorOfRank(achievement!.pos!+1) : Colors.grey)), + if (achievement!.vt != 5) TextSpan(text: " • ", style: TextStyle(color: achievement?.pos != null ? getColorOfRank(achievement!.pos!+1) : Colors.grey)), + TextSpan(text: t.stats.top(percentage: achievement?.pos != null ? percentagef4.format(achievement!.pos! / achievement!.total!) : "---"), style: TextStyle(color: achievement?.pos != null ? getColorOfRank(achievement!.pos!+1) : Colors.grey)), + ] + ), + ), + ], + ), + ), + ], + ), + const Divider(), + Text(achievement?.t != null ? timestamp(achievement!.t!) : "---", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ); + } + +} + +class LeagueCard extends StatelessWidget{ + final TetraLeague league; + final CutoffTetrio? averages; + final bool showSeasonNumber; + final double width; + + const LeagueCard({super.key, required this.league, this.averages, this.showSeasonNumber = false, this.width = double.infinity}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (showSeasonNumber) width > 600.0 ? Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text("${t.season} ${league.season}", style: Theme.of(context).textTheme.titleSmall), + Spacer(), + Text( + "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey)), + ], + ) : Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("${t.season} ${league.season}", style: Theme.of(context).textTheme.titleSmall), + Text( + "${seasonStarts.elementAtOrNull(league.season - 1) != null ? timestamp(seasonStarts[league.season - 1]) : "---"} — ${seasonEnds.elementAtOrNull(league.season - 1) != null ? timestamp(seasonEnds[league.season - 1]) : "---"}", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey)), + ], + ) + else Text(t.gamemodes["league"]!, style: Theme.of(context).textTheme.titleSmall), + const Divider(), + TLRatingThingy(userID: league.id, tlData: league, showPositions: true), + const Divider(), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.grey), + children: [ + TextSpan(text: "${league.apm != null ? f2.format(league.apm) : "-.--"} ${t.stats.apm.short}", style: TextStyle(color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.pps != null ? f2.format(league.pps) : "-.--"} ${t.stats.pps.short}", style: TextStyle(color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.vs != null ? f2.format(league.vs) : "-.--"} ${t.stats.vs.short}", style: TextStyle(color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.nerdStats != null ? f2.format(league.nerdStats!.app) : "-.--"} ${t.stats.app.short}", style: TextStyle(color: league.nerdStats != null ? getStatColor(league.nerdStats!.app, averages?.nerdStats?.app, true) : null)), + TextSpan(text: " • "), + TextSpan(text: "${league.nerdStats != null ? f2.format(league.nerdStats!.vsapm) : "-.--"} ${t.stats.vsapm.short}", style: TextStyle(color: league.nerdStats != null ? getStatColor(league.nerdStats!.vsapm, averages?.nerdStats?.vsapm, true) : null)), + ] + )), + ], + ), + ), + ), + ); + } + +} + +class _DestinationHomeState extends State with SingleTickerProviderStateMixin { + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + late MapEntry? closestAverageBlitz; + late bool blitzBetterThanClosestAverage; + late MapEntry? closestAverageSprint; + late bool sprintBetterThanClosestAverage; + late AnimationController _transition; + late final Animation _offsetAnimation; + bool? sprintBetterThanRankAverage; + bool? blitzBetterThanRankAverage; + + Widget getOverviewCard(Summaries summaries, CutoffTetrio? averages, double width){ + return LayoutGrid( + areas: width > 600 ? ''' + h h + t t + 1 2 + 3 4 + 5 6 + ''' : ''' + t + 1 + 2 + 3 + 4 + 5 + 6 + ''', + columnSizes: width > 600 ? [auto, auto] : [auto], + rowSizes: width > 600 ? [auto, auto, auto, auto, auto] : [auto, auto, auto, auto, auto, auto, auto], + columnGap: 0, + rowGap: 0, + children: [ + if (width > 600) Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.homeNavigation.overview, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center), + ], + ), + ), + ), + ).inGridArea('h'), + LeagueCard(league: summaries.league, averages: averages).inGridArea('t'), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.gamemodes['40l']!, style: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + const Divider(), + RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.rank), + const Divider(), + Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "-.--"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "-.--"} KPP", style: const TextStyle(color: Colors.grey), textAlign: TextAlign.center) + ], + ), + ), + ).inGridArea('1'), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.gamemodes['blitz']!, style: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + const Divider(), + RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.rank), + const Divider(), + Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "-.--"} PPS", style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ).inGridArea('2'), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.gamemodes['zenith']!, style: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + const Divider(), + RecordSummary(record: summaries.zenith != null ? summaries.zenith : summaries.zenithCareerBest, hideRank: true, old: summaries.zenith == null), + const Divider(), + Text(t.overallPB(pb: (summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"), style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ).inGridArea('3'), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.gamemodes['zenithex']!, style: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + const Divider(), + RecordSummary(record: summaries.zenithEx != null ? summaries.zenithEx : summaries.zenithExCareerBest, hideRank: true, old: summaries.zenith == null), + const Divider(), + Text(t.overallPB(pb: (summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"), style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ).inGridArea('4'), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center(child: Text(t.gamemodes['zen']!, style: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center)), + Text("${t.stats.level.full} ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), + Text("${t.stats.score} ${intf.format(summaries.zen.score)}"), + Text(t.stats.levelUpRequirement(p: intf.format(summaries.zen.scoreRequirement)), style: const TextStyle(color: Colors.grey)) + ], + ), + ), + ).inGridArea('5'), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + const Text("f", style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 65, + height: 1.2, + )), + Positioned(left: 25, top: 20, child: Text(t.stats.finesse.widgetTitle, style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text("${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? + f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: const TextStyle( + //shadows: textShadow, + fontFamily: "Eurostile Round Extended", + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ) + ], + ), + Row( + children: [ + Text("${t.stats.piecesTotal}:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"), + ], + ), + Row( + children: [ + Text(" - ${t.stats.piecesWithPerfectFinesse}:"), + const Spacer(), + Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"), + ], + ) + ], + ), + ), + ).inGridArea('6'), + ], + ); + } + + Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs, CutoffTetrio? averages, List states, PlayerLeaderboardPosition? lbPos, double width, List achievements){ + TetraLeague toSee; + TetraLeague? toCompare; + if (currentRangeValues.start.round() == 0){ + toSee = data; + }else{ + toSee = states[currentRangeValues.start.round()-1]; + } + if (currentRangeValues.end.round() == 1){ + toCompare = states.length >= 2 ? states.elementAtOrNull(2) : null; + }else{ + toCompare = states[currentRangeValues.end.round()-1]; + } + return Column( + children: [ + if (toCompare != null) Card( + child: RangeSlider(values: currentRangeValues, max: states.length.toDouble(), + labels: RangeLabels( + currentRangeValues.start.round().toString(), + currentRangeValues.end.round().toString(), + ), + onChanged: (RangeValues values) { + setState(() { + currentRangeValues = values; + }); + }, + ), + ), + Card( + //surfaceTintColor: rankColors[data.rank], + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.gamemodes["league"]!, style: widget.constraints.maxWidth > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall), + if (toCompare != null) Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text(t.comparingWith(newDate: timestamp(toSee.timestamp), oldDate: timestamp(toCompare.timestamp)), textAlign: TextAlign.center, style: widget.constraints.maxWidth > 768.0 ? null : TextStyle(fontSize: 12.0)), + ) + ], + ), + ), + ), + ), + TetraLeagueThingy(league: toSee, toCompare: toCompare, cutoffs: cutoffs, averages: averages, lbPos: lbPos, width: width), + // Center( + // child: Card( + // child: ElevatedButton.icon( + // onPressed: (){teto.fetchAndsaveTLHistory(data.id, 1).then((_) => setState((){}));}, + // icon: const Icon(Icons.query_stats), + // label: Text(t.graphsDestination.fetchAndsaveTLHistory), + // style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12.0))))) + // ) + // ), + // ), + if (data.nerdStats != null) Card(child: Center(child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text(t.nerdStats, style: widget.constraints.maxWidth > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + ))), + if (data.nerdStats != null) NerdStatsThingy(nerdStats: toSee.nerdStats!, oldNerdStats: toCompare?.nerdStats, averages: averages, lbPos: lbPos, width: width), + if (data.nerdStats != null) Graphs(toSee.apm!, toSee.pps!, toSee.vs!, toSee.nerdStats!, toSee.playstyle!), + Card(child: Center(child: Text(t.relatedAchievements, style: widget.constraints.maxWidth > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center))), + Wrap( + direction: Axis.horizontal, + children: [ + for (Achievement achievement in achievements) FractionallySizedBox(widthFactor: 1/((width/600).ceil()), child: AchievementSummary(achievement: achievement)), + ], + ), + ], + ); + } + + Widget getPreviousSeasonsList(Map pastLeague, double width){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.previousSeasons, style: widget.constraints.maxWidth > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + for (var key in pastLeague.keys) Card( + child: LeagueCard(league: pastLeague[key]!, showSeasonNumber: true, width: width), + ) + ], + ); + } + + Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ + return Column( + children: [ + Card( + child: Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.records, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), + //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) + ], + ), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: DefaultTabController(length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TabBar( + tabs: [ + Tab(text: t.recent), + Tab(text: t.top), + ], + ), + SizedBox( + height: constraints.maxHeight - 192, + child: TabBarView( + children: [ + FutureBuilder( + future: teto.fetchStream(widget.searchFor, recentStream), + 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 SingleChildScrollView( + child: Column( + children: [ + for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => "40L", + "blitz" => "BLZ", + "5mblast" => "5MB", + "zenith" => "QP", + "zenithex" => "QPE", + String() => "huh", + }, + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.stats.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.stats.qpWithMods(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.stats.qpWithMods(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: Theme.of(context).textTheme.displayLarge), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ), + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + FutureBuilder( + future: teto.fetchStream(widget.searchFor, topStream), + 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 SingleChildScrollView( + child: Column( + children: [ + for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), + leading: Text( + "#${i+1}", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) + ), + title: Text( + switch (snapshot.data!.records[i].gamemode){ + "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "blitz" => t.stats.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), + "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), + "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.stats.qpWithMods(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.stats.qpWithMods(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", + String() => "huh", + }, + style: Theme.of(context).textTheme.displayLarge), + subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), + trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) + ) + ], + ), + ); + } + if (snapshot.hasError){ return FutureError(snapshot); } + } + return const Text("what?"); + }, + ), + ] + ), + ) + ], + ), + ) + ), + ], + ); + } + + Widget getRecentTLrecords(BoxConstraints constraints, String userID){ + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(t.recent, style: widget.constraints.maxWidth > 768.0 ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleSmall), + ], + ), + ), + ), + ), + TLRecords(userID), + ], + ); + } + + @override + initState(){ + _transition = AnimationController(vsync: this, duration: Durations.long4); + _offsetAnimation = Tween( + begin: Offset.zero, + end: const Offset(1.5, 0.0), + ).animate(CurvedAnimation( + parent: _transition, + curve: Curves.elasticIn, + )); + super.initState(); + } + + Widget rigthCard(AsyncSnapshot snapshot, List sprintAchievements, List blitzAchievements, List tlAchievements, List qpAchievements, List qpExAchievements, double width){ + return switch (rightCard){ + Cards.overview => getOverviewCard(snapshot.data!.summaries!, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.percentileRank] : null, width), + Cards.tetraLeague => switch (cardMod){ + CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs, (snapshot.data!.averages != null && snapshot.data!.summaries!.league.rank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.rank] : (snapshot.data!.averages != null && snapshot.data!.summaries!.league.percentileRank != "z") ? snapshot.data!.averages!.data[snapshot.data!.summaries!.league.percentileRank] : null, snapshot.data!.states, snapshot.data!.playerPos, width, tlAchievements), + CardMod.ex => getPreviousSeasonsList(snapshot.data!.summaries!.pastLeague, width), + CardMod.records => getRecentTLrecords(widget.constraints, snapshot.data!.player!.userId), + _ => const Center(child: Text("huh?")) + }, + Cards.quickPlay => switch (cardMod){ + CardMod.info => ZenithCard(snapshot.data?.summaries?.zenith != null ? snapshot.data!.summaries!.zenith : snapshot.data!.summaries?.zenithCareerBest, snapshot.data!.summaries?.zenith == null, qpAchievements, width: width), + CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), + CardMod.ex => ZenithCard(snapshot.data?.summaries?.zenithEx != null ? snapshot.data!.summaries!.zenithEx : snapshot.data!.summaries?.zenithExCareerBest, snapshot.data!.summaries?.zenithEx == null, qpExAchievements, width: width), + CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), + }, + Cards.sprint => switch (cardMod){ + CardMod.info => RecordCard(snapshot.data?.summaries!.sprint, sprintAchievements, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank, width: width), + CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + Cards.blitz => switch (cardMod){ + CardMod.info => RecordCard(snapshot.data?.summaries!.blitz, blitzAchievements, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank, width: width), + CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), + _ => const Center(child: Text("huh?")) + }, + }; + } + + @override + Widget build(BuildContext context) { + double width = widget.noSidebar ? widget.constraints.maxWidth : widget.constraints.maxWidth - 80; + bool screenIsBig = width >= 768; + return FutureBuilder( + future: widget.dataFuture, + 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){ + if (!snapshot.data!.success) return ErrorThingy(data: snapshot.data!); + blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null) ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; + sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null) ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; + if (snapshot.data!.summaries!.sprint != null) { + closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); + sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; + } else { + closestAverageSprint = sprintAverages.entries.last; + sprintBetterThanClosestAverage = false; + } + if (snapshot.data!.summaries!.blitz != null){ + closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); + blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; + } else { + closestAverageBlitz = blitzAverages.entries.last; + blitzBetterThanClosestAverage = false; + } + List tlAchievements = snapshot.data!.summaries!.achievements.isNotEmpty ? [ + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 10), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 12), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 13), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 14), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 15), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 47), + ] : []; + List qpAchievements = snapshot.data!.summaries!.achievements.isNotEmpty ? [ + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 16), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 17), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 18), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 20), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 21), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 22), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 23), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 24), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 25), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 26), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 27), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 28), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 29), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 30), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 33), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 41), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 43), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 44), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 45), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 46), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 51), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 54), + ] : []; + List qpExAchievements = snapshot.data!.summaries!.achievements.isNotEmpty ? [ + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 19), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 31), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 32), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 34), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 40), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 49), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 50), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 53), + ] : []; + List sprintAchievements = snapshot.data!.summaries!.achievements.isNotEmpty ? [ + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 5), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 7), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 8), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 9), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 36), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 37), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 38), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 48), + ] : []; + List blitzAchievements = snapshot.data!.summaries!.achievements.isNotEmpty ? [ + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 6), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 39), + snapshot.data!.summaries!.achievements.firstWhere((e) => e.k == 52), + ] : []; + return TweenAnimationBuilder( + duration: Durations.long4, + tween: Tween(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: screenIsBig ? Row( + children: [ + SizedBox( + width: 450, + child: Column( + children: [ + UserThingy(player: snapshot.data!.player!, initIsTracking: snapshot.data!.isTracked, showStateTimestamp: false, setState: setState), + if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), + if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), + if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), + if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) + else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), + if (snapshot.data!.player!.bio != null) Card( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], + ), + ), + Expanded( + child: NewsThingy(snapshot.data!.news!) + ) + ], + ), + ), + SizedBox( + width: width - 450, + child: Column( + children: [ + SizedBox( + height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, + child: SlideTransition( + position: _offsetAnimation, + child: SingleChildScrollView( + child: rigthCard(snapshot, sprintAchievements, blitzAchievements, tlAchievements, qpAchievements, qpExAchievements, width - 450), + ), + ), + ), + if (modeButtons[rightCard]!.length > 1 && !widget.noSidebar) SegmentedButton( + showSelectedIcon: false, + selected: {cardMod}, + segments: modeButtons[rightCard]!, + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + //_transition.; + }); + }, + ), + if (!widget.noSidebar) SegmentedButton( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: Cards.overview, + tooltip: t.homeNavigation.overview, + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + tooltip: t.gamemodes["league"], + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.quickPlay, + tooltip: t.gamemodes["zenith"], + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.sprint, + tooltip: t.gamemodes["40l"], + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.blitz, + tooltip: t.gamemodes["blitz"], + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ], + selected: {rightCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + cardMod = CardMod.info; + rightCard = newSelection.first; + });}) + ], + ) + ) + ], + ) : SingleChildScrollView( + child: Column( + children: [ + UserThingy(player: snapshot.data!.player!, initIsTracking: snapshot.data!.isTracked, showStateTimestamp: false, setState: setState), + if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), + if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), + if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), + if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) + else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), + rigthCard(snapshot, sprintAchievements, blitzAchievements, tlAchievements, qpAchievements, qpExAchievements, width), + if (rightCard == Cards.overview && snapshot.data?.player?.bio != null) Card( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), + ) + ], + ), + ), + if (rightCard == Cards.overview) NewsThingy(snapshot.data!.news!) + ], + ) + ), + ); + } + } + return const Text("End of FutureBuilder"); + }, + ); + } +} diff --git a/lib/views/destination_info.dart b/lib/views/destination_info.dart new file mode 100644 index 0000000..f1e3ae5 --- /dev/null +++ b/lib/views/destination_info.dart @@ -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 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 { + @override + Widget build(BuildContext context) { + List 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), + ), + ) + ], + ); + } +} diff --git a/lib/views/destination_leaderboards.dart b/lib/views/destination_leaderboards.dart new file mode 100644 index 0000000..ff70180 --- /dev/null +++ b/lib/views/destination_leaderboards.dart @@ -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 createState() => _DestinationLeaderboardsState(); +} + +const double transformThreshold = 768.0; + +enum Leaderboards{ + tl, + fullTL, + xp, + ar, + sprint, + blitz, + zenith, + zenithex, +} + +class _DestinationLeaderboardsState extends State { + //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); + final Map 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> _dataStreamController = StreamController>.broadcast(); + late final ScrollController _scrollController; + Stream> get dataStream => _dataStreamController.stream; + List list = []; + bool _isFetchingData = false; + bool _reachedTheEnd = false; + List _excludeRanks = []; + bool _reverse = false; + String? prisecter; + List _countries = [for (MapEntry e in t.countries.entries) DropdownMenuEntry(value: e.key, label: e.value)]; + List _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 _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>( + 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: [ + 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)), + ], + ); + } +} diff --git a/lib/views/destination_saved_data.dart b/lib/views/destination_saved_data.dart new file mode 100644 index 0000000..845cb54 --- /dev/null +++ b/lib/views/destination_saved_data.dart @@ -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 createState() => _DestinationSavedData(); +} + +class _DestinationSavedData extends State { + String? selectedID; + + Future<(List, List, List)> 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, List, List)>( + 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>( + 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"); + }, + ); + } +} diff --git a/lib/views/destination_settings.dart b/lib/views/destination_settings.dart new file mode 100644 index 0000000..be1b18c --- /dev/null +++ b/lib/views/destination_settings.dart @@ -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 createState() => _DestinationSettings(); +} + +enum SettingsCardMod{ + general, + customization, + database +} + +Map 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 with SingleTickerProviderStateMixin { + SettingsCardMod mod = SettingsCardMod.general; + List> locales = >[]; + 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 _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: [ + ElevatedButton( + child: Text(t.actions.apply), + onPressed: () { + setState(() { + context.findAncestorStateOfType()?.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(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: [ + 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: [ + 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: ['db'], + ); + openFile(acceptedTypeGroups: [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( + 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) + ], + ); + } +} diff --git a/lib/views/first_time_view.dart b/lib/views/first_time_view.dart new file mode 100644 index 0000000..1d23a84 --- /dev/null +++ b/lib/views/first_time_view.dart @@ -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 createState() => _FirstTimeState(); +} + +class _FirstTimeState extends State with SingleTickerProviderStateMixin { + late AnimationController _animController; + late final Animation _spinAnimation; + late Animation _opacity; + late Animation _enterNicknameOpacity; + late Animation _transform; + late Animation _badNicknameAnim; + late Animation _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( + 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( + begin: 0.0, + end: 1.0, + ).animate( + CurvedAnimation( + parent: _animController, + curve: const Interval( + 0.0, + 0.5, + curve: Curves.linear, + ), + ), + ); + _enterNicknameOpacity = Tween( + begin: 1.0, + end: 0.0 + ).animate( + CurvedAnimation( + parent: _animController, + curve: const Interval( + 0.75, + 0.9, + curve: Curves.ease, + ), + ), + ); + _transform = Tween( + begin: 0.0, + end: 150.0 + ).animate( + CurvedAnimation( + parent: _animController, + curve: const Interval( + 0.75, + 0.9, + curve: Curves.easeInOut, + ), + ), + ); + _fadeOutOpacity = Tween( + 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 _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(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 + ) + ), + ), + ); + } + +} \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 9e52818..1763532 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,1616 +1,581 @@ -// ignore_for_file: type_literal_in_constant_pattern, use_build_context_synchronously - -import 'dart:async'; -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:http/http.dart'; -import 'package:intl/intl.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/services.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart' show prefs, teto; -import 'package:tetra_stats/services/crud_exceptions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/utils/open_in_browser.dart'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/singleplayer_record_view.dart'; -import 'package:tetra_stats/views/tl_match_view.dart' show TlMatchResultView; -import 'package:tetra_stats/views/zenith_record_view.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; -import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; -import 'package:tetra_stats/widgets/recent_sp_games.dart'; -import 'package:tetra_stats/widgets/search_box.dart'; -import 'package:tetra_stats/widgets/singleplayer_record.dart'; -import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/widgets/tl_thingy.dart'; -import 'package:tetra_stats/widgets/user_thingy.dart'; -import 'package:tetra_stats/widgets/zenith_thingy.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:go_router/go_router.dart'; - -int _chartsIndex = 0; -int _season = currentSeason-1; -bool _gamesPlayedInsteadOfDateAndTime = false; -late ZoomPanBehavior _zoomPanBehavior; -bool _smooth = false; -List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; -late ScrollController _scrollController; - -class MainView extends StatefulWidget { - final String? player; - /// The very first view, that user see when he launch this programm. - /// By default it loads my or defined in preferences user stats, but - /// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu. - const MainView({super.key, this.player}); - - @override - State createState() => _MainState(); -} - -Future copyToClipboard(String text) async { - await Clipboard.setData(ClipboardData(text: text)); -} - -class _MainState extends State with TickerProviderStateMixin { - Future me = Future.delayed(const Duration(seconds: 60), () => [null, null, null, null, null, null]); // I love lists shut up - TetrioPlayersLeaderboard? everyone; - PlayerLeaderboardPosition? meAmongEveryone; - TetraLeague? rankAverages; - double? thatRankCutoff; - double? nextRankCutoff; - double? thatRankGlickoCutoff; - double? nextRankGlickoCutoff; - String _searchFor = "6098518e3d5155e6ec429cdc"; // who we looking for - String _titleNickname = ""; - /// Each dropdown menu item contains list of dots for the graph - /// chartsData[season-1][chart] - List>>> chartsData = []; - //var tableData = []; - final bodyGlobalKey = GlobalKey(); - bool _showSearchBar = false; - Timer backgroundUpdate = Timer(const Duration(days: 365), (){}); - bool _TLHistoryWasFetched = false; - late TabController _tabController; - late TabController _wideScreenTabController; - bool zenithEX = false; - - String get title => "Tetra Stats: $_titleNickname"; - - @override - void initState() { - initDB(); - _scrollController = ScrollController(); - _tabController = TabController(length: 9, vsync: this); - _wideScreenTabController = TabController(length: 5, vsync: this); - _zoomPanBehavior = ZoomPanBehavior( - enablePinching: true, - enableSelectionZooming: true, - enableMouseWheelZooming : true, - enablePanning: true, - ); - // We need to show something - if (widget.player != null){ // if we have user input, - changePlayer(widget.player!); // it's gonna be user input - }else{ - _getPreferences() // otherwise, checking for preferences - .then((value) => changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc")); // no preferences - loading me - } - super.initState(); - } - - @override - void dispose() { - _tabController.dispose(); - _scrollController.dispose(); - super.dispose(); - } - - Future _getPreferences() async { - prefs = await SharedPreferences.getInstance(); - } - - /// That function initiate search of data about [player]. If [fetchHistory] is true, - /// also attempting to retrieve players history. Can trow an Exception if fails - void changePlayer(String player, {bool fetchHistory = false, bool fetchTLmatches = false}) { - setState(() { - _searchFor = player; - me = fetch(_searchFor, fetchHistory: fetchHistory, fetchTLmatches: fetchTLmatches); - }); - } - - void initDB() async{ - await teto.open(); - } - - /// Retrieves data from 3 different Tetra Channel API endpoints + 1 endpoint from p1nkl0bst3r's API - /// using [nickOrID] of player. - /// - /// If [fetchHistory] is true, also retrieves players history from p1nkl0bst3r's API. If [fetchTLmatches] is true, also retrieves players old Tetra League - /// matches from p1nkl0bst3r's API. Returns list which contains [TetrioPlayer], his records, previous states, TL matches, previous TL state, - /// if player tracked (bool), news entries and topTR. - /// - /// If at least one request to Tetra Channel API fails, whole function will throw an exception. - Future fetch(String nickOrID, {bool fetchHistory = false, bool fetchTLmatches = false}) async { - TetrioPlayer me; - _TLHistoryWasFetched = false; - backgroundUpdate.cancel(); - - // If user trying to search with discord id - if (nickOrID.startsWith("ds:")){ - me = await teto.fetchPlayer(nickOrID.substring(3), isItDiscordID: true); // we trying to get him with that - }else{ - me = await teto.fetchPlayer(nickOrID); // Otherwise it's probably a user id or username - } - _searchFor = me.userId; // gonna use user id for next requests - - // Change view title and window title if avaliable - setState((){_titleNickname = me.username;}); - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) await windowManager.setTitle(title); - - // Requesting Tetra League (alpha), records, news and top TR of player - List requests; - Summaries summaries = await teto.fetchSummaries(_searchFor); - late TetraLeagueBetaStream tlStream; - late News news; - // late SingleplayerStream recentSprint; - // late SingleplayerStream recentBlitz; - // late SingleplayerStream sprint; - // late SingleplayerStream blitz; - late SingleplayerStream recentZenith; - late SingleplayerStream recentZenithEX; - late TetrioPlayerFromLeaderboard? topOne; - // late TopTr? topTR; - requests = await Future.wait([ - teto.fetchSummaries(_searchFor), - teto.fetchTLStream(_searchFor), - teto.fetchNews(_searchFor), - teto.fetchStream(_searchFor, "zenith/recent"), - teto.fetchStream(_searchFor, "zenithex/recent"), - teto.fetchCutoffsBeanserver(), - (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? teto.fetchTopOneFromTheLeaderboard() : Future.delayed(Duration.zero, ()=>null), - ]); - //prefs.getBool("showPositions") != true ? teto.fetchCutoffsBeanserver() : Future.delayed(Duration.zero, ()=>>[]), - - //(summaries.league.gamesPlayed > 9) ? teto.fetchTopTR(_searchFor) : Future.delayed(Duration.zero, () => null) // can retrieve this only if player has TR - summaries = requests[0] as Summaries; - tlStream = requests[1] as TetraLeagueBetaStream; - // records = requests[1] as UserRecords; - news = requests[2] as News; - recentZenith = requests[3] as SingleplayerStream; - recentZenithEX = requests[4] as SingleplayerStream; - // recent = requests[3] as SingleplayerStream; - // sprint = requests[4] as SingleplayerStream; - // blitz = requests[5] as SingleplayerStream; - topOne = requests[6] as TetrioPlayerFromLeaderboard?; - // topTR = requests[8] as TopTr?; // No TR - no Top TR - - meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); - if (prefs.getBool("showPositions") == true){ - // Get tetra League leaderboard - everyone = teto.getCachedLeaderboard(); - everyone ??= await teto.fetchTLLeaderboard(); - if (meAmongEveryone == null && everyone!.leaderboard.isNotEmpty){ - meAmongEveryone = await compute(everyone!.getLeaderboardPosition, {me.userId: summaries.league}); - if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); - } - } - Map? cutoffs = (requests[5] as Cutoffs?)?.tr; - Map? cutoffsGlicko = (requests[5] as Cutoffs?)?.glicko; - - if (summaries.league.gamesPlayed > 9) { - thatRankCutoff = cutoffs?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank]; - thatRankGlickoCutoff = cutoffsGlicko?[summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank]; - nextRankCutoff = (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? topOne?.tr??25000 : cutoffs?[ranks.elementAtOrNull(ranks.indexOf(summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank)+1)]; - nextRankGlickoCutoff = (summaries.league.rank != "z" ? summaries.league.rank == "x+" : summaries.league.percentileRank == "x+") ? topOne?.glicko??double.infinity : cutoffsGlicko?[ranks.elementAtOrNull(ranks.indexOf(summaries.league.rank != "z" ? summaries.league.rank : summaries.league.percentileRank)+1)]; - } - - if (everyone != null && summaries.league.gamesPlayed > 9) rankAverages = everyone?.averages[summaries.league.percentileRank]?[0]; - - // Making list of Tetra League matches - bool isTracking = await teto.isPlayerTracking(me.userId); - List> states = await Future.wait>([ - teto.getStates(me.userId, season: 1), teto.getStates(me.userId, season: 2), - ]); - List storedRecords = await teto.getTLMatchesbyPlayerID(me.userId); // get old matches - if (isTracking){ // if tracked - save data to local DB - await teto.storeState(summaries.league); - //await teto.saveTLMatchesFromStream(tlStream); - } - TetraLeagueAlphaStream? oldMatches; - // building list of TL matches - if(fetchTLmatches) { - try{ - oldMatches = await teto.fetchAndSaveOldTLmatches(_searchFor); - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndSaveOldTLmatchesResult(number: oldMatches.records.length)))); - }on TetrioHistoryNotExist{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.p1nkl0bst3rTLmatches))); - }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))); - }finally{ - _TLHistoryWasFetched = true; - } - } - if (storedRecords.isNotEmpty) { - _TLHistoryWasFetched = true; - tlStream.addFromAlphaStream(storedRecords); - } - - // tlMatches.sort((a, b) { // Newest matches gonna be shown at the top of the list - // if(a.ts.isBefore(b.ts)) return 1; - // if(a.ts.isAtSameMomentAs(b.ts)) return 0; - // if(a.ts.isAfter(b.ts)) return -1; - // return 0; - // }); - - // Handling history - if(fetchHistory){ - try{ - var history = await teto.fetchAndsaveTLHistory(_searchFor); // Retrieve if needed - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); - }on TetrioHistoryNotExist{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.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))); - } - } - - //states.addAll(await teto.getPlayer(me.userId)); - // for (var element in states) { // For graphs I need only unique entries - // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); - // if (uniqueTL.isEmpty) uniqueTL.add(summaries.league); - // } - // Also i need previous Tetra League State for comparison if avaliable - TetraLeague? compareWith; - if (states[1].length >= 2 || states[0].length >= 2){ - compareWith = states[1].length >= 2 ? states[1].elementAtOrNull(states.length - 2) : null; - chartsData = [for (List s in states) >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in s) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in s) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in s) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), - ]]; - }else{ - compareWith = null; - chartsData = []; - } - - if (prefs.getBool("updateInBG") == true) { - backgroundUpdate = Timer(me.cachedUntil!.difference(DateTime.now()), () { - changePlayer(me.userId); - }); - } - return [me, summaries, news, tlStream, recentZenith, recentZenithEX, states[currentSeason-1]]; - //return [me, records, states, tlMatches, compareWith, isTracking, news, topTR, recent, sprint, blitz, tlMatches.elementAtOrNull(0)?.timestamp]; - } - - /// Triggers widgets rebuild - void _justUpdate() { - setState(() {}); - } - - void toggleZenith(){ - setState(() {zenithEX = !zenithEX;}); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - bool bigScreen = MediaQuery.of(context).size.width > 1024; - return Scaffold( - drawer: widget.player == null ? NavDrawer(changePlayer) : null, // Side menu hidden if player provided - drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.2, // 20% of left side of the screen used of Drawer gesture - appBar: AppBar( - title: _showSearchBar ? SearchBox(onSubmit: changePlayer, bigScreen: MediaQuery.of(context).size.width > 768) : Text(title, style: const TextStyle(shadows: textShadow)), - backgroundColor: Colors.black, - actions: widget.player == null ? [ // search bar and PopupMenuButton hidden if player provided TODO: Subject to change - _showSearchBar - ? IconButton( - onPressed: () { - setState(() { - _showSearchBar = false; - }); - }, - icon: const Icon(Icons.clear), - tooltip: t.closeSearch, - ) - : IconButton( - onPressed: () { - setState(() { - _showSearchBar = true; - }); - }, - icon: const Icon(Icons.search), - tooltip: t.openSearch, - ), - PopupMenuButton( - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - value: "refresh", - child: Text(t.refresh), - ), - PopupMenuItem( - value: "history", - child: Text(t.fetchAndsaveTLHistory), - ), - PopupMenuItem( - value: "TLmatches", - child: Text(t.fetchAndSaveOldTLmatches), - ), - PopupMenuItem( - value: "/states", - child: Text(t.showStoredData), - ), - PopupMenuItem( - value: "/calc", - child: Text(t.statsCalc), - ), - PopupMenuItem( - value: "/settings", - child: Text(t.settings), - ), - ], - onSelected: (value) { - switch (value){ - case "refresh": - changePlayer(_searchFor); - break; - case "history": - changePlayer(_searchFor, fetchHistory: true); - break; - case "TLmatches": - changePlayer(_searchFor, fetchTLmatches: true); - break; - default: - context.go(value); - } - }, - ), - ] : null, - ), - body: SafeArea( - child: FutureBuilder>( - future: me, - 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 RefreshIndicator( - onRefresh: () { - return Future(() => changePlayer(snapshot.data![0].userId)); - }, - notificationPredicate: (notification) { - // with NestedScrollView local(depth == 2) OverscrollNotification are not sent - if (!kIsWeb && (notification is OverscrollNotification || Platform.isIOS)) { - return notification.depth == 2; - } - return notification.depth == 0; - }, - child: NestedScrollView( - scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false, physics: const AlwaysScrollableScrollPhysics()), - controller: _scrollController, - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: UserThingy( - player: snapshot.data![0], - showStateTimestamp: false, - setState: _justUpdate, - )), - SliverToBoxAdapter( - child: TabBar( - controller: bigScreen ? _wideScreenTabController : _tabController, - padding: const EdgeInsets.all(0.0), - isScrollable: true, - tabs: bigScreen ? [ - Tab(text: t.tetraLeague,), - Tab(text: t.history), - Tab(text: t.quickPlay), - Tab(text: "${t.sprint} & ${t.blitz}"), - Tab(text: t.other), - ] : [ - Tab(text: t.tetraLeague), - Tab(text: t.tlRecords), - Tab(text: t.history), - Tab(text: t.quickPlay), - Tab(text: "${t.quickPlay} ${t.recent}"), - Tab(text: t.sprint), - Tab(text: t.blitz), - Tab(text: t.recentRuns), - Tab(text: t.other), - ], - ), - ), - ]; - }, - body: TabBarView( - controller: bigScreen ? _wideScreenTabController : _tabController, - children: bigScreen ? [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: MediaQuery.of(context).size.width-450, - constraints: const BoxConstraints(maxWidth: 1024), - child: TLThingy( - tl: snapshot.data![1].league, - userID: snapshot.data![0].userId, - states: snapshot.data![6], - //topTR: snapshot.data![7]?.tr, - //lastMatchPlayed: snapshot.data![11], - bot: snapshot.data![0].role == "bot", - guest: snapshot.data![0].role == "anon", - thatRankCutoff: thatRankCutoff, - thatRankCutoffGlicko: thatRankGlickoCutoff, - thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, - nextRankCutoff: nextRankCutoff, - nextRankCutoffGlicko: nextRankGlickoCutoff, - nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, - averages: rankAverages, - lbPositions: meAmongEveryone - ), - ), - SizedBox( - width: 450, - child: _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true) - ), - ],), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: MediaQuery.of(context).size.width-450, - constraints: const BoxConstraints(maxWidth: 1024), - child: SingleChildScrollView(child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)) - ), - SizedBox( - width: 450.0, - child: _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true), - ) - ], - ), - _TwoRecordsThingy(sprint: snapshot.data![1].sprint, blitz: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, recent: SingleplayerStream(userId: "userId", records: [], type: "recent"), sprintStream: SingleplayerStream(userId: "userId", records: [], type: "40l"), blitzStream: SingleplayerStream(userId: "userId", records: [], type: "blitz")), - _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![2]) - ] : [ - TLThingy( - tl: snapshot.data![1].league, - userID: snapshot.data![0].userId, - states: const [], //snapshot.data![2], - //topTR: snapshot.data![7]?.tr, - //lastMatchPlayed: snapshot.data![11], - bot: snapshot.data![0].role == "bot", - guest: snapshot.data![0].role == "anon", - thatRankCutoff: thatRankCutoff, - thatRankCutoffGlicko: thatRankGlickoCutoff, - thatRankTarget: snapshot.data![1].league.rank != "z" ? rankTargets[snapshot.data![1].league.rank] : null, - nextRankCutoff: nextRankCutoff, - nextRankCutoffGlicko: nextRankGlickoCutoff, - nextRankTarget: (snapshot.data![1].league.rank != "z" && snapshot.data![1].league.rank != "x") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![1].league.rank)+1)] : null, - averages: rankAverages, - lbPositions: meAmongEveryone - ), - _TLRecords(userID: snapshot.data![0].userId, changePlayer: changePlayer, data: snapshot.data![3].records, wasActiveInTL: true, oldMathcesHere: _TLHistoryWasFetched, separateScrollController: true), - _History(chartsData: chartsData, changePlayer: changePlayer, userID: _searchFor, update: _justUpdate, wasActiveInTL: snapshot.data![1].league.gamesPlayed > 0), - SingleChildScrollView(child: ZenithThingy(record: snapshot.data![1].zenith, recordEX: snapshot.data![1].zenithEx, parentZenithToggle: toggleZenith, initEXvalue: zenithEX)), - _ZenithRecords(userID: snapshot.data![0].userId, data: snapshot.data![zenithEX ? 5 : 4], separateScrollController: true), - SingleplayerRecord(record: snapshot.data![1].sprint, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "40l")), - SingleplayerRecord(record: snapshot.data![1].blitz, rank: snapshot.data![1].league.percentileRank, stream: SingleplayerStream(userId: "userId", records: [], type: "Blitz")), - _RecentSingleplayersThingy(SingleplayerStream(userId: "userId", records: [], type: "recent")), - _OtherThingy(zen: snapshot.data![1].zen, bio: snapshot.data![0].bio, distinguishment: snapshot.data![0].distinguishment, newsletter: snapshot.data![2]) - ], - ), - ), - ); - } else if (snapshot.hasError) { - String errText = ""; - String? subText; - switch (snapshot.error.runtimeType){ - case TetrioPlayerNotExist: - errText = t.errors.noSuchUser; - subText = t.errors.noSuchUserSub; - break; - case TetrioDiscordNotExist: - errText = t.errors.discordNotAssigned; - subText = t.errors.discordNotAssignedSub; - case ConnectionIssue: - var err = snapshot.error as ConnectionIssue; - errText = t.errors.connection(code: err.code, message: err.message); - break; - case TetrioForbidden: - 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 = snapshot.error.toString(); - subText = snapshot.stackTrace.toString(); - } - return Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - 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, 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)); - }, - ), - ), - ); - } -} - -class NavDrawer extends StatefulWidget { - final Function changePlayer; - - /// Thing, that shows from the left side of the view. - /// Requires [changePlayer] function in order to be able to change players on main view - const NavDrawer(this.changePlayer, {super.key}); - - @override - State createState() => _NavDrawerState(); -} - -class _NavDrawerState extends State { - String homePlayerNickname = "Checking..."; - @override - void initState() { - super.initState(); - _setHomePlayerNickname(prefs.getString("player")); - } - - @override - void dispose() { - super.dispose(); - } - - /// Sets username for home button in NavDrawer. - /// Accepts [id] or username. If it's not provided, sets my nickname. - /// Otherwise, sets username or [id] if failed to find - Future _setHomePlayerNickname(String? id) async { - if (id != null) { - try { - homePlayerNickname = await teto.getNicknameByID(id); - } on TetrioPlayerNotExist { - homePlayerNickname = id; - } - } else { - homePlayerNickname = "dan63047"; - } - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Drawer( - child: StreamBuilder( - stream: teto.allPlayers, - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - final allPlayers = (snapshot.data != null) - ? snapshot.data as Map - : {}; - allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted - List keys = allPlayers.keys.toList(); - return NestedScrollView( - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: DrawerHeader( - child: Text(t.playersYouTrack, style: const TextStyle(color: Colors.white, fontSize: 25), - ))), - SliverToBoxAdapter( - child: ListTile( // Home button - leading: const Icon(Icons.home), - title: Text(homePlayerNickname), - onTap: () { - widget.changePlayer(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // changes player on main view to the one from preferences - Navigator.of(context).pop(); // and then NavDrawer closes itself. - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( // Leaderboard button - leading: const Icon(Icons.leaderboard), - title: Text(t.tlLeaderboard), - onTap: () { - context.go("/leaderboard"); - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( // Rank averages button - leading: const Icon(Icons.compress), - title: Text(t.rankAveragesViewTitle), - onTap: () { - context.go("/LBvalues"); - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( // Rank averages button - leading: const Icon(Icons.bar_chart), - title: Text(t.sprintAndBlitsViewTitle), - onTap: () { - context.go("/sprintAndBlitzAverages"); - }, - ), - ), - const SliverToBoxAdapter(child: Divider()) - ]; - }, - body: ListView.builder( // Builds list of tracked players. - itemCount: allPlayers.length, - itemBuilder: (context, index) { - var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. - return ListTile( - title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states - onTap: () { - widget.changePlayer(keys[i]); // changes to chosen player - Navigator.of(context).pop(); // and closes itself. - }, - ); - })); - case ConnectionState.done: - return const Center(child: Text('done case of StreamBuilder')); // what if that thing breaks? - } - }, - ), - ); - } -} - -class _TLRecords extends StatelessWidget { - final String userID; - final Function changePlayer; - final List data; - final bool wasActiveInTL; - final bool oldMathcesHere; - final bool separateScrollController; - - /// Widget, that displays Tetra League records. - /// Accepts list of TL records ([data]) and [userID] of player from the view - const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere, this.separateScrollController = false}); - - @override - Widget build(BuildContext context) { - if (data.isEmpty) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) - ], - )); - } - bool bigScreen = MediaQuery.of(context).size.width >= 768; - int length = data.length; - return ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: separateScrollController ? ScrollController() : null, - itemCount: oldMathcesHere ? length : length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == length) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) - ], - )); - } - - var accentColor = data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins > data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins ? Colors.green : Colors.red; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - stops: const [0, 0.05], - colors: [accentColor, Colors.transparent] - ) - ), - child: ListTile( - leading: Text("${data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins} : ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins}", - style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), - title: Text("vs. ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).username}"), - subtitle: Text(timestamp(data[index].ts), style: const TextStyle(color: Colors.grey)), - trailing: TrailingStats( - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.apm, - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.pps, - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.vs, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.apm, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.pps, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.vs, - ), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), - ), - ); - }); - } -} - -class _ZenithRecords extends StatelessWidget { - final String userID; - final SingleplayerStream data; - final bool separateScrollController; - - /// Widget, that displays Quick Play records. - /// Accepts list of TL records ([data]) and [userID] of player from the view - const _ZenithRecords({required this.userID, required this.data, this.separateScrollController = false}); - - @override - Widget build(BuildContext context) { - if (data.records.isEmpty) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - ], - )); - } - bool bigScreen = MediaQuery.of(context).size.width >= 768; - int length = data.records.length; - return ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: separateScrollController ? ScrollController() : null, - itemCount: length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == length) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - ], - )); - } - const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100, fontSize: 13); - return Container( - child: ListTile( - leading: Text("QP", - style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), - title: Text("${f2.format(data.records[index].stats.zenith!.altitude)} m${(data.records[index].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (data.records[index].extras as ZenithExtras).mods.length)})" : ""}"), - subtitle: Text(timestamp(data.records[index].timestamp), style: const TextStyle(color: Colors.grey)), - trailing: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text("${f2.format(data.records[index].aggregateStats.apm)} APM, ${f2.format(data.records[index].aggregateStats.pps)} PPS", style: style, textAlign: TextAlign.right), - Text("${f2.format(data.records[index].stats.cps)} CSP (${f2.format(data.records[index].stats.zenith!.peakrank)} peak)", style: style, textAlign: TextAlign.right), - Text("${data.records[index].stats.kills} KO's, ${getMoreNormalTime(data.records[index].stats.finalTime)}", style: style, textAlign: TextAlign.right) - ], - ), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => ZenithRecordView(record: data.records[index]))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), - ), - ); - }); - } -} - -class _History extends StatelessWidget{ - final List>>> chartsData; - final String userID; - final Function update; - final Function changePlayer; - final bool wasActiveInTL; - - /// Widget, that can show history of some stat of the player on the graph. - /// Requires player [states], which is list of states and function [update], which rebuild widgets - const _History({required this.chartsData, required this.userID, required this.changePlayer, required this.update, required this.wasActiveInTL}); - - @override - Widget build(BuildContext context) { - if (chartsData.isEmpty) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) - ], - )); - } - bool bigScreen = MediaQuery.of(context).size.width > 768; - List<_HistoryChartSpot> selectedGraph = chartsData[_season][_chartsIndex].value!; - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("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) { - _season = value!; - update(); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: const [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], - value: _gamesPlayedInsteadOfDateAndTime, - onChanged: (value) { - _gamesPlayedInsteadOfDateAndTime = value!; - update(); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: chartsData[_season], - value: chartsData[_season][_chartsIndex].value, - onChanged: (value) { - _chartsIndex = chartsData[_season].indexWhere((element) => element.value == value); - update(); - } - ), - ], - ), - if (selectedGraph.length > 300) Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox(value: _smooth, - checkColor: Colors.black, - onChanged: ((value) { - _smooth = value!; - update(); - })), - Text(t.smooth, style: const TextStyle(color: Colors.white, fontSize: 22)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - if(chartsData[_season][_chartsIndex].value!.length > 1) _HistoryChartThigy(data: selectedGraph, smooth: _smooth, yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(), xFormat: NumberFormat.compact()) - else if (chartsData[_season][_chartsIndex].value!.length <= 1) Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL && _season == 0) Text(t.errors.actionSuggestion), - if (wasActiveInTL && _season == 0) TextButton(onPressed: (){changePlayer(userID, fetchHistory: true);}, child: Text(t.fetchAndsaveTLHistory)) - ], - )) - ], - ), - ); - } -} - -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 _HistoryChartThigy extends StatefulWidget{ - final List<_HistoryChartSpot> data; - final bool smooth; - final String yAxisTitle; - final bool bigScreen; - final double leftSpace; - final NumberFormat yFormat; - final NumberFormat? xFormat; - - /// Implements graph for the _History widget. Requires [data] which is a list of dots for the graph. [yAxisTitle] used to keep track of changes. - /// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets number format - /// for left titles - const _HistoryChartThigy({required this.data, required this.smooth, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat, this.xFormat}); - - @override - State<_HistoryChartThigy> createState() => _HistoryChartThigyState(); -} - -class _HistoryChartThigyState extends State<_HistoryChartThigy> { - late String previousAxisTitle; - late bool previousGamesPlayedInsteadOfDateAndTime; - late TooltipBehavior _tooltipBehavior; - - - @override - void initState(){ - super.initState(); - _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( - "${f4.format(data.stat)} ${widget.yAxisTitle}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), - ), - ), - Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) - ], - ), - ); - } - ); - previousAxisTitle = widget.yAxisTitle; - previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; - } - - @override - void dispose(){ - super.dispose(); -} - - @override - Widget build(BuildContext context) { - if ((previousAxisTitle != widget.yAxisTitle) || (previousGamesPlayedInsteadOfDateAndTime != _gamesPlayedInsteadOfDateAndTime)) { - previousAxisTitle = widget.yAxisTitle; - previousGamesPlayedInsteadOfDateAndTime = _gamesPlayedInsteadOfDateAndTime; - setState((){}); - } - EdgeInsets padding = widget.bigScreen ? const EdgeInsets.fromLTRB(40, 30, 40, 30) : const EdgeInsets.fromLTRB(0, 40, 16, 48); - return SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height - 104, - child: Padding( padding: padding, - 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: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, - ), - margin: const EdgeInsets.all(0), - series: [ - if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( - enableTooltip: true, - dataSource: widget.data, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (widget.data.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ) - else StepLineSeries<_HistoryChartSpot, DateTime>( - enableTooltip: true, - dataSource: widget.data, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (widget.data.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ), - ], - ), - ), - ) - ); - } -} - -class _TwoRecordsThingy extends StatelessWidget { - final RecordSingle? sprint; - final RecordSingle? blitz; - final SingleplayerStream recent; - final SingleplayerStream sprintStream; - final SingleplayerStream blitzStream; - final String? rank; - - const _TwoRecordsThingy({required this.sprint, required this.blitz, this.rank, required this.recent, required this.sprintStream, required this.blitzStream}); - - Color getColorOfRank(int rank){ - if (rank == 1) return Colors.yellowAccent; - if (rank == 2) return Colors.blueGrey; - if (rank == 3) return Colors.brown[400]!; - if (rank <= 9) return Colors.blueAccent; - if (rank <= 99) return Colors.greenAccent; - return Colors.grey; - } - - @override - Widget build(BuildContext context) { - late MapEntry closestAverageBlitz; - late bool blitzBetterThanClosestAverage; - bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && blitz != null) ? blitz!.stats.score > blitzAverages[rank]! : null; - late MapEntry closestAverageSprint; - late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+" && sprint != null) ? sprint!.stats.finalTime < sprintAverages[rank]! : null; - if (sprint != null) { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-sprint!.stats.finalTime).abs() < (b -sprint!.stats.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = sprint!.stats.finalTime < closestAverageSprint.value; - } - if (blitz != null){ - closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-blitz!.stats.score).abs() < (b -blitz!.stats.score).abs() ? a : b)); - blitzBetterThanClosestAverage = blitz!.stats.score > closestAverageBlitz.value; - } - return SingleChildScrollView(child: Padding( - padding: const EdgeInsets.only(top: 20.0), - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - crossAxisAlignment: WrapCrossAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding(padding: const EdgeInsets.only(right: 8.0), - child: sprint != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: sprint != null ? get40lTime(sprint!.stats.finalTime.inMicroseconds) : "---", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: sprint != null ? Colors.white : Colors.grey), - //children: [TextSpan(text: get40lTime(record!.stats.finalTime.inMicroseconds), style: TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ), - if (sprint != null) RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(sprint!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )), - TextSpan(text: "№${sprint!.rank}", style: TextStyle(color: getColorOfRank(sprint!.rank))), - const TextSpan(text: " • "), - TextSpan(text: timestamp(sprint!.timestamp)), - ] - ), - ), - ],), - ], - ), - if (sprint != null) Wrap( - //mainAxisSize: MainAxisSize.max, - alignment: WrapAlignment.spaceBetween, - spacing: 20, - children: [ - StatCellNum(playerStat: sprint!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: sprint!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: sprint!.stats.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - ], - ), - if (sprint != null) FinesseThingy(sprint?.stats.finesse, sprint?.stats.finessePercentage), - if (sprint != null) LineclearsThingy(sprint!.stats.clears, sprint!.stats.lines, sprint!.stats.holds, sprint!.stats.tSpins), - if (sprint != null) Text("${sprint!.stats.inputs} KP • ${f2.format(sprint!.stats.kps)} KPS"), - if (sprint != null) Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${sprint!.replayId}"));}, child: Text(t.openSPreplay)), - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${sprint!.replayId}"));}, child: Text(t.downloadSPreplay)), - ], - ), - if (sprintStream.records.length > 1) SizedBox( - width: 400, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (int i = 1; i < sprintStream.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: sprintStream.records[i]))), - leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), - title: Text(get40lTime(sprintStream.records[i].stats.finalTime.inMicroseconds), - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(sprintStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(sprintStream.records[i], sprintStream.records[i].gamemode) - ) - ], - ), - ) - ] - ), - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), - children: [ - TextSpan(text: blitz != null ? NumberFormat.decimalPattern().format(blitz!.stats.score) : "---"), - //WidgetSpan(child: Image.asset("res/icons/kagari.png", height: 48)) - ] - ), - ), - if (blitz != null) RichText( - textAlign: TextAlign.end, - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(blitz!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )), - TextSpan(text: timestamp(blitz!.timestamp)), - const TextSpan(text: " • "), - TextSpan(text: "№${blitz!.rank}", style: TextStyle(color: getColorOfRank(blitz!.rank))), - ] - ), - ), - ],), - Padding(padding: const EdgeInsets.only(left: 8.0), - child: blitz != null ? Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) : Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96)), - ], - ), - if (blitz != null) Wrap( - //mainAxisSize: MainAxisSize.max, - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - StatCellNum(playerStat: blitz!.stats.level, playerStatLabel: t.statCellNum.level, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: blitz!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: true, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: blitz!.stats.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: true, higherIsBetter: true) - ], - ), - if (blitz != null) FinesseThingy(blitz?.stats.finesse, blitz?.stats.finessePercentage), - if (blitz != null) LineclearsThingy(blitz!.stats.clears, blitz!.stats.lines, blitz!.stats.holds, blitz!.stats.tSpins), - if (blitz != null) Text("${blitz!.stats.piecesPlaced} P • ${blitz!.stats.inputs} KP • ${f2.format(blitz!.stats.kpp)} KPP • ${f2.format(blitz!.stats.kps)} KPS"), - if (blitz != null) Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${blitz!.replayId}"));}, child: Text(t.openSPreplay)), - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${blitz!.replayId}"));}, child: Text(t.downloadSPreplay)), - ], - ), - if (blitzStream.records.length > 1) SizedBox( - width: 400, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (int i = 1; i < blitzStream.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: blitzStream.records[i]))), - leading: Text("#${i+1}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) ), - title: Text("${NumberFormat.decimalPattern().format(blitzStream.records[i].stats.score)} points", - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(blitzStream.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(blitzStream.records[i], blitzStream.records[i].gamemode) - ) - ], - ), - ) - ], - ), - SizedBox( - width: 400, - child: RecentSingleplayerGames(recent: recent), - ) - ]), - )); - } -} - -class _RecentSingleplayersThingy extends StatelessWidget { - final SingleplayerStream recent; - - const _RecentSingleplayersThingy(this.recent); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: RecentSingleplayerGames(recent: recent, hideTitle: true) - ); - } -} - -class _OtherThingy extends StatelessWidget { - final TetrioZen? zen; - final String? bio; - final Distinguishment? distinguishment; - final News? newsletter; - - /// Widget, that shows players [distinguishment], [bio], [zen] and [newsletter] - const _OtherThingy({required this.zen, required this.bio, required this.distinguishment, this.newsletter}); - - /// Distinguishment title is not very predictable thing. - /// Receives [text], which is header and returns sets of widgets for RichText widget - List getDistinguishmentTitle(String? text) { - // TWC champions don't have header in their distinguishments - if (distinguishment?.type == "twc") return [const TextSpan(text: "TETR.IO World Champion", 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 [const TextSpan(text: "Header is missing", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.redAccent))]; - - // Handling placeholders for logos - var exploded = text.split(" "); // wtf PHP reference? - List 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 "${distinguishment?.detail} TETR.IO World Championship"; - // In case if it missing for some other reason, return this - if (text == null) return "Footer is missing"; - // If everything ok, return as it is - return text; - } - - /// Handles [news] entry and returns widget that contains this entry - ListTile getNewsTile(NewsEntry news){ - Map gametypes = { - "40l": t.sprint, - "blitz": t.blitz, - "5mblast": "5,000,000 Blast", - "zenith": "Quick Play", - "zenithex": "Quick Play Expert", - }; - - // Individuly handle each entry type - switch (news.type) { - case "leaderboard": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.leaderboardStart, - children: [ - TextSpan(text: "№${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.leaderboardMiddle), - TextSpan(text: "№${gametypes[news.data["gametype"]]}", style: const TextStyle(fontWeight: FontWeight.bold)), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - ); - case "personalbest": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.personalbest, - children: [ - TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.personalbestMiddle), - TextSpan(text: switch (news.data["gametype"]){ - "blitz" => NumberFormat.decimalPattern().format(news.data["result"]), - "40l" => get40lTime((news.data["result"]*1000).floor()), - "5mblast" => get40lTime((news.data["result"]*1000).floor()), - "zenith" => "${f2.format(news.data["result"])} m.", - "zenithex" => "${f2.format(news.data["result"])} m.", - _ => "unknown" - }, - style: const TextStyle(fontWeight: FontWeight.bold) - ), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/improvement-local.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "badge": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.badgeStart, - children: [ - TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.badgeEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_badges/${news.data["type"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "rankup": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.rankupStart, - children: [ - TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.rankupEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter_gift": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterGiftStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - default: // if type is unknown - return ListTile( - title: Text(t.newsParts.unknownNews(type: news.type)), - subtitle: Text(timestamp(news.timestamp)), - ); - } - } - - Widget getShit(BuildContext context, bool bigScreen, bool showNewsTitle){ - return Column( - children: [ - if (distinguishment != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), - child: Column( - children: [ - Text(t.distinguishment, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28), textAlign: TextAlign.center), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: getDistinguishmentTitle(distinguishment?.header), - ), - ), - Text(getDistinguishmentSubtitle(distinguishment?.footer), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), - ], - ), - ), - if (bio != null) - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 48), - child: Column( - children: [ - Text(t.bio, style: TextStyle(fontFamily: "Eurostile Round Extended",fontSize: bigScreen ? 42 : 28)), - MarkdownBody(data: bio!, styleSheet: MarkdownStyleSheet(textScaleFactor: 1.5, textAlign: WrapAlignment.center)) // Text(bio!, style: const TextStyle(fontSize: 18)), - ], - ), - ), - if (zen != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), - child: Column( - children: [ - Text(t.zen, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Text("${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), - Text("${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: const TextStyle(fontSize: 18)), - Container( - constraints: const BoxConstraints(maxWidth: 300.0), - child: Row(children: [ - const Text("Score requirement to level up:"), - const Spacer(), - Text(intf.format(zen!.scoreRequirement)) - ],), - ) - ], - ), - ), - if (newsletter != null && newsletter!.news.isNotEmpty && showNewsTitle) - Text(t.news, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - ], - ); - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - if (constraints.maxWidth >= 1024){ - return Row( - children: [ - SizedBox(width: 450, child: getShit(context, true, false)), - SizedBox(width: constraints.maxWidth - 450, child: ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: newsletter!.news.length+1, - itemBuilder: (BuildContext context, int index) { - return index == 0 ? Center(child: Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42))) : getNewsTile(newsletter!.news[index-1]); - } - )) - ] - ); - } - else { - return ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: newsletter!.news.length+1, - itemBuilder: (BuildContext context, int index) { - return index == 0 ? getShit(context, bigScreen, true) : getNewsTile(newsletter!.news[index-1]); - }, - ); - } - }); - } -} +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.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/summaries.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.dart'; +import 'package:tetra_stats/data_objects/tetrio_players_leaderboard.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/views/destination_calculator.dart'; +import 'package:tetra_stats/views/destination_cutoffs.dart'; +import 'package:tetra_stats/views/destination_graphs.dart'; +import 'package:tetra_stats/views/destination_home.dart'; +import 'package:tetra_stats/views/destination_info.dart'; +import 'package:tetra_stats/views/destination_leaderboards.dart'; +import 'package:tetra_stats/views/destination_saved_data.dart'; +import 'package:tetra_stats/views/destination_settings.dart'; +import 'package:tetra_stats/main.dart'; + +late Future _data; +TetrioPlayersLeaderboard? _everyone; +late RangeValues currentRangeValues; +int destination = 0; + +// TODO: Redesign some widgets, so they could look nice on mobile view +// - stats below TL progress bar & similar parts in other widgets +// - APP and VS/APM gadget +// - different design for radar graphs +// - i should put tooltips everywhere +Future getData(String searchFor, {bool withHistory = false}) async { + TetrioPlayer player; + try{ + if (searchFor.startsWith("ds:")){ + player = await teto.fetchPlayer(searchFor.substring(3), isItDiscordID: true); // we trying to get him with that + }else{ + player = await teto.fetchPlayer(searchFor); // Otherwise it's probably a user id or username + } + + }on TetrioPlayerNotExist{ + return FetchResults(false, null, [], null, null, null, null, null, false, TetrioPlayerNotExist()); + } + late Summaries summaries; + late News? news; + late Cutoffs? cutoffs; + late CutoffsTetrio? averages; + try { + List requests = await Future.wait([ + teto.fetchSummaries(player.userId), + teto.fetchNews(player.userId), + teto.fetchCutoffsBeanserver(), + if (prefs.getBool("showAverages") ?? true) teto.fetchCutoffsTetrio() + ]); + + summaries = requests[0]; + news = requests[1]; + cutoffs = requests.elementAtOrNull(2); + averages = requests.elementAtOrNull(3); + + if(withHistory) await teto.fetchAndsaveTLHistory(player.userId, 1); // Retrieve if needed + } on Exception catch (e) { + return FetchResults(false, null, [], null, null, null, null, null, false, e); + } + PlayerLeaderboardPosition? _meAmongEveryone; + if (prefs.getBool("showPositions") == true){ + // Get tetra League leaderboard + _everyone = teto.getCachedLeaderboard(); + _everyone ??= await teto.fetchTLLeaderboard(); + if (_everyone!.leaderboard.isNotEmpty){ + _meAmongEveryone = await compute(_everyone!.getLeaderboardPosition, {player.userId: summaries.league}); + if (_meAmongEveryone != null) teto.cacheLeaderboardPositions(player.userId, _meAmongEveryone); + } + } + List states = await teto.getStates(player.userId, season: currentSeason); + + bool isTracking = await teto.isPlayerTracking(player.userId); + if (isTracking){ // if tracked - save data to local DB + await teto.storeState(summaries.league); + } + + return FetchResults(true, player, states.reversed.toList(), summaries, news, cutoffs, averages, _meAmongEveryone, isTracking, null); +} + +class MainView extends StatefulWidget { + final String? player; + /// The very first view, that user see when he launch this programm. + /// By default it loads my or defined in preferences user stats, but + /// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu. + const MainView({super.key, this.player}); + + @override + State createState() => _MainState(); +} + +enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} +enum CardMod {info, records, ex, exRecords} +Map cardsTitles = { + Cards.overview: t.homeNavigation.overview, + Cards.tetraLeague: t.gamemodes["league"]!, + Cards.quickPlay: t.gamemodes["zenith"]!, + //Cards.quickPlayExpert: "${t.quickPlay} ${t.expert}", + Cards.sprint: t.gamemodes["zenith"]!, + Cards.blitz: t.gamemodes["zenithex"]!, + //Cards.other: t.other +}; + +late ScrollController controller; + +class _MainState extends State with TickerProviderStateMixin { + String _searchFor = "6098518e3d5155e6ec429cdc"; + final TextEditingController _searchController = TextEditingController(); + Timer _backgroundUpdate = Timer(const Duration(days: 365), (){}); + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + void initState() { + teto.open(); + controller = ScrollController(); + changePlayer(prefs.getString('playerID')??_searchFor); + + if (prefs.getBool("updateInBG") == true) { + _backgroundUpdate = Timer(Duration(minutes: 5), () { + changePlayer(_searchFor); + }); + } + + super.initState(); + } + + void changePlayer(String player) { + setState(() { + currentRangeValues = const RangeValues(0, 1); + _searchFor = player; + _data = getData(_searchFor); + }); + } + + @override + void dispose() { + controller.dispose(); + _searchController.dispose(); + _backgroundUpdate.cancel(); + super.dispose(); + } + + NavigationRailDestination getDestinationButton(IconData icon, String title){ + return NavigationRailDestination( + icon: Tooltip( + message: title, + child: Icon(icon) + ), + selectedIcon: Icon(icon), + label: Text(title), + ); + } + + NavigationDestination getMobileDestinationButton(IconData icon, String title){ + return NavigationDestination( + icon: Tooltip( + message: title, + child: Icon(icon) + ), + selectedIcon: Icon(icon), + label: title, + ); + } + + Widget pickers(int destination){ + return switch (destination) { + 0 => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SegmentedButton( + showSelectedIcon: false, + selected: {cardMod}, + segments: modeButtons[rightCard]!, + onSelectionChanged: (p0) { + setState(() { + cardMod = p0.first; + }); + }, + ), + ), + SegmentedButton( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: Cards.overview, + tooltip: t.homeNavigation.overview, + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Cards.tetraLeague, + tooltip: t.gamemodes["league"], + icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.quickPlay, + tooltip: t.gamemodes["zenith"], + icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.sprint, + tooltip: t.gamemodes["40l"], + icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ButtonSegment( + value: Cards.blitz, + tooltip: t.gamemodes["blitz"], + icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), + ], + selected: {rightCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + cardMod = CardMod.info; + rightCard = newSelection.first; + });}) + ], + ), + 1 => SegmentedButton( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: Graph.history, + label: Text(t.graphsNavigation.history)), + ButtonSegment( + value: Graph.leagueState, + label: Text(t.graphsNavigation.league)), + ButtonSegment( + value: Graph.leagueCutoffs, + label: Text(t.graphsNavigation.cutoffs), + ), + ], + selected: {graph}, + onSelectionChanged: (Set newSelection) { + setState(() { + graph = newSelection.first; + switch (newSelection.first){ + case Graph.leagueCutoffs: + case Graph.history: + Ychart = Stats.tr; + case Graph.leagueState: + Ychart = Stats.apm; + } + });}), + 4 => SegmentedButton( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: CalcCards.calc, + label: Text(t.calcNavigation.stats), + ), + ButtonSegment( + value: CalcCards.damage, + label: Text(t.calcNavigation.damage), + ), + ], + selected: {calcCard}, + onSelectionChanged: (Set newSelection) { + setState(() { + calcCard = newSelection.first; + });}), + _ => Container() + }; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints){ + bool screenIsBig = constraints.maxWidth > 1030.00; + return Scaffold( + key: _scaffoldKey, + drawer: SearchDrawer(changePlayer: changePlayer, controller: _searchController), + endDrawer: DestinationsDrawer(changeDestination: (value) {setState(() {destination = value;});}), + persistentFooterButtons: screenIsBig ? null : [Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton.filled( + icon: const Icon(Icons.search), + style: IconButton.styleFrom( + visualDensity: VisualDensity(horizontal: 1.0, vertical: 1.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))), + backgroundColor: Theme.of(context).colorScheme.primary, + fixedSize: Size(52, 52) + ), + onPressed: () { + _scaffoldKey.currentState!.openDrawer(); + _searchController.clear(); + }, + ), + IconButton( + onPressed: (){ + _scaffoldKey.currentState!.openEndDrawer(); + }, + style: IconButton.styleFrom( + visualDensity: VisualDensity(horizontal: 1.0, vertical: 1.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))), + ), + icon: Icon(Icons.menu) + ), + Expanded(child: pickers(destination)), + ], + )], + persistentFooterAlignment: AlignmentDirectional.bottomCenter, + body: SafeArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (screenIsBig) TweenAnimationBuilder( + child: NavigationRail( + leading: FloatingActionButton( + elevation: 0, + onPressed: () { + _scaffoldKey.currentState!.openDrawer(); + _searchController.clear(); + }, + child: const Icon(Icons.search), + ), + trailing: IconButton( + tooltip: t.refresh, + onPressed: () { + changePlayer(_searchFor); + }, + icon: const Icon(Icons.refresh), + ), + destinations: [ + getDestinationButton(Icons.home, t.destinations.home), + getDestinationButton(Icons.data_thresholding_outlined, t.destinations.graphs), + getDestinationButton(Icons.leaderboard, t.destinations.leaderboards), + getDestinationButton(Icons.compress, t.destinations.cutoffs), + getDestinationButton(Icons.calculate, t.destinations.calc), + getDestinationButton(Icons.info_outline, t.destinations.info), + getDestinationButton(Icons.storage, t.destinations.data), + getDestinationButton(Icons.settings, t.destinations.settings), + ], + selectedIndex: destination, + onDestinationSelected: (value) { + setState(() { + destination = value; + }); + }, + ), + duration: Durations.long4, + tween: Tween(begin: 0, end: 1), + curve: Easing.standard, + builder: (context, value, child) { + return Container( + transform: Matrix4.translationValues(-80+value*80, 0, 0), + child: Opacity(opacity: value, child: child), + ); + }, + ), + Expanded( + child: switch (destination){ + 0 => DestinationHome(searchFor: _searchFor, constraints: constraints, dataFuture: _data, noSidebar: !screenIsBig), + 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints, noSidebar: !screenIsBig), + 2 => DestinationLeaderboards(constraints: constraints, noSidebar: !screenIsBig), + 3 => DestinationCutoffs(constraints: constraints), + 4 => DestinationCalculator(constraints: constraints), + 5 => DestinationInfo(constraints: constraints), + 6 => DestinationSavedData(constraints: constraints), + 7 => DestinationSettings(constraints: constraints), + _ => Text("Unknown destination $destination") + }, + ) + ] + ), + )); + } + ); + } +} + +class SearchDrawer extends StatefulWidget{ + final Function changePlayer; + final TextEditingController controller; + const SearchDrawer({super.key, required this.changePlayer, required this.controller}); + + @override + State createState() => _SearchDrawerState(); +} + +class _SearchDrawerState extends State { + @override + Widget build(BuildContext context) { + return Drawer( + child: SafeArea( + child: StreamBuilder( + stream: teto.allPlayers, + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.done: + case ConnectionState.active: + final allPlayers = (snapshot.data != null) + ? snapshot.data as Map + : {}; + allPlayers.remove(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted + List keys = allPlayers.keys.toList(); + return NestedScrollView( + headerSliverBuilder: (BuildContext context, bool value){ + return [ + SliverToBoxAdapter( + child: SearchBar( + controller: widget.controller, + hintText: t.searchHint, + hintStyle: const WidgetStatePropertyAll(TextStyle(color: Colors.grey)), + trailing: [ + IconButton(onPressed: (){setState(() { + widget.changePlayer(widget.controller.value.text); + Navigator.of(context).pop(); + });}, icon: const Icon(Icons.search)) + ], + onSubmitted: (value) { + setState(() { + widget.changePlayer(value); + Navigator.of(context).pop(); + }); + }, + autoFocus: true, + ), + ), + SliverToBoxAdapter( + child: ListTile( + leading: Icon(Icons.home), + title: Text(prefs.getString("player") ?? "dan63"), + onTap: () { + widget.changePlayer(prefs.getString("playerID") ?? "6098518e3d5155e6ec429cdc"); + Navigator.of(context).pop(); + }, + ), + ), + SliverToBoxAdapter( + child: Divider(), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text(t.trackedPlayers, style: Theme.of(context).textTheme.headlineLarge), + ), + ) + ]; + }, + body: ListView.builder( // Builds list of tracked players. + itemCount: allPlayers.length, + itemBuilder: (context, index) { + var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. + return ListTile( + title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states + trailing: IconButton(onPressed: (){ + teto.deletePlayerToTrack(keys[i]); + }, icon: Icon(Icons.delete, color: Colors.grey)), + onTap: () { + widget.changePlayer(keys[i]); // changes to chosen player + Navigator.of(context).pop(); // and closes itself. + }, + ); + }) + ); + } + } + ), + ) + ); + } +} + +class DestinationsDrawer extends StatefulWidget{ + final Function changeDestination; + + const DestinationsDrawer({super.key, required this.changeDestination}); + + @override + State createState() => _DestinationsDrawerState(); + +} + +class _DestinationsDrawerState extends State{ + @override + Widget build(BuildContext context) { + return Drawer( + child: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool value){ + return [ + SliverToBoxAdapter( + child: DrawerHeader( + child: Text(t.navMenu, style: const TextStyle(color: Colors.white, fontSize: 25), + ))) + ]; + }, + body: ListView( + children: [ + ListTile( + leading: Icon(Icons.home), + title: Text(t.destinations.home), + onTap: (){ + widget.changeDestination(0); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(Icons.data_thresholding_outlined), + title: Text(t.destinations.graphs), + onTap: (){ + widget.changeDestination(1); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(Icons.leaderboard), + title: Text(t.destinations.leaderboards), + onTap: (){ + widget.changeDestination(2); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(Icons.compress), + title: Text(t.destinations.cutoffs), + onTap: (){ + widget.changeDestination(3); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(Icons.calculate), + title: Text(t.destinations.calc), + onTap: (){ + widget.changeDestination(4); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(Icons.info_outline), + title: Text(t.destinations.info), + onTap: (){ + widget.changeDestination(5); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(Icons.storage), + title: Text(t.destinations.data), + onTap: (){ + widget.changeDestination(6); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(Icons.settings), + title: Text(t.destinations.settings), + onTap: (){ + widget.changeDestination(7); + Navigator.of(context).pop(); + }, + ), + ], + ) + ) + ); + } +} + +// class EstTrThingy extends StatelessWidget{ +// final EstTr estTr; + +// const EstTrThingy({super.key, required this.estTr}); + +// @override +// Widget build(BuildContext context) { +// return const Card( +// //child: , +// ); +// } +// } \ No newline at end of file diff --git a/lib/views/main_view_tiles.dart b/lib/views/main_view_tiles.dart deleted file mode 100644 index c749d5b..0000000 --- a/lib/views/main_view_tiles.dart +++ /dev/null @@ -1,2934 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide Badge; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:intl/intl.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/tetra_stats.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/services/crud_exceptions.dart'; -import 'package:tetra_stats/utils/colors_functions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/singleplayer_record_view.dart'; -import 'package:tetra_stats/views/tl_match_view.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; -import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; -import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; -import 'package:tetra_stats/main.dart'; -import 'package:tetra_stats/widgets/tl_progress_bar.dart'; -import 'package:tetra_stats/widgets/user_thingy.dart'; - -var fDiff = NumberFormat("+#,###.####;-#,###.####"); - -class MainView extends StatefulWidget { - final String? player; - /// The very first view, that user see when he launch this programm. - /// By default it loads my or defined in preferences user stats, but - /// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu. - const MainView({super.key, this.player}); - - @override - State createState() => _MainState(); -} - -enum Page {home, leaderboards, leagueAverages, calculator, settings} -enum Cards {overview, tetraLeague, quickPlay, sprint, blitz} -enum CardMod {info, records, ex, exRecords} -Map cardsTitles = { - Cards.overview: "Overview", - Cards.tetraLeague: t.tetraLeague, - Cards.quickPlay: t.quickPlay, - //Cards.quickPlayExpert: "${t.quickPlay} ${t.expert}", - Cards.sprint: t.sprint, - Cards.blitz: t.blitz, - //Cards.other: t.other -}; - -late ScrollController controller; - -class _MainState extends State with TickerProviderStateMixin { - int destination = 0; - String _searchFor = "6098518e3d5155e6ec429cdc"; - final TextEditingController _searchController = TextEditingController(); - - @override - void initState() { - teto.open(); - controller = ScrollController(); - super.initState(); - } - - void changePlayer(String player) { - setState(() { - _searchFor = player; - }); - } - - @override - void dispose() { - controller.dispose(); - _searchController.dispose(); - super.dispose(); - } - - NavigationRailDestination getDestinationButton(IconData icon, String title){ - return NavigationRailDestination( - icon: Tooltip( - message: title, - child: Icon(icon) - ), - selectedIcon: Icon(icon), - label: Text(title), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - drawer: SearchDrawer(changePlayer: changePlayer, controller: _searchController), - body: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - NavigationRail( - leading: FloatingActionButton( - elevation: 0, - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - child: const Icon(Icons.search), - ), - trailing: IconButton( - onPressed: () { - // Add your onPressed code here! - }, - icon: const Icon(Icons.more_horiz_rounded), - ), - destinations: [ - getDestinationButton(Icons.home, "Home"), - getDestinationButton(Icons.data_thresholding_outlined, "Graphs"), - getDestinationButton(Icons.leaderboard, "Leaderboards"), - getDestinationButton(Icons.compress, "Cutoffs"), - getDestinationButton(Icons.calculate, "Calc"), - getDestinationButton(Icons.storage, "Saved Data"), - getDestinationButton(Icons.settings, "Settings"), - ], - selectedIndex: destination, - onDestinationSelected: (value) { - setState(() { - destination = value; - }); - }, - ), - Expanded( - child: switch (destination){ - 0 => DestinationHome(searchFor: _searchFor, constraints: constraints), - 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), - 2 => DestinationLeaderboards(constraints: constraints), - _ => Text("Unknown destination $destination") - }, - ) - ]); - }, - )); - } -} - -class DestinationLeaderboards extends StatefulWidget{ - final BoxConstraints constraints; - - const DestinationLeaderboards({super.key, required this.constraints}); - - @override - State createState() => _DestinationLeaderboardsState(); -} - -class _DestinationLeaderboardsState extends State { - Cards rightCard = Cards.tetraLeague; - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - final List leaderboards = ["Tetra League", "Quick Play", "Quick Play Expert"]; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - width: 350.0, - height: widget.constraints.maxHeight, - child: Column( - children: [ - const Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Spacer(), - Text("Leaderboards", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36)), - Spacer() - ], - ), - ), - Expanded( - child: ListView.builder( - itemCount: leaderboards.length, - itemBuilder: (BuildContext context, int index) { - return Card( - surfaceTintColor: theme.colorScheme.primary, - child: ListTile( - title: Text(leaderboards[index]), - ), - ); - } - ), - ), - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 350 - 88, - child: const Card( - child: Column( - children: [ - - ], - ), - ), - ), - ], - ); - } -} - -class DestinationGraphs extends StatefulWidget{ - final String searchFor; - //final Function setState; - final BoxConstraints constraints; - - const DestinationGraphs({super.key, required this.searchFor, required this.constraints}); - - @override - State createState() => _DestinationGraphsState(); -} - -class _DestinationGraphsState extends State { - Cards rightCard = Cards.tetraLeague; - bool fetchData = false; - bool _gamesPlayedInsteadOfDateAndTime = false; - late ZoomPanBehavior _zoomPanBehavior; - late TooltipBehavior _tooltipBehavior; - String yAxisTitle = ""; - bool _smooth = false; - final List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR", "Opener", "Plonk", "Inf. DS", "Stride"]; - int _chartsIndex = 0; - late List>> chartsData; - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - - @override - void initState(){ - _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( - "${f4.format(data.stat)} $yAxisTitle", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20), - ), - ), - Text(_gamesPlayedInsteadOfDateAndTime ? t.gamesPlayed(games: t.games(n: data.gamesPlayed)) : timestamp(data.timestamp)) - ], - ), - ); - } - ); - _zoomPanBehavior = ZoomPanBehavior( - enablePinching: true, - enableSelectionZooming: true, - enableMouseWheelZooming : true, - enablePanning: true, - ); - super.initState(); - } - - Future>>> getChartsData(bool fetchHistory) async { - List states = []; - Set uniqueTL = {}; - - if(fetchHistory){ - try{ - var history = await teto.fetchAndsaveTLHistory(widget.searchFor); - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.fetchAndsaveTLHistoryResult(number: history.length)))); - }on TetrioHistoryNotExist{ - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.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))); - } - } - - //states.addAll(await teto.getPlayer(widget.searchFor)); - // for (var element in states) { - // if (element.tlSeason1 != null && uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1!); - // if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1!); - // } - - if (uniqueTL.length >= 2){ - chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.tr)], child: Text(t.statCellNum.tr)), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.glicko!)], child: const Text("Glicko")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.rd!)], child: const Text("Rating Deviation")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.apm != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.apm!)], child: Text(t.statCellNum.apm.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.pps != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.pps!)], child: Text(t.statCellNum.pps.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.vs != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.vs!)], child: Text(t.statCellNum.vs.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.app)], child: Text(t.statCellNum.app.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dss)], child: Text(t.statCellNum.dss.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.dsp)], child: Text(t.statCellNum.dsp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.appdsp)], child: const Text("APP + DS/P")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.vsapm)], child: const Text("VS/APM")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.cheese)], child: Text(t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.gbe)], child: Text(t.statCellNum.gbe.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.nyaapp)], child: Text(t.statCellNum.nyaapp.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.nerdStats != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.nerdStats!.area)], child: Text(t.statCellNum.area.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.estTr != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.estTr!.esttr)], child: Text(t.statCellNum.estOfTR.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.esttracc != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.esttracc!)], child: Text(t.statCellNum.accOfEst.replaceAll(RegExp(r'\n'), " "))), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.opener)], child: const Text("Opener")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.plonk)], child: const Text("Plonk")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.infds)], child: const Text("Inf. DS")), - DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.playstyle != null) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.playstyle!.stride)], child: const Text("Stride")), - ]; - }else{ - chartsData = []; - } - - fetchData = false; - - return chartsData; - } - - @override - Widget build(BuildContext context) { - return FutureBuilder>>>( - future: getChartsData(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 && snapshot.data!.isNotEmpty){ - List<_HistoryChartSpot> selectedGraph = snapshot.data![_chartsIndex].value!; - yAxisTitle = _historyShortTitles[_chartsIndex]; - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - child: Wrap( - spacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("X:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: const [DropdownMenuItem(value: false, child: Text("Date & Time")), DropdownMenuItem(value: true, child: Text("Games Played"))], - value: _gamesPlayedInsteadOfDateAndTime, - onChanged: (value) { - setState(() { - _gamesPlayedInsteadOfDateAndTime = value!; - }); - } - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding(padding: EdgeInsets.all(8.0), child: Text("Y:", style: TextStyle(fontSize: 22))), - DropdownButton( - items: chartsData, - value: chartsData[_chartsIndex].value, - onChanged: (value) { - setState(() { - _chartsIndex = chartsData.indexWhere((element) => element.value == value); - }); - } - ), - ], - ), - if (selectedGraph.length > 300) 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)) - ], - ), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) - ], - ), - ), - if(chartsData[_chartsIndex].value!.length > 1) Card( - child: SizedBox( - width: MediaQuery.of(context).size.width - 88, - height: MediaQuery.of(context).size.height - 60, - child: Padding( padding: const EdgeInsets.fromLTRB(40, 30, 40, 30), - child: SfCartesianChart( - tooltipBehavior: _tooltipBehavior, - zoomPanBehavior: _zoomPanBehavior, - primaryXAxis: _gamesPlayedInsteadOfDateAndTime ? const NumericAxis() : const DateTimeAxis(), - primaryYAxis: const NumericAxis( - rangePadding: ChartRangePadding.additional, - ), - margin: const EdgeInsets.all(0), - series: [ - if (_gamesPlayedInsteadOfDateAndTime) StepLineSeries<_HistoryChartSpot, int>( - enableTooltip: true, - dataSource: chartsData[_chartsIndex].value!, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.gamesPlayed, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (chartsData[_chartsIndex].value!.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ) - else StepLineSeries<_HistoryChartSpot, DateTime>( - enableTooltip: true, - dataSource: chartsData[_chartsIndex].value!, - animationDuration: 0, - opacity: _smooth ? 0 : 1, - xValueMapper: (_HistoryChartSpot data, _) => data.timestamp, - yValueMapper: (_HistoryChartSpot data, _) => data.stat, - color: Theme.of(context).colorScheme.primary, - trendlines:[ - Trendline( - isVisible: _smooth, - period: (chartsData[_chartsIndex].value!.length/175).floor(), - type: TrendlineType.movingAverage, - color: Theme.of(context).colorScheme.primary) - ], - ), - ], - ), - ) - ), - ) - else if (chartsData[_chartsIndex].value!.length <= 1) Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - Text(t.errors.actionSuggestion), - TextButton(onPressed: (){setState(() { - fetchData = true; - });}, child: Text(t.fetchAndsaveTLHistory)) - ], - )) - ], - ), - ); - } - if (snapshot.hasError || snapshot.data!.isEmpty){ - return Center(child: - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text(snapshot.error != null ? snapshot.error.toString() : t.noHistorySaved, 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 != null ? snapshot.stackTrace.toString() : "lol", textAlign: TextAlign.center), - ), - ], - ) - ); - } - } - return const Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("lol", style: TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - ], - )); - }, - ); - } -} - -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 DestinationHome extends StatefulWidget{ - final String searchFor; - //final Function setState; - final BoxConstraints constraints; - - const DestinationHome({super.key, required this.searchFor, required this.constraints}); - - @override - State createState() => _DestinationHomeState(); -} - -class FetchResults{ - bool success; - TetrioPlayer? player; - Summaries? summaries; - Cutoffs? cutoffs; - Exception? exception; - - FetchResults(this.success, this.player, this.summaries, this.cutoffs, this.exception); -} - -class RecordSummary extends StatelessWidget{ - final RecordSingle? record; - final bool hideRank; - final bool? betterThanRankAverage; - final MapEntry? closestAverage; - final bool? betterThanClosestAverage; - final String? rank; - - const RecordSummary({super.key, required this.record, this.betterThanRankAverage, this.closestAverage, this.betterThanClosestAverage, this.rank, this.hideRank = false}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (closestAverage != null && record != null) Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage!.key}.png", height: 96)) - else !hideRank ? Image.asset("res/tetrio_tl_alpha_ranks/z.png", height: 96) : Container(), - if (record != null) Column( - crossAxisAlignment: hideRank ? CrossAxisAlignment.center : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RichText( - textAlign: hideRank ? TextAlign.center : TextAlign.start, - text: TextSpan( - text: switch(record!.gamemode){ - "40l" => get40lTime(record!.stats.finalTime.inMicroseconds), - "blitz" => NumberFormat.decimalPattern().format(record!.stats.score), - "5mblast" => get40lTime(record!.stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(record!.stats.zenith!.altitude)} m", - "zenithex" => "${f2.format(record!.stats.zenith!.altitude)} m", - _ => record!.stats.score.toString() - }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white, height: 0.9), - ), - ), - RichText( - textAlign: hideRank ? TextAlign.center : TextAlign.start, - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ - "40l" => readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), - "blitz" => readableIntDifference(record!.stats.score, blitzAverages[rank]!), - _ => record!.stats.score.toString() - }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if ((rank == null || rank == "z") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record!.gamemode){ - "40l" => readableTimeDifference(record!.stats.finalTime, closestAverage!.value), - "blitz" => readableIntDifference(record!.stats.score, closestAverage!.value), - _ => record!.stats.score.toString() - }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage!.key.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )), - if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), - if (record!.rank != -1 && record!.countryRank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), - const TextSpan(text: "\n"), - TextSpan(text: timestamp(record!.timestamp)), - ] - ), - ), - ], - ) else if (hideRank) RichText(text: const TextSpan( - text: "---", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.grey), - ), - ) - ], - ); - } - -} - -class _DestinationHomeState extends State { - Cards rightCard = Cards.overview; - CardMod cardMod = CardMod.info; - //Duration postSeasonLeft = seasonStart.difference(DateTime.now()); - late Map>> modeButtons; - late MapEntry? closestAverageBlitz; - late bool blitzBetterThanClosestAverage; - late MapEntry? closestAverageSprint; - late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage; - bool? blitzBetterThanRankAverage; - - Future _getData() async { - TetrioPlayer player; - try{ - if (widget.searchFor.startsWith("ds:")){ - player = await teto.fetchPlayer(widget.searchFor.substring(3), isItDiscordID: true); // we trying to get him with that - }else{ - player = await teto.fetchPlayer(widget.searchFor); // Otherwise it's probably a user id or username - } - }on TetrioPlayerNotExist{ - return FetchResults(false, null, null, null, TetrioPlayerNotExist()); - } - late Summaries summaries; - late Cutoffs cutoffs; - List requests = await Future.wait([ - teto.fetchSummaries(player.userId), - teto.fetchCutoffsBeanserver(), - ]); - summaries = requests[0]; - cutoffs = requests[1]; - return FetchResults(true, player, summaries, cutoffs, null); - } - - Widget getOverviewCard(Summaries summaries){ - return Column( - children: [ - const Card( - child: Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Overview", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - ), - ), - ), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - TLRatingThingy(userID: "", tlData: summaries.league, showPositions: true), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("${summaries.league.apm != null ? f2.format(summaries.league.apm) : "-.--"} APM • ${summaries.league.pps != null ? f2.format(summaries.league.pps) : "-.--"} PPS • ${summaries.league.vs != null ? f2.format(summaries.league.vs) : "-.--"} VS • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.app) : "-.--"} APP • ${summaries.league.nerdStats != null ? f2.format(summaries.league.nerdStats!.vsapm) : "-.--"} VS/APM", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("40 Lines", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - RecordSummary(record: summaries.sprint, betterThanClosestAverage: sprintBetterThanClosestAverage, betterThanRankAverage: sprintBetterThanRankAverage, closestAverage: closestAverageSprint, rank: summaries.league.percentileRank), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("${summaries.sprint != null ? intf.format(summaries.sprint!.stats.piecesPlaced) : "---"} P • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.pps) : "---"} PPS • ${summaries.sprint != null ? f2.format(summaries.sprint!.stats.kpp) : "---"} KPP", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Blitz", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - RecordSummary(record: summaries.blitz, betterThanClosestAverage: blitzBetterThanClosestAverage, betterThanRankAverage: blitzBetterThanRankAverage, closestAverage: closestAverageBlitz, rank: summaries.league.percentileRank), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("Level ${summaries.blitz != null ? intf.format(summaries.blitz!.stats.level): "--"} • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.spp) : "-.--"} SPP • ${summaries.blitz != null ? f2.format(summaries.blitz!.stats.pps) : "---"} PPS", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("QP", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - RecordSummary(record: summaries.zenith, hideRank: true), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 18).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 18).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("QP Expert", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - RecordSummary(record: summaries.zenithEx, hideRank: true,), - const Divider(color: Color.fromARGB(50, 158, 158, 158)), - Text("Overall PB: ${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 19).v != null) ? f2.format(summaries.achievements.firstWhere((e) => e.k == 19).v!) : "-.--"} m", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 8.0, 20.0, 14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("Zen", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, height: 0.9)), - Text("Level ${intf.format(summaries.zen.level)}", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), - Text("Score ${intf.format(summaries.zen.score)}"), - Text("Level up requirement: ${intf.format(summaries.zen.scoreRequirement)}", style: const TextStyle(color: Colors.grey)) - ], - ), - ), - ), - ), - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - const Text("f", style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 65, - height: 1.2, - )), - const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text("${(summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? - f3.format(summaries.achievements.firstWhere((e) => e.k == 4).v!/summaries.achievements.firstWhere((e) => e.k == 1).v! * 100) : "--.---"}%", style: const TextStyle( - //shadows: textShadow, - fontFamily: "Eurostile Round Extended", - fontSize: 36, - fontWeight: FontWeight.w500, - color: Colors.white - )), - ) - ], - ), - Row( - children: [ - const Text("Total pieces placed:"), - const Spacer(), - Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 1).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 1).v!) : "---"), - ], - ), - Row( - children: [ - const Text(" - Placed with perfect finesse:"), - const Spacer(), - Text((summaries.achievements.isNotEmpty && summaries.achievements.firstWhere((e) => e.k == 4).v != null) ? intf.format(summaries.achievements.firstWhere((e) => e.k == 4).v!) : "---"), - ], - ) - ], - ), - ), - ), - ), - ], - ), - if (summaries.achievements.isNotEmpty) Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0), - child: Column( - children: [ - if (summaries.achievements.firstWhere((e) => e.k == 16).v != null) Row( - children: [ - const Text("Total height climbed in QP"), - const Spacer(), - Text("${f2.format(summaries.achievements.firstWhere((e) => e.k == 16).v!)} m"), - ], - ), - if (summaries.achievements.firstWhere((e) => e.k == 17).v != null) Row( - children: [ - const Text("KO's in QP"), - const Spacer(), - Text(intf.format(summaries.achievements.firstWhere((e) => e.k == 17).v!)), - ], - ) - ], - ), - ), - ), - ] - ); - } - - Widget getTetraLeagueCard(TetraLeague data, Cutoffs? cutoffs){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.tetraLeague, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - TetraLeagueThingy(league: data, cutoffs: cutoffs), - if (data.nerdStats != null) Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - if (data.nerdStats != null) NerdStatsThingy(nerdStats: data.nerdStats!), - if (data.nerdStats != null) GraphsThingy(nerdStats: data.nerdStats!, playstyle: data.playstyle!, apm: data.apm!, pps: data.pps!, vs: data.vs!) - ], - ); - } - - Widget getListOfRecords(String recentStream, String topStream, BoxConstraints constraints){ - return Column( - children: [ - const Card( - child: Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("Records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("${t.seasonStarts} ${countdown(postSeasonLeft)}", textAlign: TextAlign.center) - ], - ), - ), - ), - ), - Card( - clipBehavior: Clip.antiAlias, - child: DefaultTabController(length: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const TabBar( - tabs: [ - Tab(text: "Recent"), - Tab(text: "Top"), - ], - ), - SizedBox( - height: 400, - child: TabBarView( - children: [ - FutureBuilder( - future: teto.fetchStream(widget.searchFor, recentStream), - 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 Column( - children: [ - for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), - leading: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => "40L", - "blitz" => "BLZ", - "5mblast" => "5MB", - "zenith" => "QP", - "zenithex" => "QPE", - String() => "huh", - }, - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), - "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - String() => "huh", - }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) - ) - ], - ); - } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } - } - return const Text("what?"); - }, - ), - FutureBuilder( - future: teto.fetchStream(widget.searchFor, topStream), - 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 Column( - children: [ - for (int i = 0; i < snapshot.data!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: snapshot.data!.records[i]))), - leading: Text( - "#${i+1}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (snapshot.data!.records[i].gamemode){ - "40l" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(snapshot.data!.records[i].stats.score)), - "5mblast" => get40lTime(snapshot.data!.records[i].stats.finalTime.inMicroseconds), - "zenith" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - "zenithex" => "${f2.format(snapshot.data!.records[i].stats.zenith!.altitude)} m${(snapshot.data!.records[i].extras as ZenithExtras).mods.isNotEmpty ? " (${t.withModsPlural(n: (snapshot.data!.records[i].extras as ZenithExtras).mods.length)})" : ""}", - String() => "huh", - }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(snapshot.data!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(snapshot.data!.records[i], snapshot.data!.records[i].gamemode) - ) - ], - ); - } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } - } - return const Text("what?"); - }, - ), - ] - ), - ) - ], - ), - ) - ), - ], - ); - } - - Widget getRecentTLrecords(BoxConstraints constraints){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.recent, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - ], - ), - ), - ), - ), - Card( - clipBehavior: Clip.antiAlias, - child: FutureBuilder( - future: teto.fetchTLStream(widget.searchFor), - 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 SizedBox(height: constraints.maxHeight - 145, child: _TLRecords(userID: widget.searchFor, changePlayer: (){}, data: snapshot.data!.records, wasActiveInTL: snapshot.data!.records.isNotEmpty, oldMathcesHere: false)); - } - if (snapshot.hasError){ - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } - } - return const Text("what?"); - }, - ), - ), - ], - ); - } - - Widget getZenithCard(RecordSingle? record){ - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(t.quickPlay, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - //Text("Leaderboard reset in ${countdown(postSeasonLeft)}", textAlign: TextAlign.center), - ], - ), - ), - ), - ), - ZenithThingy(zenith: record), - if (record != null) Row( - children: [ - Expanded( - child: Card( - child: Column( - children: [ - FinesseThingy(record.stats.finesse, record.stats.finessePercentage), - LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins, showMoreClears: true), - if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") - ], - ), - ), - ), - Expanded( - child: Card( - child: SizedBox( - width: 300, - height: 318, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - const Text("T", style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 65, - height: 1.2, - )), - const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text(getMoreNormalTime(record.stats.finalTime), style: const TextStyle( - shadows: textShadow, - fontFamily: "Eurostile Round Extended", - fontSize: 36, - fontWeight: FontWeight.w500, - color: Colors.white - )), - ) - ], - ), - SizedBox( - width: 300.0, - child: Table( - columnWidths: const { - 0: FixedColumnWidth(36) - }, - children: [ - const TableRow( - children: [ - Text("Floor"), - Text("Split", textAlign: TextAlign.right), - Text("Total", textAlign: TextAlign.right), - ] - ), - for (int i = 0; i < record.stats.zenith!.splits.length; i++) TableRow( - children: [ - Text((i+1).toString()), - Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]-(i-1 != -1 ? record.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), - Text(record.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), - ] - ) - ], - ), - ), - ], - ), - ), - ), - ), - ], - ), - if (record != null) Card( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - Text(t.nerdStats, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)), - const Spacer() - ], - ), - ), - if (record != null) NerdStatsThingy(nerdStats: record.aggregateStats.nerdStats), - if (record != null) GraphsThingy(nerdStats: record.aggregateStats.nerdStats, playstyle: record.aggregateStats.playstyle, apm: record.aggregateStats.apm, pps: record.aggregateStats.pps, vs: record.aggregateStats.vs) - ], - ); - } - - Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ - if (record == null) { - return const Card( - child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), - ); - } - return Column( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(switch(record.gamemode){ - "40l" => t.sprint, - "blitz" => t.blitz, - "5mblast" => "5,000,000 Blast", - _ => record.gamemode - }, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42)) - ], - ), - ), - ), - ), - Card( - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (closestAverage != null) Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverage.key}.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RichText(text: TextSpan( - text: switch(record.gamemode){ - "40l" => get40lTime(record.stats.finalTime.inMicroseconds), - "blitz" => NumberFormat.decimalPattern().format(record.stats.score), - "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), - _ => record.stats.score.toString() - }, - style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (rank != null && rank != "z" && rank != "x+") TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, sprintAverages[rank]!), - "blitz" => readableIntDifference(record.stats.score, blitzAverages[rank]!), - _ => record.stats.score.toString() - }, verdict: betterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if ((rank == null || rank == "z" || rank == "x+") && closestAverage != null) TextSpan(text: "${t.verdictGeneral(n: switch(record.gamemode){ - "40l" => readableTimeDifference(record.stats.finalTime, closestAverage.value), - "blitz" => readableIntDifference(record.stats.score, closestAverage.value), - _ => record.stats.score.toString() - }, verdict: betterThanClosestAverage??false ? t.verdictBetter : t.verdictWorse, rank: closestAverage.key.toUpperCase())}\n", style: TextStyle( - color: betterThanClosestAverage??false ? Colors.greenAccent : Colors.redAccent - )), - if (record.rank != -1) TextSpan(text: "№ ${intf.format(record.rank)}", style: TextStyle(color: getColorOfRank(record.rank))), - if (record.rank != -1) const TextSpan(text: " • "), - if (record.countryRank != -1) TextSpan(text: "№ ${intf.format(record.countryRank)} local", style: TextStyle(color: getColorOfRank(record.countryRank))), - if (record.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(record.timestamp)), - ] - ), - ), - ], - ), - ], - ), - Row( - children: [ - Expanded( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => record.stats.piecesPlaced.toString(), - "blitz" => record.stats.level.toString(), - "5mblast" => NumberFormat.decimalPattern().format(record.stats.spp), - _ => "What if " - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " Pieces", - "blitz" => " Level", - "5mblast" => " SPP", - _ => " i wanted to" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(record.stats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" PPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => f2.format(record.stats.kpp), - "blitz" => f2.format(record.stats.spp), - "5mblast" => record.stats.piecesPlaced.toString(), - _ => "but god said" - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " KPP", - "blitz" => " SPP", - "5mblast" => " Pieces", - _ => " no" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - Expanded( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(intf.format(record.stats.inputs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Key presses", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(f2.format(record.stats.kps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" KPS", textAlign: TextAlign.left, style: TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - Text(switch(record.gamemode){ - "40l" => " ", - "blitz" => record.stats.piecesPlaced.toString(), - "5mblast" => record.stats.piecesPlaced.toString(), - _ => "but god said" - }, textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - Text(switch(record.gamemode){ - "40l" => " ", - "blitz" => " Pieces", - "5mblast" => " Pieces", - _ => " no" - }, textAlign: TextAlign.left, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - ], - ) - ], - ), - ), - Card( - child: Center( - child: Column( - children: [ - FinesseThingy(record.stats.finesse, record.stats.finessePercentage), - LineclearsThingy(record.stats.clears, record.stats.lines, record.stats.holds, record.stats.tSpins), - if (record.gamemode == 'blitz') Text("${f2.format(record.stats.kpp)} KPP") - ], - ), - ), - ) - ] - ); - } - - @override - initState(){ - modeButtons = { - Cards.overview: [ - const ButtonSegment( - value: CardMod.info, - label: Text('General'), - ), - ], - Cards.tetraLeague: [ - const ButtonSegment( - value: CardMod.info, - label: Text('Standing'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Recent Matches'), - ), - ], - Cards.quickPlay: [ - const ButtonSegment( - value: CardMod.info, - label: Text('Normal'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ), - const ButtonSegment( - value: CardMod.ex, - label: Text('Expert'), - ), - const ButtonSegment( - value: CardMod.exRecords, - label: Text('Expert Records'), - ) - ], - Cards.blitz: [ - const ButtonSegment( - value: CardMod.info, - label: Text('PB'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ) - ], - Cards.sprint: [ - const ButtonSegment( - value: CardMod.info, - label: Text('PB'), - ), - const ButtonSegment( - value: CardMod.records, - label: Text('Records'), - ) - ] - }; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _getData(), - 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 Center(child: - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.errors.noSuchUser, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(t.errors.noSuchUserSub, textAlign: TextAlign.center), - ), - ], - ) - ); - } - if (snapshot.hasData){ - blitzBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.blitz != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.blitz!.stats.score > blitzAverages[snapshot.data!.summaries!.league.rank]! : null; - sprintBetterThanRankAverage = (snapshot.data!.summaries!.league.rank != "z" && snapshot.data!.summaries!.sprint != null && snapshot.data!.summaries!.league.rank != "x+") ? snapshot.data!.summaries!.sprint!.stats.finalTime < sprintAverages[snapshot.data!.summaries!.league.rank]! : null; - if (snapshot.data!.summaries!.sprint != null) { - closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.sprint!.stats.finalTime).abs() < (b -snapshot.data!.summaries!.sprint!.stats.finalTime).abs() ? a : b)); - sprintBetterThanClosestAverage = snapshot.data!.summaries!.sprint!.stats.finalTime < closestAverageSprint!.value; - } - if (snapshot.data!.summaries!.blitz != null){ - closestAverageBlitz = blitzAverages.entries.singleWhere((element) => element.value == blitzAverages.values.reduce((a, b) => (a-snapshot.data!.summaries!.blitz!.stats.score).abs() < (b -snapshot.data!.summaries!.blitz!.stats.score).abs() ? a : b)); - blitzBetterThanClosestAverage = snapshot.data!.summaries!.blitz!.stats.score > closestAverageBlitz!.value; - } - return Row( - children: [ - SizedBox( - width: 450, - child: Column( - children: [ - NewUserThingy(player: snapshot.data!.player!, showStateTimestamp: false, setState: setState), - if (snapshot.data!.player!.badges.isNotEmpty) BadgesThingy(badges: snapshot.data!.player!.badges), - if (snapshot.data!.player!.distinguishment != null) DistinguishmentThingy(snapshot.data!.player!.distinguishment!), - if (snapshot.data!.player!.role == "bot") FakeDistinguishmentThingy(bot: true, botMaintainers: snapshot.data!.player!.botmaster), - if (snapshot.data!.player!.role == "banned") FakeDistinguishmentThingy(banned: true) - else if (snapshot.data!.player!.badstanding == true) FakeDistinguishmentThingy(badStanding: true), - if (snapshot.data!.player!.bio != null) Card( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.bio, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ], - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: MarkdownBody(data: snapshot.data!.player!.bio!, styleSheet: MarkdownStyleSheet(textAlign: WrapAlignment.center)), - ) - ], - ), - ), - //if (testNews != null && testNews!.news.isNotEmpty) - Expanded( - child: FutureBuilder( - future: teto.fetchNews(widget.searchFor), - builder: (context, snapshot) { - switch (snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Card(child: Center(child: CircularProgressIndicator())); - case ConnectionState.done: - if (snapshot.hasData){ - return NewsThingy(snapshot.data!); - }else if (snapshot.hasError){ - return Card(child: Column(children: [ - Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - Text(snapshot.stackTrace.toString()) - ] - )); - } - } - return const Text("what?"); - } - ), - ) - ], - ), - ), - SizedBox( - width: widget.constraints.maxWidth - 450 - 80, - child: Column( - children: [ - SizedBox( - height: rightCard != Cards.overview ? widget.constraints.maxHeight - 64 : widget.constraints.maxHeight - 32, - child: SingleChildScrollView( - child: switch (rightCard){ - Cards.overview => getOverviewCard(snapshot.data!.summaries!), - Cards.tetraLeague => switch (cardMod){ - CardMod.info => getTetraLeagueCard(snapshot.data!.summaries!.league, snapshot.data!.cutoffs), - CardMod.records => getRecentTLrecords(widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.quickPlay => switch (cardMod){ - CardMod.info => getZenithCard(snapshot.data?.summaries!.zenith), - CardMod.records => getListOfRecords("zenith/recent", "zenith/top", widget.constraints), - CardMod.ex => getZenithCard(snapshot.data?.summaries!.zenithEx), - CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.sprint => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("40l/recent", "40l/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - Cards.blitz => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), - CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), - _ => const Center(child: Text("huh?")) - }, - }, - ), - ), - if (modeButtons[rightCard]!.length > 1) SegmentedButton( - showSelectedIcon: false, - selected: {cardMod}, - segments: modeButtons[rightCard]!, - onSelectionChanged: (p0) { - setState(() { - cardMod = p0.first; - }); - }, - ), - SegmentedButton( - showSelectedIcon: false, - segments: >[ - const ButtonSegment( - value: Cards.overview, - //label: Text('Overview'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Cards.tetraLeague, - //label: Text('Tetra League'), - icon: SvgPicture.asset("res/icons/league.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.quickPlay, - //label: Text('Quick Play'), - icon: SvgPicture.asset("res/icons/qp.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.sprint, - //label: Text('40 Lines'), - icon: SvgPicture.asset("res/icons/40l.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ButtonSegment( - value: Cards.blitz, - //label: Text('Blitz'), - icon: SvgPicture.asset("res/icons/blitz.svg", height: 16, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.modulate))), - ], - selected: {rightCard}, - onSelectionChanged: (Set newSelection) { - setState(() { - cardMod = CardMod.info; - rightCard = newSelection.first; - });}) - ], - ) - ) - ], - ); - } - } - return const Text("End of FutureBuilder"); - }, - ); - } -} - -class NewsThingy extends StatelessWidget{ - final News news; - - const NewsThingy(this.news, {super.key}); - - ListTile getNewsTile(NewsEntry news){ - Map gametypes = { - "40l": t.sprint, - "blitz": t.blitz, - "5mblast": "5,000,000 Blast", - "zenith": "Quick Play", - "zenithex": "Quick Play Expert", - }; - - // Individuly handle each entry type - switch (news.type) { - case "leaderboard": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.leaderboardStart, - children: [ - TextSpan(text: "№${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.leaderboardMiddle), - TextSpan(text: "№${gametypes[news.data["gametype"]]}", style: const TextStyle(fontWeight: FontWeight.bold)), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - ); - case "personalbest": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.personalbest, - children: [ - TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.personalbestMiddle), - TextSpan(text: switch (news.data["gametype"]){ - "blitz" => NumberFormat.decimalPattern().format(news.data["result"]), - "40l" => get40lTime((news.data["result"]*1000).floor()), - "5mblast" => get40lTime((news.data["result"]*1000).floor()), - "zenith" => "${f2.format(news.data["result"])} m.", - "zenithex" => "${f2.format(news.data["result"])} m.", - _ => "unknown" - }, - style: const TextStyle(fontWeight: FontWeight.bold) - ), - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/improvement-local.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "badge": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.badgeStart, - children: [ - TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.badgeEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_badges/${news.data["type"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "rankup": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.rankupStart, - children: [ - TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: t.newsParts.rankupEnd) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - case "supporter_gift": - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), - text: t.newsParts.supporterGiftStart, - children: [ - TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) - ] - ) - ), - subtitle: Text(timestamp(news.timestamp)), - leading: Image.asset( - "res/icons/supporter-tag.png", - height: 48, - width: 48, - errorBuilder: (context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 64, width: 64); - }, - ), - ); - default: // if type is unknown - return ListTile( - title: Text(t.newsParts.unknownNews(type: news.type)), - subtitle: Text(timestamp(news.timestamp)), - ); - } - } - - @override - Widget build(BuildContext context) { - return Card( - child: SingleChildScrollView( - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended")), - const Spacer() - ] - ), - if (news.news.isEmpty) const Center(child: Text("Empty list")) - else for (NewsEntry entry in news.news) getNewsTile(entry) - ], - ), - ), - ); - } - -} - -class DistinguishmentThingy extends StatelessWidget{ - final Distinguishment distinguishment; - - const DistinguishmentThingy(this.distinguishment, {super.key}); - - List getDistinguishmentTitle(String? text) { - // TWC champions don't have header in their distinguishments - if (distinguishment.type == "twc") return [const TextSpan(text: "TETR.IO World Champion", 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 [const TextSpan(text: "Header is missing", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.redAccent))]; - - // Handling placeholders for logos - var exploded = text.split(" "); // wtf PHP reference? - List 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 "${distinguishment.detail} TETR.IO World Championship"; - // In case if it missing for some other reason, return this - if (text == null) return "Footer is missing"; - // 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), - ), - ), - Text(getDistinguishmentSubtitle(distinguishment.footer), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), - ], - ), - ); - } -} - -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 = "banned"; - if (badStanding) text = "bad standing"; - if (bot) text = "bot account"; - return TextSpan(text: text.toUpperCase(), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white)); - } - - String getDistinguishmentSubtitle(){ - if (banned) return "Bans are placed when TETR.IO rules or terms of service are broken"; - if (badStanding) return "One or more recent bans on record"; - if (bot) return "Operated by $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()], - ), - ), - ), - Text(getDistinguishmentSubtitle(), style: const TextStyle(fontSize: 18), textAlign: TextAlign.center), - ], - ), - ), - ); - } - -} - -class BadgesThingy extends StatelessWidget{ - final List badges; - - 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: [ - const Text("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( - 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.asset("res/tetrio_badges/${badge.badgeId}.png"), - Text(badge.ts != null - ? t.obtainDate(date: timestamp(badge.ts!)) - : t.assignedManualy), - ], - ) - ], - ), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ), - tooltip: badge.label, - icon: Image.asset( - "res/tetrio_badges/${badge.badgeId}.png", - height: 32, - width: 32, - errorBuilder: (context, error, stackTrace) { - return Image.network( - kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png", - height: 32, - width: 32, - errorBuilder:(context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 32, width: 32); - } - ); - }, - ) - ) - ], - ), - ) - ], - ), - ); - } -} - -class NewUserThingy extends StatelessWidget { - final TetrioPlayer player; - final bool showStateTimestamp; - final Function setState; - - const NewUserThingy({super.key, required this.player, required this.showStateTimestamp, required this.setState}); - - Color roleColor(String role){ - switch (role){ - case "sysop": - return const Color.fromARGB(255, 23, 165, 133); - case "admin": - return const Color.fromARGB(255, 255, 78, 138); - case "mod": - return const Color.fromARGB(255, 204, 128, 242); - case "halfmod": - return const Color.fromARGB(255, 95, 118, 254); - case "bot": - return const Color.fromARGB(255, 60, 93, 55); - case "banned": - return const Color.fromARGB(255, 248, 28, 28); - default: - return Colors.white10; - } - } - - String fontStyle(int length){ - if (length < 10) return "Eurostile Round Extended"; - else if (length < 13) return "Eurostile Round"; - else return "Eurostile Round Condensed"; - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return LayoutBuilder(builder: (context, constraints) { - double pfpHeight = 128; - int xpTableID = 0; - - while (player.xp > xpTableScuffed.values.toList()[xpTableID]) { - xpTableID++; - } - - return Card( - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Container( - constraints: const BoxConstraints(maxWidth: 960), - height: player.bannerRevision != null ? 218.0 : 138.0, - child: Stack( - //clipBehavior: Clip.none, - children: [ - // TODO: osk banner can cause memory leak - if (player.bannerRevision != null) Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", - fit: BoxFit.cover, - height: 120, - errorBuilder: (context, error, stackTrace) { - return Container(); - }, - ), - Positioned( - top: player.bannerRevision != null ? 90.0 : 10.0, - left: 16.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: player.role == "banned" - ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) - : player.avatarRevision != null - ? Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", - fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { - return Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight); - }) - : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight), - ) - ), - Positioned( - top: player.bannerRevision != null ? 120.0 : 40.0, - left: 160.0, - child: Tooltip( - message: "${player.userId}\n(Click to copy user ID)", - child: RichText(text: TextSpan(text: player.username, style: TextStyle( - fontFamily: fontStyle(player.username.length), - fontSize: 28, - ), - recognizer: TapGestureRecognizer()..onTap = (){ - copyToClipboard(player.userId); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); - } - ) - ) - ), - ), - Positioned( - top: player.bannerRevision != null ? 160.0 : 80.0, - left: 160.0, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Chip(label: Text(player.role.toUpperCase(), style: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(player.role))), - ), - RichText( - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: - [ - if (player.friendCount > 0) const WidgetSpan(child: Icon(Icons.person), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - if (player.friendCount > 0) TextSpan(text: "${intf.format(player.friendCount)} "), - if (player.supporterTier > 0) WidgetSpan(child: Icon(player.supporterTier > 1 ? Icons.star : Icons.star_border, color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - if (player.supporterTier > 0) TextSpan(text: player.supporterTier.toString(), style: TextStyle(color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)), - ] - ) - ) - ], - ), - ), - Positioned( - top: player.bannerRevision != null ? 193.0 : 113.0, - left: 160.0, - child: SizedBox( - width: 270, - child: RichText( - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - if (player.country != null) TextSpan(text: "${t.countries[player.country]} • "), - TextSpan(text: player.registrationTime == null ? t.wasFromBeginning : timestamp(player.registrationTime!), style: const TextStyle(color: Colors.grey)) - ] - ) - ), - ) - ), - Positioned( - top: player.bannerRevision != null ? 126.0 : 46.0, - right: 16.0, - child: RichText( - textAlign: TextAlign.end, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - TextSpan(text: "Level ${(player.level.isNegative || player.level.isNaN) ? "---" : intf.format(player.level.floor())}", style: TextStyle(decoration: (player.level.isNegative || player.level.isNaN) ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: (player.level.isNegative || player.level.isNaN) ? Colors.grey : Colors.white), recognizer: (player.level.isNegative || player.level.isNaN) ? null : TapGestureRecognizer()?..onTap = (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text("Level ${intf.format(player.level.floor())}", textAlign: TextAlign.center), - content: SingleChildScrollView( - child: ListBody(children: [ - Text( - "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(player.xp)} XP", - style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold) - ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: SfLinearGauge( - minimum: 0, - maximum: 1, - interval: 1, - ranges: [ - LinearGaugeRange(startValue: 0, endValue: player.level - player.level.floor(), color: Colors.cyanAccent), - LinearGaugeRange(startValue: 0, endValue: (player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) - ], - showTicks: true, - showLabels: false - ), - ), - Text("${t.statCellNum.xpProgress}: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"), - Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - player.xp)} ${t.statCellNum.xpLeft})") - ] - ), - ), - actions: [ - TextButton( - child: const Text("OK"), - onPressed: () {Navigator.of(context).pop();} - ) - ] - ) - ); - }), - const TextSpan(text:"\n"), - TextSpan(text: player.gameTime.isNegative ? "-h --m" : playtime(player.gameTime), style: TextStyle(color: player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(t.exactGametime, textAlign: TextAlign.center), - content: SingleChildScrollView( - child: ListBody(children: [ - Text( - "${intf.format(player.gameTime.inDays)}d ${nonsecs.format(player.gameTime.inHours%24)}h ${nonsecs.format(player.gameTime.inMinutes%60)}m ${nonsecs.format(player.gameTime.inSeconds%60)}s ${nonsecs3.format(player.gameTime.inMilliseconds%1000)}ms ${nonsecs3.format(player.gameTime.inMicroseconds%1000)}μs", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24) - ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text("It's ${f4.format(player.gameTime.inSeconds/31536000)} years,"), - ), - Text("${f4.format(player.gameTime.inSeconds/2628000)} monts,"), - Text("${f4.format(player.gameTime.inSeconds/3600)} hours,"), - Text("${f2.format(player.gameTime.inMilliseconds/60000)} minutes,"), - Text("${intf.format(player.gameTime.inSeconds)} seconds"), - ] - ), - ), - actions: [ - TextButton( - child: const Text("OK"), - onPressed: () {Navigator.of(context).pop();} - ) - ] - ) - ); - }) : null), - const TextSpan(text:"\n"), - TextSpan(text: player.gamesWon > -1 ? intf.format(player.gamesWon) : "---", style: TextStyle(color: player.gamesWon > -1 ? Colors.white : Colors.grey)), - TextSpan(text: "/${player.gamesPlayed > -1 ? intf.format(player.gamesPlayed) : "---"}", style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), - ] - ) - ) - ) - ], - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.person_add), label: Text(t.track), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0))))))), - Expanded(child: ElevatedButton.icon(onPressed: (){print("ok, and?");}, icon: const Icon(Icons.balance), label: Text(t.compare), style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))))) - ], - ) - ], - ), - ); - }); - } -} - -class SearchDrawer extends StatefulWidget{ - final Function changePlayer; - final TextEditingController controller; - const SearchDrawer({super.key, required this.changePlayer, required this.controller}); - - @override - State createState() => _SearchDrawerState(); -} - -class _SearchDrawerState extends State { - @override - Widget build(BuildContext context) { - return Drawer( - child: StreamBuilder( - stream: teto.allPlayers, - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.done: - case ConnectionState.active: - final allPlayers = (snapshot.data != null) - ? snapshot.data as Map - : {}; - allPlayers.remove(prefs.getString("player") ?? "6098518e3d5155e6ec429cdc"); // player from the home button will be delisted - List keys = allPlayers.keys.toList(); - return NestedScrollView( - headerSliverBuilder: (BuildContext context, bool value){ - return [ - SliverToBoxAdapter( - child: SearchBar( - controller: widget.controller, - hintText: "Hello", - hintStyle: const WidgetStatePropertyAll(TextStyle(color: Colors.grey)), - trailing: [ - IconButton(onPressed: (){setState(() { - widget.changePlayer(widget.controller.value.text); - Navigator.of(context).pop(); - });}, icon: const Icon(Icons.search)) - ], - onSubmitted: (value) { - setState(() { - widget.changePlayer(value); - Navigator.of(context).pop(); - }); - }, - ), - ), - SliverToBoxAdapter( - child: ListTile( - title: Text(prefs.getString("player") ?? "dan63"), - onTap: () { - widget.changePlayer("6098518e3d5155e6ec429cdc"); - Navigator.of(context).pop(); - }, - ), - ) - ]; - }, - body: ListView.builder( // Builds list of tracked players. - itemCount: allPlayers.length, - itemBuilder: (context, index) { - var i = allPlayers.length-1-index; // Last players in this map are most recent ones, they are gonna be shown at the top. - return ListTile( - title: Text(allPlayers[keys[i]]??keys[i]), // Takes last known username from list of states - onTap: () { - widget.changePlayer(keys[i]); // changes to chosen player - Navigator.of(context).pop(); // and closes itself. - }, - ); - }) - ); - } - } - ) - ); - } -} - -class TetraLeagueThingy extends StatelessWidget{ - final TetraLeague league; - final Cutoffs? cutoffs; - - const TetraLeagueThingy({super.key, required this.league, this.cutoffs}); - - @override - Widget build(BuildContext context) { - return Card( - child: Column( - children: [ - TLRatingThingy(userID: "w", tlData: league), - TLProgress( - tlData: league, - previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, - nextRankTRcutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.tr[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, - nextRankTRcutoffTarget: league.rank != "z" ? rankTargets[league.rank] : null, - previousRankTRcutoffTarget: (league.rank != "z" && league.rank != "x+") ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(league.rank)+1)] : null, - previousGlickoCutoff: cutoffs != null ? cutoffs!.glicko[league.rank != "z" ? league.rank : league.percentileRank] : null, - nextRankGlickoCutoff: cutoffs != null ? (league.rank != "z" ? league.rank == "x+" : league.percentileRank == "x+") ? 25000 : cutoffs!.glicko[ranks.elementAtOrNull(ranks.indexOf(league.rank != "z" ? league.rank : league.percentileRank)+1)] : null, - ), - Row( - // spacing: 25.0, - // alignment: WrapAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - const Text("APM: ", style: TextStyle(fontSize: 21)), - Text(f2.format(league.apm??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - const Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(f2.format(league.pps??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - const Text("VS: ", style: TextStyle(fontSize: 21)), - Text(f2.format(league.vs??0.00), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - ), - SizedBox( - height: 128.0, - width: 128.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: SfRadialGauge( - backgroundColor: Colors.black, - axes: [ - RadialAxis( - minimum: 0.0, - maximum: 1.0, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.25, - minorTicksPerInterval: 0, - ranges:[ - GaugeRange(startValue: 0, endValue: league.winrate, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - Text(percentage.format(league.winrate), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), - angle: 90,positionFactor: 0.1 - ), - GaugeAnnotation(widget: Container(child: - Text(t.statCellNum.winrate, textAlign: TextAlign.center)), - angle: 270,positionFactor: 0.4 - ) - ], - ) - ] - ), - ), - ), - Expanded( - child: Center( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - //Text("VS: ", style: TextStyle(fontSize: 21)), - Text("№ ${league.standingLocal.isNegative ? "---" : intf.format(league.standingLocal)}", textAlign: TextAlign.right, style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)), - Text(" local", style: TextStyle(fontSize: 21, color: league.standingLocal.isNegative ? Colors.grey : Colors.white)) - ]), - TableRow(children: [ - //Text("APM: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Games", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - //Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Won", style: TextStyle(fontSize: 21)) - ]) - ], - ), - ), - ), - ], - ), - ], - ), - ); - } -} - -class NerdStatsThingy extends StatelessWidget{ - final NerdStats nerdStats; - - const NerdStatsThingy({super.key, required this.nerdStats}); - - @override - Widget build(BuildContext context) { - return Card( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 0.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 256.0, - width: 256.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: SfRadialGauge( - backgroundColor: Colors.black, - axes: [ - RadialAxis( - startAngle: 200, - endAngle: 340, - minimum: 0.0, - maximum: 1.0, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.app, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "APP\n"), - TextSpan(text: f3.format(nerdStats.app), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), - //TextSpan(text: "\nAPP"), - ] - ))), - angle: 270,positionFactor: 0.5 - ), - ], - ), - RadialAxis( - startAngle: 20, - endAngle: 160, - isInversed: true, - minimum: 1.8, - maximum: 2.4, - radiusFactor: 1.01, - showTicks: true, - showLabels: false, - interval: 0.1, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: nerdStats.vsapm, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round"), - children: [ - const TextSpan(text: "VS/APM\n"), - TextSpan(text: f3.format(nerdStats.vsapm), style: const TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100)), - ] - ))), - angle: 90,positionFactor: 0.5 - ) - ], - ) - ] - ), - ), - ), - Expanded( - child: Wrap( - alignment: WrapAlignment.center, - spacing: 10, - children: [ - GaugetThingy(value: nerdStats.dss, min: 0, max: 1.0, tickInterval: .2, label: "DS/S", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.dsp, min: 0, max: 1.0, tickInterval: .2, label: "DS/P", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.appdsp, min: 0, max: 1.2, tickInterval: .2, label: "APP+DS/P", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.cheese, min: -80, max: 80, tickInterval: 40, label: "Cheese", sideSize: 128.0, fractionDigits: 2), - GaugetThingy(value: nerdStats.gbe, min: 0, max: 1.0, tickInterval: .2, label: "GbE", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: "wAPP", sideSize: 128.0, fractionDigits: 3), - GaugetThingy(value: nerdStats.area, min: 0, max: 1000, tickInterval: 100, label: "Area", sideSize: 128.0, fractionDigits: 1), - ], - ), - ) - ] - ), - ), - ], - ) - ); - } -} - -class EstTrThingy extends StatelessWidget{ - final EstTr estTr; - - const EstTrThingy({super.key, required this.estTr}); - - @override - Widget build(BuildContext context) { - return const Card( - //child: , - ); - } -} - -class GraphsThingy extends StatelessWidget{ - final double apm; - final double pps; - final double vs; - final NerdStats nerdStats; - final Playstyle playstyle; - - const GraphsThingy({super.key, required this.nerdStats, required this.playstyle, required this.apm, required this.pps, required this.vs}); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Center(child: Graphs(apm, pps, vs, nerdStats, playstyle)), - ), - ); - } - -} - -class GaugetThingy extends StatelessWidget{ - final double value; - final double min; - final double max; - final double tickInterval; - final String label; - final double sideSize; - final int fractionDigits; - - GaugetThingy({super.key, required this.value, required this.min, required this.max, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits}); - - @override - Widget build(BuildContext context) { - NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits); - return ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: SizedBox( - height: sideSize, - width: sideSize, - child: SfRadialGauge( - backgroundColor: Colors.black, - axes: [ - RadialAxis( - radiusFactor: 1.01, - minimum: min, - maximum: max, - showTicks: true, - showLabels: false, - interval: tickInterval, - //labelsPosition: ElementsPosition.outside, - ranges:[ - GaugeRange(startValue: 0, endValue: value, color: theme.colorScheme.primary) - ], - annotations: [ - GaugeAnnotation(widget: Container(child: - Text(f.format(value), textAlign: TextAlign.center, style: const TextStyle(fontSize: 25,fontWeight: FontWeight.bold))), - angle: 90,positionFactor: 0.10 - ), - GaugeAnnotation(widget: Container(child: - Text(label, textAlign: TextAlign.center, style: const TextStyle(height: .9))), - angle: 270,positionFactor: 0.4 - ) - ], - ) - ] - ), - ), - ); - } -} - -class ZenithThingy extends StatelessWidget{ - final RecordSingle? zenith; - - const ZenithThingy({super.key, required this.zenith}); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - RichText( - text: TextSpan( - text: zenith != null ? "${f2.format(zenith!.stats.zenith!.altitude)} m" : "--- m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: zenith != null ? Colors.white : Colors.grey), - ), - ), - if (zenith != null) RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (zenith!.rank != -1) TextSpan(text: "№ ${intf.format(zenith!.rank)}", style: TextStyle(color: getColorOfRank(zenith!.rank))), - if (zenith!.rank != -1) const TextSpan(text: " • "), - if (zenith!.countryRank != -1) TextSpan(text: "№ ${intf.format(zenith!.countryRank)} local", style: TextStyle(color: getColorOfRank(zenith!.countryRank))), - if (zenith!.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(zenith!.timestamp)), - ] - ), - ), - ], - ), - if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) Container(width: 16.0), - if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty) for (String mod in (zenith!.extras as ZenithExtras).mods) Image.asset("res/icons/${mod}.png", height: 64.0) - ], - ), - if (zenith != null) Row( - children: [ - Expanded( - child: Center( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - const Text("APM: ", style: TextStyle(fontSize: 21)), - Text(f2.format(zenith!.aggregateStats.apm), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - const Text("PPS: ", style: TextStyle(fontSize: 21)), - Text(f2.format(zenith!.aggregateStats.pps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - ]), - TableRow(children: [ - const Text("VS: ", style: TextStyle(fontSize: 21)), - Text(f2.format(zenith!.aggregateStats.vs), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - ]) - ], - ), - ), - ), - Expanded( - child: Center( - child: Table( - defaultColumnWidth:const IntrinsicColumnWidth(), - children: [ - TableRow(children: [ - Text(intf.format(zenith!.stats.kills), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" KO's", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - Text(f2.format(zenith!.stats.cps), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" CPS", style: TextStyle(fontSize: 21)) - ]), - TableRow(children: [ - Text(f2.format(zenith!.stats.zenith!.peakrank), textAlign: TextAlign.right, style: const TextStyle(fontSize: 21)), - const Text(" Peak CPS", style: TextStyle(fontSize: 21)) - ]) - ], - ), - ), - ), - ], - ) - ] - ), - ) - ); - } - -} - -class _TLRecords extends StatelessWidget { - final String userID; - final Function changePlayer; - final List data; - final bool wasActiveInTL; - final bool oldMathcesHere; - final bool separateScrollController; - - /// Widget, that displays Tetra League records. - /// Accepts list of TL records ([data]) and [userID] of player from the view - const _TLRecords({required this.userID, required this.changePlayer, required this.data, required this.wasActiveInTL, required this.oldMathcesHere, this.separateScrollController = false}); - - @override - Widget build(BuildContext context) { - if (data.isEmpty) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) - ], - )); - } - bool bigScreen = MediaQuery.of(context).size.width >= 768; - int length = data.length; - return ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: separateScrollController ? ScrollController() : null, - itemCount: oldMathcesHere ? length : length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == length) { - return Center(child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(t.noOldRecords(n: length), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)), - if (wasActiveInTL) Text(t.errors.actionSuggestion), - if (wasActiveInTL) TextButton(onPressed: (){changePlayer(userID, fetchTLmatches: true);}, child: Text(t.fetchAndSaveOldTLmatches)) - ], - )); - } - - var accentColor = data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins > data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins ? Colors.green : Colors.red; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - stops: const [0, 0.05], - colors: [accentColor, Colors.transparent] - ) - ), - child: ListTile( - leading: Text("${data[index].results.leaderboard.firstWhere((element) => element.id == userID).wins} : ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).wins}", - style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28, shadows: textShadow) : const TextStyle(fontSize: 28, shadows: textShadow)), - title: Text("vs. ${data[index].results.leaderboard.firstWhere((element) => element.id != userID).username}"), - subtitle: Text(timestamp(data[index].ts), style: const TextStyle(color: Colors.grey)), - trailing: TrailingStats( - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.apm, - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.pps, - data[index].results.leaderboard.firstWhere((element) => element.id == userID).stats.vs, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.apm, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.pps, - data[index].results.leaderboard.firstWhere((element) => element.id != userID).stats.vs, - ), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))) //Navigator.push(context, MaterialPageRoute(builder: (context) => TlMatchResultView(record: data[index], initPlayerId: userID))), - ), - ); - }); - } -} - -class TLRatingThingy extends StatelessWidget{ - final String userID; - final TetraLeague tlData; - final TetraLeague? oldTl; - final double? topTR; - final bool? showPositions; - final DateTime? lastMatchPlayed; - - const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed, this.showPositions}); - - @override - Widget build(BuildContext context) { - bool oskKagariGimmick = prefs.getBool("oskKagariGimmick")??true; - bool bigScreen = MediaQuery.of(context).size.width >= 768; - String decimalSeparator = f4.symbols.DECIMAL_SEP; - List formatedTR = f4.format(tlData.tr).split(decimalSeparator); - List formatedGlicko = tlData.glicko != null ? f4.format(tlData.glicko).split(decimalSeparator) : ["---","--"]; - List formatedPercentile = f4.format(tlData.percentile * 100).split(decimalSeparator); - //DateTime now = DateTime.now(); - //bool beforeS1end = now.isBefore(seasonEnd); - //int daysLeft = seasonEnd.difference(now).inDays; - //int safeRD = min(100, (100 + ((tlData.rd! >= 100 && tlData.decaying) ? 7 : max(0, 7 - (lastMatchPlayed != null ? now.difference(lastMatchPlayed!).inDays : 7))) - daysLeft).toInt()); - return Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceAround, - crossAxisAlignment: WrapCrossAlignment.center, - clipBehavior: Clip.hardEdge, - children: [ - (userID == "5e32fc85ab319c2ab1beb07c" && oskKagariGimmick) // he love her so much, you can't even imagine - ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? - : Image.asset("res/tetrio_tl_alpha_ranks/${tlData.rank}.png", height: 128), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white), - children: (tlData.gamesPlayed > 9) ? switch(prefs.getInt("ratingMode")){ - 1 => [ - TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (formatedGlicko.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedGlicko[1]), - TextSpan(text: " Glicko", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - ], - 2 => [ - TextSpan(text: "${t.top} ${formatedPercentile[0]}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (formatedPercentile.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedPercentile[1]), - TextSpan(text: " %", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - ], - _ => [ - TextSpan(text: formatedTR[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (formatedTR.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedTR[1]), - TextSpan(text: " TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) - ], - } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),] - ) - ), - if (oldTl != null) Text( - switch(prefs.getInt("ratingMode")){ - 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", - 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", - _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" - }, - textAlign: TextAlign.center, - style: TextStyle( - color: tlData.tr - oldTl!.tr < 0 ? - Colors.red : - Colors.green - ), - ), - if (tlData.gamesPlayed > 9) Column( - children: [ - RichText( - textAlign: TextAlign.center, - softWrap: true, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan(text: prefs.getInt("ratingMode") == 2 ? "${f2.format(tlData.tr)} TR • % ${t.rank}: ${tlData.percentileRank.toUpperCase()}" : "${t.top} ${f2.format(tlData.percentile * 100)}% (${tlData.percentileRank.toUpperCase()})"), - if (tlData.bestRank != "z") const TextSpan(text: " • "), - if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"), - if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"), - TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.tr)} TR • RD: " : "Glicko: ${tlData.glicko != null ? f2.format(tlData.glicko) : "---"}±"}"), - TextSpan(text: f2.format(tlData.rd!), style: tlData.decaying ? TextStyle(color: tlData.rd! > 98 ? Colors.red : Colors.yellow) : null), - if (tlData.decaying) WidgetSpan(child: Icon(Icons.trending_up, color: tlData.rd! > 98 ? Colors.red : Colors.yellow,), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - //if (beforeS1end) tlData.rd! <= safeRD ? TextSpan(text: " (Safe)", style: TextStyle(color: Colors.greenAccent)) : TextSpan(text: " (> ${safeRD} RD !!!)", style: TextStyle(color: Colors.redAccent)) - ], - ), - ), - ], - ), - if (showPositions == true) RichText( - textAlign: TextAlign.start, - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (tlData.standing != -1) TextSpan(text: "№ ${intf.format(tlData.standing)}", style: TextStyle(color: getColorOfRank(tlData.standing))), - if (tlData.standing != -1 || tlData.standingLocal != -1) const TextSpan(text: " • "), - if (tlData.standingLocal != -1) TextSpan(text: "№ ${intf.format(tlData.standingLocal)} local", style: TextStyle(color: getColorOfRank(tlData.standingLocal))), - if (tlData.standing != -1 && tlData.standingLocal != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(tlData.timestamp)), - ] - ), - ), - ], - ), - ], - ); - } -} \ No newline at end of file diff --git a/lib/views/mathes_view.dart b/lib/views/mathes_view.dart deleted file mode 100644 index fa709c8..0000000 --- a/lib/views/mathes_view.dart +++ /dev/null @@ -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 createState() => MatchesState(); -} - -class MatchesState extends State { - - @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)))], - ); - } - } - ) - ) - ); - } -} diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart deleted file mode 100644 index 14c6ad0..0000000 --- a/lib/views/rank_averages_view.dart +++ /dev/null @@ -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 = [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 _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; -List<_MyScatterSpot> _spots = []; -Stats _sortBy = Stats.tr; -late List they; -bool _reversed = false; -List _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 createState() => RankState(); -} - -class RankState extends State 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 hoveredPointId = ValueNotifier(-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); -} diff --git a/lib/views/rank_view.dart b/lib/views/rank_view.dart new file mode 100644 index 0000000..f544b65 --- /dev/null +++ b/lib/views/rank_view.dart @@ -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 createState() => _RankState(); +} + +enum CardMod{ + graph, + minimums, + maximums +} + +class _RankState extends State { + CardMod cardMod = CardMod.graph; + + Future getRanksAverages(String rank) async { + var lb = await teto.fetchTLLeaderboard(); + return lb.getRankData(rank); + } + + Widget partOfTheWidget(List? 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>( + 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"); + } + ), + ); + } + + @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>( + 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)) + ] + )), + ), + ), + ); + } + +} \ No newline at end of file diff --git a/lib/views/ranks_averages_view.dart b/lib/views/ranks_averages_view.dart deleted file mode 100644 index c2f6387..0000000 --- a/lib/views/ranks_averages_view.dart +++ /dev/null @@ -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 createState() => RanksAverages(); -} - -late String oldWindowTitle; - -class RanksAverages extends State { - - @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(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"); - } - }) - ), - ); - } -} diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart deleted file mode 100644 index 1eb96c5..0000000 --- a/lib/views/settings_view.dart +++ /dev/null @@ -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 createState() => SettingsState(); -} - -class SettingsState extends State { - 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 _setDefaultNickname(String? n) async { - if (n != null) { - try { - defaultNickname = await teto.getNicknameByID(n); - } on TetrioPlayerNotExist { - defaultNickname = n; - } - } else { - defaultNickname = "dan63047"; - } - setState(() {}); - } - - Future _setPlayer(String player) async { - await prefs.setString('player', player); - await _setDefaultNickname(player); - } - - Future _removePlayer() async { - await prefs.remove('player'); - await _setDefaultNickname("6098518e3d5155e6ec429cdc"); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - List>? locales = >[]; - for (var v in AppLocale.values){ - locales.add(DropdownMenuItem( - 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: [ - 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: [ - 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: ['db'], - ); - openFile(acceptedTypeGroups: [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: [ - 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: (){},), - // ], - // ), - ], - )), - ); - } -} diff --git a/lib/views/singleplayer_record_view.dart b/lib/views/singleplayer_record_view.dart index 2126c2f..3614dfc 100644 --- a/lib/views/singleplayer_record_view.dart +++ b/lib/views/singleplayer_record_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/views/destination_home.dart'; import 'package:tetra_stats/widgets/singleplayer_record.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; class SingleplayerRecordView extends StatelessWidget { final RecordSingle record; @@ -15,28 +15,28 @@ class SingleplayerRecordView extends StatelessWidget { //bool bigScreen = MediaQuery.of(context).size.width >= 368; return Scaffold( backgroundColor: Colors.black, - appBar: AppBar( - title: Text("${ - switch (record.gamemode){ - "40l" => t.sprint, - "blitz" => t.blitz, - String() => "5000000 Blast", - } - } ${timestamp(record.timestamp)}"), + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + floatingActionButton: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 0.0, 0.0), + child: FloatingActionButton( + onPressed: () => Navigator.pop(context), + tooltip: t.goBackButton, + child: const Icon(Icons.arrow_back), + ), ), body: SafeArea( child: SingleChildScrollView( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - SingleplayerRecord(record: record, hideTitle: true), - // TODO: Insert replay link here - ] - ) - ], + child: Center( + child: Container( + constraints: BoxConstraints( + maxWidth: 768 + ), + child: switch (record.gamemode){ + "zenith" => ZenithCard(record, false, [], width: MediaQuery.of(context).size.width), + "zenithex" => ZenithCard(record, false, [], width: MediaQuery.of(context).size.width), + _ => SingleplayerRecord(record: record, hideTitle: true) + }, + ), ) ) ), diff --git a/lib/views/sprint_and_blitz_averages.dart b/lib/views/sprint_and_blitz_averages.dart index a37fbd9..df115fa 100644 --- a/lib/views/sprint_and_blitz_averages.dart +++ b/lib/views/sprint_and_blitz_averages.dart @@ -1,12 +1,9 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:window_manager/window_manager.dart'; late String oldWindowTitle; final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode); @@ -22,16 +19,16 @@ class SprintAndBlitzState extends State { @override void initState() { - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ - windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${t.settings}"); - } + // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ + // windowManager.getTitle().then((value) => oldWindowTitle = value); + // windowManager.setTitle("Tetra Stats: ${t.settings}"); + // } super.initState(); } @override void dispose(){ - if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); + // if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) windowManager.setTitle(oldWindowTitle); super.dispose(); } @@ -40,8 +37,14 @@ class SprintAndBlitzState extends State { final t = Translations.of(context); bool bigScreen = MediaQuery.of(context).size.width >= 368; return Scaffold( - appBar: AppBar( - title: Text(t.sprintAndBlitsViewTitle), + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + floatingActionButton: Padding( + padding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 0.0), + child: FloatingActionButton( + onPressed: () => Navigator.pop(context), + tooltip: t.goBackButton, + child: const Icon(Icons.arrow_back), + ), ), backgroundColor: Colors.black, body: SafeArea( @@ -67,11 +70,11 @@ class SprintAndBlitzState extends State { Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(t.sprint, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + child: Text(t.gamemodes["40l"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(t.blitz, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), + child: Text(t.gamemodes["blitz"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), ), ] ), @@ -81,7 +84,7 @@ class SprintAndBlitzState extends State { Container(decoration: BoxDecoration(boxShadow: [BoxShadow(color: Colors.black.withAlpha(132), blurRadius: 32.0, blurStyle: BlurStyle.inner)]), child: Image.asset("res/tetrio_tl_alpha_ranks/${sprintEntry.key}.png", height: 48)), Padding( padding: const EdgeInsets.only(right: 8.0), - child: Text(get40lTime(sprintEntry.value.inMicroseconds), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + child: Text(getALittleBitMoreNormalTime(sprintEntry.value), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), ), Padding( padding: const EdgeInsets.only(right: 8.0), @@ -91,7 +94,7 @@ class SprintAndBlitzState extends State { ) ], ), - Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 5, 26)))) + Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 8, 25)))) ], ), ), diff --git a/lib/views/state_view.dart b/lib/views/state_view.dart index b97f141..00fa591 100644 --- a/lib/views/state_view.dart +++ b/lib/views/state_view.dart @@ -2,11 +2,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/widgets/graphs.dart'; +import 'package:tetra_stats/widgets/nerd_stats_thingy.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/tl_thingy.dart'; -import 'package:tetra_stats/widgets/user_thingy.dart'; import 'package:window_manager/window_manager.dart'; final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); @@ -29,7 +30,7 @@ class StateState extends State { _scrollController = ScrollController(); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}"); + windowManager.setTitle(t.stateView.title(date: timestamp(widget.state.timestamp))); } super.initState(); } @@ -41,20 +42,24 @@ class StateState extends State { super.dispose(); } - void _justUpdate() { - setState(() {}); - } - @override Widget build(BuildContext context) { - final t = Translations.of(context); + //final t = Translations.of(context); return Scaffold( appBar: AppBar( - title: Text("State from ${timestamp(widget.state.timestamp)}"), + title: Text(t.stateView.title(date: timestamp(widget.state.timestamp)), style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 28)), ), backgroundColor: Colors.black, body: SafeArea( - child: TLThingy(tl: widget.state, userID: widget.state.id, states: []) + child: SingleChildScrollView( + child: Column( + children: [ + TetraLeagueThingy(league: widget.state), + if (widget.state.nerdStats != null) NerdStatsThingy(nerdStats: widget.state.nerdStats!), + if (widget.state.playstyle != null) Graphs(widget.state.apm!, widget.state.pps!, widget.state.vs!, widget.state.nerdStats!, widget.state.playstyle!) + ], + ), + ) ) ); } diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart deleted file mode 100644 index bf0fc5f..0000000 --- a/lib/views/states_view.dart +++ /dev/null @@ -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 createState() => StatesState(); -} - -late String oldWindowTitle; - -class StatesState extends State { - @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>(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)); - } - )));} - } diff --git a/lib/views/tl_leaderboard_view.dart b/lib/views/tl_leaderboard_view.dart deleted file mode 100644 index d7699ad..0000000 --- a/lib/views/tl_leaderboard_view.dart +++ /dev/null @@ -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 _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; -Stats _sortBy = Stats.tr; -bool reversed = false; -List _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 createState() => TLLeaderboardState(); -} - -class TLLeaderboardState extends State { - @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"); - } - })), - ); - } -} diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index fc4b50d..5ff2c5e 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -1,16 +1,16 @@ // ignore_for_file: use_build_context_synchronously, type_literal_in_constant_pattern import 'dart:io'; +import 'package:tetra_stats/data_objects/beta_record.dart'; import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/views/compare_view.dart' show CompareThingy; +import 'package:tetra_stats/widgets/compare_thingy.dart'; import 'package:tetra_stats/widgets/list_tile_trailing_stats.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; import 'package:tetra_stats/widgets/vs_graphs.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/open_in_browser.dart'; import 'package:window_manager/window_manager.dart'; @@ -43,60 +43,23 @@ class TlMatchResultState extends State { late String reason; Duration totalTime = const Duration(); List roundLengths = []; - List timeWeightedStats = []; late bool initPlayerWon; @override void initState(){ - rounds = [DropdownMenuItem(value: -1, child: Text(t.match))]; - rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]); - if (rounds.indexWhere((element) => element.value == -2) == -1) rounds.insert(1, DropdownMenuItem(value: -2, child: Text(t.timeWeightedmatch))); + rounds = [DropdownMenuItem(value: -1, child: Text(t.tlMatchView.match))]; + rounds.addAll([for (int i = 0; i < widget.record.results.rounds.length; i++) DropdownMenuItem(value: i, child: Text(t.tlMatchView.roundNumber(n: i+1)))]); greenSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id == widget.initPlayerId); redSidePlayer = widget.record.results.leaderboard.indexWhere((element) => element.id != widget.initPlayerId); - List apmMultipliedByWeights = [0, 0]; - List ppsMultipliedByWeights= [0, 0]; - List vsMultipliedByWeights = [0, 0]; for (var round in widget.record.results.rounds){ var longerLifetime = round[0].lifetime.compareTo(round[1].lifetime) == 1 ? round[0].lifetime : round[1].lifetime; roundLengths.add(longerLifetime); totalTime += longerLifetime; - - BetaLeagueRound greenSide = round.firstWhere((element) => element.id == widget.initPlayerId); - BetaLeagueRound redSide = round.firstWhere((element) => element.id != widget.initPlayerId); - - apmMultipliedByWeights[0] += greenSide.stats.apm*longerLifetime.inMilliseconds; - apmMultipliedByWeights[1] += redSide.stats.apm*longerLifetime.inMilliseconds; - ppsMultipliedByWeights[0] += greenSide.stats.pps*longerLifetime.inMilliseconds; - ppsMultipliedByWeights[1] += redSide.stats.pps*longerLifetime.inMilliseconds; - vsMultipliedByWeights[0] += greenSide.stats.vs*longerLifetime.inMilliseconds; - vsMultipliedByWeights[1] += redSide.stats.vs*longerLifetime.inMilliseconds; } - timeWeightedStats = [ - BetaLeagueStats( - apm: apmMultipliedByWeights[0]/totalTime.inMilliseconds, - pps: ppsMultipliedByWeights[0]/totalTime.inMilliseconds, - vs: vsMultipliedByWeights[0]/totalTime.inMilliseconds, - garbageSent: widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent, - garbageReceived: widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived, - kills: widget.record.results.leaderboard[greenSidePlayer].stats.kills, - altitude: widget.record.results.leaderboard[greenSidePlayer].stats.altitude, - rank: widget.record.results.leaderboard[greenSidePlayer].stats.rank - ), - BetaLeagueStats( - apm: apmMultipliedByWeights[1]/totalTime.inMilliseconds, - pps: ppsMultipliedByWeights[1]/totalTime.inMilliseconds, - vs: vsMultipliedByWeights[1]/totalTime.inMilliseconds, - garbageSent: widget.record.results.leaderboard[redSidePlayer].stats.garbageSent, - garbageReceived: widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived, - kills: widget.record.results.leaderboard[redSidePlayer].stats.kills, - altitude: widget.record.results.leaderboard[redSidePlayer].stats.altitude, - rank: widget.record.results.leaderboard[redSidePlayer].stats.rank - ), - ]; initPlayerWon = widget.record.results.leaderboard[greenSidePlayer].wins > widget.record.results.leaderboard[redSidePlayer].wins; if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); - windowManager.setTitle("Tetra Stats: ${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}"); + windowManager.setTitle("Tetra Stats: ${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.tlMatchView.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${timestamp(widget.record.ts)}"); } super.initState(); } @@ -112,11 +75,11 @@ class TlMatchResultState extends State { bool bigScreen = width >= 768; if (roundSelector.isNegative){ time = totalTime; - readableTime = !time.isNegative ? "${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "${t.matchLength}: ---"; + readableTime = !time.isNegative ? "${t.tlMatchView.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "${t.tlMatchView.matchLength}: ---"; }else{ time = roundLengths[roundSelector]; int alive = widget.record.results.rounds[roundSelector].indexWhere((element) => element.alive); - readableTime = "${t.roundLength}: ${!time.isNegative ? "${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "---"}\n${t.winner}: ${alive == -1 ? "idk" : widget.record.results.rounds[roundSelector][alive].username}"; + readableTime = "${t.tlMatchView.roundLength}: ${!time.isNegative ? "${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}" : "---"}\n${t.tlMatchView.winner}: ${alive == -1 ? "idk" : widget.record.results.rounds[roundSelector][alive].username}"; } return SizedBox( width: width, @@ -189,7 +152,7 @@ class TlMatchResultState extends State { crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ - Text("${t.statsFor}: ", + Text("${t.tlMatchView.statsFor}: ", style: const TextStyle(color: Colors.white, fontSize: 25)), DropdownButton(items: rounds, value: roundSelector, onChanged: ((value) { roundSelector = value; @@ -199,9 +162,6 @@ class TlMatchResultState extends State { ), ), ), - if (widget.record.id == widget.record.replayID && showMobileSelector) SliverToBoxAdapter( - child: Center(child: Text(t.p1nkl0bst3rAlert, textAlign: TextAlign.center)), - ), if (showMobileSelector) SliverToBoxAdapter(child: Center(child: Text(readableTime, textAlign: TextAlign.center))), const SliverToBoxAdapter( child: Divider(), @@ -213,38 +173,32 @@ class TlMatchResultState extends State { Column( children: [ CompareThingy( - label: "APM", - greenSide: roundSelector == -2 ? timeWeightedStats[0].apm : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, - redSide: roundSelector == -2 ? timeWeightedStats[1].apm : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, + label: t.stats.apm.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, + redSide: roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( - label: "PPS", - greenSide: roundSelector == -2 ? timeWeightedStats[0].pps : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, - redSide: roundSelector == -2 ? timeWeightedStats[1].pps : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps, + label: t.stats.pps.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, + redSide: roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( - label: "VS", - greenSide: roundSelector == -2 ? timeWeightedStats[0].vs : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, - redSide: roundSelector == -2 ? timeWeightedStats[1].vs : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs, + label: t.stats.vs.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, + redSide: roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs, fractionDigits: 2, higherIsBetter: true, ), if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageSent, redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageSent : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageSent, - label: "Sent", higherIsBetter: true), + label: t.stats.sent, higherIsBetter: true), if (widget.record.gamemode == "league") CompareThingy(greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.garbageReceived, redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.garbageReceived : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.garbageReceived, - label: "Received", higherIsBetter: true), const Divider(), + label: t.stats.received, higherIsBetter: true), const Divider(), Column( children: [ Padding( @@ -255,142 +209,114 @@ class TlMatchResultState extends State { fontSize: bigScreen ? 42 : 28)), ), CompareThingy( - label: "APP", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.app : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.app, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.app : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.app, + label: t.stats.app.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.app, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.app : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.app, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "VS/APM", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.vsapm : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.vsapm, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.vsapm : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.vsapm, + label: t.stats.vsapm.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.vsapm, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.vsapm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.vsapm, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "DS/S", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dss : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dss, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dss : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dss, + label: t.stats.dss.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dss, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dss : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dss, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "DS/P", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.dsp : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dsp, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.dsp : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dsp, + label: t.stats.dsp.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.dsp, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.dsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.dsp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "APP + DS/P", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.appdsp : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.appdsp, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.appdsp : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.appdsp, + label: t.stats.appdsp.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.appdsp, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.appdsp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.appdsp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: t.statCellNum.cheese.replaceAll(RegExp(r'\n'), " "), - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.cheese : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.cheese, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.cheese : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.cheese, + label: t.stats.cheese.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.cheese, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.cheese : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.cheese, fractionDigits: 2, higherIsBetter: false, ), CompareThingy( - label: "Gb Eff.", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.gbe : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.gbe, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.gbe : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.gbe, + label: t.stats.gbe.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.gbe, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.gbe : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.gbe, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "wAPP", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.nyaapp : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.nyaapp, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.nyaapp : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.nyaapp, + label: t.stats.nyaapp.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.nyaapp, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.nyaapp : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.nyaapp, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "Area", - greenSide: roundSelector == -2 ? timeWeightedStats[0].nerdStats.area : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.area, - redSide: roundSelector == -2 ? timeWeightedStats[1].nerdStats.area : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.area, + label: t.stats.area.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats.area, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats.area : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats.area, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( - label: t.statCellNum.estOfTRShort, - greenSide: roundSelector == -2 ? timeWeightedStats[0].estTr.esttr : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr, - redSide: roundSelector == -2 ? timeWeightedStats[1].estTr.esttr : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.estTr.esttr, + label: t.stats.etr.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.estTr.esttr, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.estTr.esttr : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.estTr.esttr, fractionDigits: 2, higherIsBetter: true, ), CompareThingy( - label: "Opener", - greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.opener : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.opener, - redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.opener : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.opener, + label: t.stats.opener.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.opener, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.opener, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "Plonk", - greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.plonk : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk, - redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.plonk : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.opener : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk, + label: t.stats.plonk.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.plonk : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.plonk, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.plonk : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.plonk, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "Stride", - greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.stride : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.stride, - redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.stride : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.stride, + label: t.stats.stride.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.stride, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.stride : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.stride, fractionDigits: 3, higherIsBetter: true, ), CompareThingy( - label: "Inf. DS", - greenSide: roundSelector == -2 ? timeWeightedStats[0].playstyle.infds : - roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.infds, - redSide: roundSelector == -2 ? timeWeightedStats[1].playstyle.infds : - roundSelector == -1 ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.infds, + label: t.stats.infds.short, + greenSide: roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle.infds, + redSide: roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle.infds : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle.infds, fractionDigits: 3, higherIsBetter: true, ), VsGraphs( - roundSelector == -2 ? timeWeightedStats[0].apm : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, - roundSelector == -2 ? timeWeightedStats[0].pps : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, - roundSelector == -2 ? timeWeightedStats[0].vs : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, - roundSelector == -2 ? timeWeightedStats[0].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats, - roundSelector == -2 ? timeWeightedStats[0].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle, - roundSelector == -2 ? timeWeightedStats[1].apm : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, - roundSelector == -2 ? timeWeightedStats[1].pps : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps, - roundSelector == -2 ? timeWeightedStats[1].vs : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs, - roundSelector == -2 ? timeWeightedStats[1].nerdStats : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats, - roundSelector == -2 ? timeWeightedStats[1].playstyle : roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle, + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.apm, + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.pps, + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.vs, + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.nerdStats, + roundSelector.isNegative ? widget.record.results.leaderboard[greenSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id == widget.initPlayerId).stats.playstyle, + roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.apm : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.apm, + roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.pps : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.pps, + roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.vs : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.vs, + roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.nerdStats : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.nerdStats, + roundSelector.isNegative ? widget.record.results.leaderboard[redSidePlayer].stats.playstyle : widget.record.results.rounds[roundSelector].firstWhere((element) => element.id != widget.initPlayerId).stats.playstyle, ) ], ), @@ -447,7 +373,7 @@ class TlMatchResultState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(t.matchLength), + Text(t.tlMatchView.matchLength), RichText( text: !totalTime.isNegative ? TextSpan( text: "${totalTime.inMinutes}:${NumberFormat("00", LocaleSettings.currentLocale.languageCode).format(totalTime.inSeconds%60)}", @@ -463,7 +389,7 @@ class TlMatchResultState extends State { if (widget.record.id != widget.record.replayID) Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(t.numberOfRounds), + Text(t.tlMatchView.numberOfRounds), RichText( text: TextSpan( text: widget.record.results.rounds.length.toString(), @@ -476,25 +402,15 @@ class TlMatchResultState extends State { ), ) ],), - Column(children: [ - OverflowBar( - alignment: MainAxisAlignment.spaceEvenly, - children: [ - 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 { final t = Translations.of(context); return Scaffold( appBar: AppBar( - title: Text("${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${t.inTLmatch} ${widget.record.gamemode} ${timestamp(widget.record.ts)}"), + title: Text( + "${widget.record.results.leaderboard[greenSidePlayer].username.toUpperCase()} ${t.tlMatchView.vs} ${widget.record.results.leaderboard[redSidePlayer].username.toUpperCase()} ${widget.record.gamemode} ${timestamp(widget.record.ts)}", + style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 28), + ), actions: [ PopupMenuButton( enabled: widget.record.gamemode == "league", itemBuilder: (BuildContext context) => [ PopupMenuItem( value: 1, - child: Text(t.downloadReplay), + child: Text(t.tlMatchView.downloadReplay), ), PopupMenuItem( value: 2, - child: Text(t.openReplay), + child: Text(t.tlMatchView.openReplay), ), ], onSelected: (value) async { diff --git a/lib/views/tracked_players_view.dart b/lib/views/tracked_players_view.dart deleted file mode 100644 index 721bce4..0000000 --- a/lib/views/tracked_players_view.dart +++ /dev/null @@ -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 createState() => TrackedPlayersState(); -} - -class TrackedPlayersState extends State { - @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) => [ - 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 : {}; - List 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]), - ), - ); - }, - ); - })); - } - })), - ); - } -} diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart new file mode 100644 index 0000000..9f39bfa --- /dev/null +++ b/lib/views/user_view.dart @@ -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 createState() => UserState(); +} + +late String oldWindowTitle; + +class UserState extends State { + 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); + } + ) + ) + ); + } +} diff --git a/lib/views/zenith_record_view.dart b/lib/views/zenith_record_view.dart deleted file mode 100644 index b9f5c29..0000000 --- a/lib/views/zenith_record_view.dart +++ /dev/null @@ -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), - ), - ) - ), - ); - } - -} \ No newline at end of file diff --git a/lib/widgets/alpha_league_entry_thingy.dart b/lib/widgets/alpha_league_entry_thingy.dart new file mode 100644 index 0000000..2a3fd9a --- /dev/null +++ b/lib/widgets/alpha_league_entry_thingy.dart @@ -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))), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/badges_thingy.dart b/lib/widgets/badges_thingy.dart new file mode 100644 index 0000000..34b2eb5 --- /dev/null +++ b/lib/widgets/badges_thingy.dart @@ -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 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( + 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: [ + 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); + } + ) + ) + ], + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/beta_league_entry_thingy.dart b/lib/widgets/beta_league_entry_thingy.dart new file mode 100644 index 0000000..8b989eb --- /dev/null +++ b/lib/widgets/beta_league_entry_thingy.dart @@ -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))), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/compare_thingy.dart b/lib/widgets/compare_thingy.dart new file mode 100644 index 0000000..a5324e1 --- /dev/null +++ b/lib/widgets/compare_thingy.dart @@ -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( + 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( + 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, + ), + )), + ], + ), + ); + } +} diff --git a/lib/widgets/distinguishment_thingy.dart b/lib/widgets/distinguishment_thingy.dart new file mode 100644 index 0000000..26dba08 --- /dev/null +++ b/lib/widgets/distinguishment_thingy.dart @@ -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 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 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), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/error_thingy.dart b/lib/widgets/error_thingy.dart new file mode 100644 index 0000000..dc317a3 --- /dev/null +++ b/lib/widgets/error_thingy.dart @@ -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(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() + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/fake_distinguishment_thingy.dart b/lib/widgets/fake_distinguishment_thingy.dart new file mode 100644 index 0000000..bddc3b6 --- /dev/null +++ b/lib/widgets/fake_distinguishment_thingy.dart @@ -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), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/finesse_thingy.dart b/lib/widgets/finesse_thingy.dart index 937d767..1ccece4 100644 --- a/lib/widgets/finesse_thingy.dart +++ b/lib/widgets/finesse_thingy.dart @@ -1,7 +1,8 @@ // ignore_for_file: curly_braces_in_flow_control_structures import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/finesse.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; @@ -28,7 +29,7 @@ class FinesseThingy extends StatelessWidget{ fontSize: 65, height: 1.2, )), - const Positioned(left: 25, top: 20, child: Text("inesse", style: TextStyle(fontFamily: "Eurostile Round Extended"))), + Positioned(left: 25, top: 20, child: Text(t.stats.finesse.widgetTitle, style: TextStyle(fontFamily: "Eurostile Round Extended"))), Positioned( right: 0, top: 20, child: Text("${finesse != null ? finesse!.faults : "---"}F", style: TextStyle( diff --git a/lib/widgets/future_error.dart b/lib/widgets/future_error.dart new file mode 100644 index 0000000..ccab8c8 --- /dev/null +++ b/lib/widgets/future_error.dart @@ -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(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() + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/gauget_num.dart b/lib/widgets/gauget_num.dart deleted file mode 100644 index 0f3bd6f..0000000 --- a/lib/widgets/gauget_num.dart +++ /dev/null @@ -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 ranges; - final double minimum; - final double maximum; - final String playerStatLabel; - final String? okText; - final String? alertTitle; - final List? 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: [ - 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)], - )],), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/gauget_thingy.dart b/lib/widgets/gauget_thingy.dart new file mode 100644 index 0000000..fa5afe7 --- /dev/null +++ b/lib/widgets/gauget_thingy.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/leaderboard_position.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/utils/colors_functions.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; + +class GaugetThingy extends StatelessWidget{ + final double? value; + final String? subString; + final double min; + final double max; + final double? oldValue; + final double? avgValue; + final bool moreIsBetter; + final double tickInterval; + final String label; + final double sideSize; + final bool percentileFormat; + final int fractionDigits; + final LeaderboardPosition? lbPos; + + const GaugetThingy({super.key, required this.value, this.subString, required this.min, required this.max, this.oldValue, this.avgValue, required this.tickInterval, required this.label, required this.sideSize, required this.fractionDigits, required this.moreIsBetter, this.percentileFormat = false, this.lbPos}); + + @override + Widget build(BuildContext context) { + NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits); + return ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: SizedBox( + height: sideSize, + width: sideSize, + child: SfRadialGauge( + backgroundColor: Colors.black, + axes: [ + RadialAxis( + radiusFactor: 1.01, + minimum: min, + maximum: max, + showTicks: true, + showLabels: false, + interval: tickInterval, + minorTicksPerInterval: 0, + ranges:[ + GaugeRange(startValue: 0, endValue: (value != null && !value!.isNaN) ? value! : 0, color: theme.colorScheme.primary) + ], + annotations: [ + GaugeAnnotation(widget: Container(child: + Text((value != null && !value!.isNaN) ? percentileFormat ? percentage.format(value) : f.format(value) : "---", textAlign: TextAlign.center, style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: (value != null && !value!.isNaN) ? getStatColor(value!, avgValue, moreIsBetter) : Colors.grey))), + angle: 90,positionFactor: 0.10 + ), + GaugeAnnotation(widget: Container(child: + Text(label, textAlign: TextAlign.center, style: TextStyle(height: .9, color: (value != null && !value!.isNaN) ? null : Colors.grey))), + angle: 270,positionFactor: 0.3, verticalAlignment: GaugeAlignment.far, + ), + if (oldValue != null && (value != null && !value!.isNaN)) GaugeAnnotation(widget: Container(child: + Text(comparef2.format(percentileFormat ? (value!-oldValue!) * 100 : value!-oldValue!), textAlign: TextAlign.center, style: TextStyle(color: getDifferenceColor(moreIsBetter ? value!-oldValue! : oldValue!-value!)))), + angle: 90,positionFactor: lbPos != null ? 0.7 : 0.45 + ), + if (subString != null) GaugeAnnotation(widget: Container(child: + Text(subString!, textAlign: TextAlign.center, style: TextStyle(color: (value != null && !value!.isNaN) ? null : Colors.grey))), + angle: 90,positionFactor: lbPos != null ? 0.7 : 0.45 + ), + if (lbPos != null) GaugeAnnotation(widget: Container(child: + Text(lbPos!.position >= 1000 ? "${t.top} ${f2.format(lbPos!.percentage*100)}%" : "№ ${lbPos!.position}", textAlign: TextAlign.center, style: TextStyle(color: (lbPos != null) ? getColorOfRank(lbPos!.position) : Colors.grey))), + angle: 90,positionFactor: 0.45 + ) + ], + ) + ] + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index 194496d..f200ccc 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -7,10 +7,11 @@ import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart'; import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; import 'package:fl_chart/src/utils/canvas_wrapper.dart'; -import 'package:fl_chart/src/utils/utils.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/main.dart' show prefs; 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'; @@ -164,17 +165,17 @@ class MyRadarChartPainter extends RadarChartPainter{ ); } - _ticksTextPaint - ..text = TextSpan( - text: percentage.format(tick), - style: Utils().getThemeAwareTextStyle(context, data.ticksTextStyle), - ) - ..textDirection = TextDirection.ltr - ..layout(maxWidth: size.width); - canvasWrapper.drawText( - _ticksTextPaint, - Offset(centerX + 5, centerY - tickRadius - _ticksTextPaint.height/2), - ); + // _ticksTextPaint + // ..text = TextSpan( + // text: percentage.format(tick), + // style: Utils().getThemeAwareTextStyle(context, data.ticksTextStyle), + // ) + // ..textDirection = TextDirection.ltr + // ..layout(maxWidth: size.width); + // canvasWrapper.drawText( + // _ticksTextPaint, + // Offset(centerX + 5, centerY - tickRadius - _ticksTextPaint.height/2), + // ); }, ); } @@ -286,205 +287,212 @@ class Graphs extends StatelessWidget{ double speed = pps / 3.75; double defense = nerdStats.dss * 1.15; double cheese = nerdStats.cheese / 110; - return Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - if (true) Padding( // vs graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.transparent, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: 'APM', angle: angle, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: 'PPS', angle: angle, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); - case 4: - return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); - case 5: - return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); - case 6: - return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); - case 7: - return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); - case 8: - return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); - case 9: - return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(100), - borderColor: Theme.of(context).colorScheme.primary, - dataEntries: [ - RadarEntry(value: apm * apmWeight), - RadarEntry(value: pps * ppsWeight), - RadarEntry(value: vs * vsWeight), - RadarEntry(value: nerdStats.app * appWeight), - RadarEntry(value: nerdStats.dss * dssWeight), - RadarEntry(value: nerdStats.dsp * dspWeight), - RadarEntry(value: nerdStats.appdsp * appdspWeight), - RadarEntry(value: nerdStats.vsapm * vsapmWeight), - RadarEntry(value: nerdStats.cheese * cheeseWeight), - RadarEntry(value: nerdStats.gbe * gbeWeight), - ], + return Card( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Center( + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 25, + crossAxisAlignment: WrapCrossAlignment.start, + clipBehavior: Clip.hardEdge, + children: [ + if (true) Padding( // vs graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 22), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.circle, + tickCount: 4, + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: t.stats.apm.short, angle: angle, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: t.stats.pps.short, angle: angle, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: t.stats.vs.short, angle: angle, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: t.stats.app.short, angle: angle + 180, positionPercentageOffset: 0.05); + case 4: + return RadarChartTitle(text: t.stats.dss.short, angle: angle + 180, positionPercentageOffset: 0.05); + case 5: + return RadarChartTitle(text: t.stats.dsp.short, angle: angle + 180, positionPercentageOffset: 0.05); + case 6: + return RadarChartTitle(text: t.stats.appdsp.short, angle: angle + 180, positionPercentageOffset: 0.05); + case 7: + return RadarChartTitle(text: t.stats.vsapm.short, angle: angle + 180, positionPercentageOffset: 0.05); + case 8: + return RadarChartTitle(text: t.stats.cheese.short, angle: angle, positionPercentageOffset: 0.05); + case 9: + return RadarChartTitle(text: t.stats.gbe.short, angle: angle, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), + borderColor: Theme.of(context).colorScheme.primary, + dataEntries: [ + RadarEntry(value: apm * apmWeight), + RadarEntry(value: pps * ppsWeight), + RadarEntry(value: vs * vsWeight), + RadarEntry(value: nerdStats.app * appWeight), + RadarEntry(value: nerdStats.dss * dssWeight), + RadarEntry(value: nerdStats.dsp * dspWeight), + RadarEntry(value: nerdStats.appdsp * appdspWeight), + RadarEntry(value: nerdStats.vsapm * vsapmWeight), + RadarEntry(value: nerdStats.cheese * cheeseWeight), + RadarEntry(value: nerdStats.gbe * gbeWeight), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 180), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 180), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ) - ], + ), ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional - ), - ), - ), - Padding( // psq graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), - titleTextStyle: const TextStyle(height: 1.1), - radarTouchData: RadarTouchData(), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: 'Opener\n${percentage.format(playstyle.opener)}', angle: 0, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: 'Stride\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: 'Inf DS\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: 'Plonk\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(100), - borderColor: Theme.of(context).colorScheme.primary, - dataEntries: [ - RadarEntry(value: playstyle.opener), - RadarEntry(value: playstyle.stride), - RadarEntry(value: playstyle.infds), - RadarEntry(value: playstyle.plonk), - ], + Padding( // psq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 22), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.circle, + tickCount: 4, + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: '${t.stats.opener.short}\n${percentage.format(playstyle.opener)}', angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: '${t.stats.stride.short}\n${percentage.format(playstyle.stride)}', angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: '${t.stats.infds.short}\n${percentage.format(playstyle.infds)}', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: '${t.stats.plonk.short}\n${percentage.format(playstyle.plonk)}', angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), + borderColor: Theme.of(context).colorScheme.primary, + dataEntries: [ + RadarEntry(value: playstyle.opener), + RadarEntry(value: playstyle.stride), + RadarEntry(value: playstyle.infds), + RadarEntry(value: playstyle.plonk), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 1), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], - ) - ], + ), ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional - ), - ), - ), - Padding( // sq graph - padding: const EdgeInsets.fromLTRB(18, 0, 18, 44), - child: SizedBox( - height: 310, - width: 310, - child: MyRadarChart( - RadarChartData( - radarShape: RadarShape.polygon, - tickCount: 4, - ticksTextStyle: const TextStyle(color: Colors.white24, fontSize: 10), - radarBorderData: const BorderSide(color: Colors.transparent, width: 1), - gridBorderData: const BorderSide(color: Colors.white24, width: 1), - tickBorderData: const BorderSide(color: Colors.transparent, width: 1), - titleTextStyle: const TextStyle(height: 1.1), - radarTouchData: RadarTouchData(), - getTitle: (index, angle) { - switch (index) { - case 0: - return RadarChartTitle(text: '${t.graphs.attack}\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05); - case 1: - return RadarChartTitle(text: '${t.graphs.speed}\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05); - case 2: - return RadarChartTitle(text: '${t.graphs.defense}\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05); - case 3: - return RadarChartTitle(text: '${t.graphs.cheese}\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05); - default: - return const RadarChartTitle(text: ''); - } - }, - dataSets: [ - RadarDataSet( - fillColor: Theme.of(context).colorScheme.primary.withAlpha(100), - borderColor: Theme.of(context).colorScheme.primary, - dataEntries: [ - RadarEntry(value: attack), - RadarEntry(value: speed), - RadarEntry(value: defense), - RadarEntry(value: cheese), - ], - ), - RadarDataSet( - fillColor: Colors.transparent, - borderColor: Colors.transparent, - dataEntries: [ - const RadarEntry(value: 0), - const RadarEntry(value: 1.2), - const RadarEntry(value: 0), - const RadarEntry(value: 0), - ], + Padding( // sq graph + padding: const EdgeInsets.fromLTRB(18, 0, 18, 22), + child: SizedBox( + height: 310, + width: 310, + child: MyRadarChart( + RadarChartData( + radarShape: RadarShape.circle, + tickCount: 4, + radarBackgroundColor: Colors.black.withAlpha(170), + radarBorderData: const BorderSide(color: Colors.white24, width: 1), + gridBorderData: const BorderSide(color: Colors.white24, width: 1), + tickBorderData: const BorderSide(color: Colors.white24, width: 1), + titleTextStyle: const TextStyle(height: 1.1), + radarTouchData: RadarTouchData(), + getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: '${t.stats.graphs.attack}\n${f2.format(apm)} APM', angle: 0, positionPercentageOffset: 0.05); + case 1: + return RadarChartTitle(text: '${t.stats.graphs.speed}\n${f2.format(pps)} PPS', angle: 0, positionPercentageOffset: 0.05); + case 2: + return RadarChartTitle(text: '${t.stats.graphs.defense}\n${f2.format(nerdStats.dss)} DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + case 3: + return RadarChartTitle(text: '${t.stats.graphs.cheese}\n${f3.format(nerdStats.cheese)}', angle: 0, positionPercentageOffset: 0.05); + default: + return const RadarChartTitle(text: ''); + } + }, + dataSets: [ + RadarDataSet( + fillColor: Theme.of(context).colorScheme.primary.withAlpha(170), + borderColor: Theme.of(context).colorScheme.primary, + dataEntries: [ + RadarEntry(value: attack), + RadarEntry(value: speed), + RadarEntry(value: defense), + RadarEntry(value: cheese), + ], + ), + RadarDataSet( + fillColor: Colors.transparent, + borderColor: Colors.transparent, + dataEntries: [ + const RadarEntry(value: 0), + const RadarEntry(value: 1.2), + const RadarEntry(value: 0), + const RadarEntry(value: 0), + ], + ) + ], + ) ) - ], + ) ) - ) - ) - ) - ], + ], + ), + ), + ), ); } diff --git a/lib/widgets/info_thingy.dart b/lib/widgets/info_thingy.dart new file mode 100644 index 0000000..dfd90c3 --- /dev/null +++ b/lib/widgets/info_thingy.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class InfoThingy extends StatelessWidget{ + final String info; + + const InfoThingy(this.info); + + @override + Widget build(BuildContext context) { + return Center(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 128.0, color: Colors.grey.shade800), + SizedBox(height: 5.0), + Text(info, textAlign: TextAlign.center), + ], + )); + } + +} \ No newline at end of file diff --git a/lib/widgets/lineclears_thingy.dart b/lib/widgets/lineclears_thingy.dart index 78745db..3373e03 100644 --- a/lib/widgets/lineclears_thingy.dart +++ b/lib/widgets/lineclears_thingy.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/clears.dart'; import 'package:tetra_stats/gen/strings.g.dart'; class LineclearsThingy extends StatelessWidget{ @@ -21,14 +21,14 @@ class LineclearsThingy extends StatelessWidget{ child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(t.numOfGameActions.lineClears(n: lines), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), - if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Pentas"), Text(clears.pentas.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Quads"), Text(clears.quads.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Triples"), Text(clears.triples.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Doubles"), Text(clears.doubles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Singles"), Text(clears.singles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("\n${t.numOfGameActions.pc}"), Text("\n${clears.allClears.toString()}")]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.numOfGameActions.hold), Text(holds.toString())]), + Text(t.stats.linesCleared(n: lines), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.stats.lineClears.penta), Text(clears.pentas.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.stats.lineClears.quad), Text(clears.quads.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.stats.lineClears.triple), Text(clears.triples.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.stats.lineClears.double), Text(clears.doubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.stats.lineClears.single), Text(clears.singles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("\n${t.stats.pcs}"), Text("\n${clears.allClears.toString()}")]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t.stats.holds), Text(holds.toString())]), ], ), ), @@ -37,18 +37,18 @@ class LineclearsThingy extends StatelessWidget{ child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(t.numOfGameActions.tspinsTotal(n: tSpins), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), - if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spin pentas"), Text(clears.tSpinPentas.toString())]), - if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spin quads"), Text(clears.tSpinQuads.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins triples"), Text(clears.tSpinTriples.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins doubles"), Text(clears.tSpinDoubles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins singles"), Text(clears.tSpinSingles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("T-spins zeros"), Text(clears.tSpinZeros.toString())]), - if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins quads"), Text(clears.tSpinMiniQuads.toString())]), - if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins triples"), Text(clears.tSpinMiniTriples.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins doubles"), Text(clears.tSpinMiniDoubles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins singles"), Text(clears.tSpinMiniSingles.toString())]), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [const Text("Mini T-spins zeros"), Text(clears.tSpinMiniZeros.toString())]), + Text(t.stats.tspinsTotal(n: tSpins), style: const TextStyle(color: Colors.white, fontFamily: "Eurostile Round Extended"), textAlign: TextAlign.center), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.tSpins} ${t.stats.lineClears.penta}"), Text(clears.tSpinPentas.toString())]), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.tSpins} ${t.stats.lineClears.quad}"), Text(clears.tSpinQuads.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.tSpins} ${t.stats.lineClears.triple}"), Text(clears.tSpinTriples.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.tSpins} ${t.stats.lineClears.double}"), Text(clears.tSpinDoubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.tSpins} ${t.stats.lineClears.single}"), Text(clears.tSpinSingles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.tSpins} ${t.stats.lineClears.zero}"), Text(clears.tSpinZeros.toString())]), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.quad}"), Text(clears.tSpinMiniQuads.toString())]), + if (showMoreClears) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.triple}"), Text(clears.tSpinMiniTriples.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.double}"), Text(clears.tSpinMiniDoubles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.single}"), Text(clears.tSpinMiniSingles.toString())]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text("${t.stats.mini} ${t.stats.tSpins} ${t.stats.lineClears.zero}"), Text(clears.tSpinMiniZeros.toString())]), ], ), ), diff --git a/lib/widgets/list_tile_trailing_stats.dart b/lib/widgets/list_tile_trailing_stats.dart index 9575506..cd8b36f 100644 --- a/lib/widgets/list_tile_trailing_stats.dart +++ b/lib/widgets/list_tile_trailing_stats.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; class TrailingStats extends StatelessWidget{ @@ -13,19 +14,19 @@ class TrailingStats extends StatelessWidget{ @override Widget build(BuildContext context) { - const TextStyle style = TextStyle(height: 1.1, fontWeight: FontWeight.w100, fontSize: 13); + const TextStyle style = TextStyle(height: 1.0, fontWeight: FontWeight.w100, fontSize: 14); return Table( defaultColumnWidth: const IntrinsicColumnWidth(), defaultVerticalAlignment: TableCellVerticalAlignment.baseline, textBaseline: TextBaseline.alphabetic, columnWidths: const { - 0: FixedColumnWidth(48), - 2: FixedColumnWidth(48), + 0: FixedColumnWidth(54), + 2: FixedColumnWidth(54), }, children: [ - TableRow(children: [Text(f2.format(yourAPM), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourAPM), textAlign: TextAlign.right, style: style), const Text(" APM", textAlign: TextAlign.right, style: style)]), - TableRow(children: [Text(f2.format(yourPPS), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourPPS), textAlign: TextAlign.right, style: style), const Text(" PPS", textAlign: TextAlign.right, style: style)]), - TableRow(children: [Text(f2.format(yourVS), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourVS), textAlign: TextAlign.right, style: style), const Text(" VS", textAlign: TextAlign.right, style: style)]), + TableRow(children: [Text(f2.format(yourAPM), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourAPM), textAlign: TextAlign.right, style: style), Text(" ${t.stats.apm.short}", textAlign: TextAlign.right, style: style)]), + TableRow(children: [Text(f2.format(yourPPS), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourPPS), textAlign: TextAlign.right, style: style), Text(" ${t.stats.pps.short}", textAlign: TextAlign.right, style: style)]), + TableRow(children: [Text(f2.format(yourVS), textAlign: TextAlign.right, style: style), const Text(" :", style: style), Text(f2.format(notyourVS), textAlign: TextAlign.right, style: style), Text(" ${t.stats.vs.short}", textAlign: TextAlign.right, style: style)]), ], ); } diff --git a/lib/widgets/nerd_stats_thingy.dart b/lib/widgets/nerd_stats_thingy.dart new file mode 100644 index 0000000..d69cb47 --- /dev/null +++ b/lib/widgets/nerd_stats_thingy.dart @@ -0,0 +1,166 @@ +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/nerd_stats.dart'; +import 'package:tetra_stats/data_objects/player_leaderboard_position.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'; +import 'package:tetra_stats/widgets/gauget_thingy.dart'; + +class NerdStatsThingy extends StatelessWidget{ + final NerdStats nerdStats; + final NerdStats? oldNerdStats; + final CutoffTetrio? averages; + final PlayerLeaderboardPosition? lbPos; + final double width; + + const NerdStatsThingy({super.key, required this.nerdStats, this.oldNerdStats, this.averages, this.lbPos, this.width = double.infinity}); + + Widget big(){ + return SizedBox( + height: 256.0, + width: 256.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: Container( + decoration: BoxDecoration(gradient: RadialGradient(colors: [Colors.black12.withAlpha(100), Colors.black], radius: 0.6)), + child: SfRadialGauge( + axes: [ + RadialAxis( + startAngle: 190, + endAngle: 350, + showLabels: false, + showTicks: true, + radiusFactor: 1, + centerY: 0.5, + minimum: 0, + maximum: 1, + ranges: [ + GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), + GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), + GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), + GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), + GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), + ], + pointers: [ + NeedlePointer( + value: nerdStats.app, + 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: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.white), + children: [ + TextSpan(text: "${t.stats.app.short}\n"), + TextSpan(text: f3.format(nerdStats.app), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.app, averages?.nerdStats?.app, true))), + if (lbPos != null) TextSpan(text: lbPos!.app!.position >= 1000 ? "\n${t.top} ${f2.format(lbPos!.app!.percentage*100)}%" : "\n№${lbPos!.app!.position}", style: TextStyle(color: getColorOfRank(lbPos!.app!.position))), + if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.app - oldNerdStats!.app)}", style: TextStyle(color: getDifferenceColor(nerdStats.app - oldNerdStats!.app))) + ] + ))), + angle: 270,positionFactor: 0.5 + )], + ), + RadialAxis( + startAngle: 20, + endAngle: 160, + isInversed: true, + showLabels: false, + showTicks: true, + radiusFactor: 1, + centerY: 0.5, + minimum: 1.8, + maximum: 2.4, + ranges: [ + GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), + GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), + GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), + ], + pointers: [ + NeedlePointer( + value: nerdStats.vsapm, + 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: Container(child: + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.white), + children: [ + TextSpan(text: "${t.stats.vsapm.short}\n"), + TextSpan(text: f3.format(nerdStats.vsapm), style: TextStyle(fontSize: 25, fontFamily: "Eurostile Round Extended", fontWeight: FontWeight.w100, color: getStatColor(nerdStats.vsapm, averages?.nerdStats?.vsapm, true))), + if (lbPos != null) TextSpan(text: lbPos!.vsapm!.position >= 1000 ? "\n${t.top} ${f2.format(lbPos!.vsapm!.percentage*100)}%" : "\n№${lbPos!.vsapm!.position}", style: TextStyle(color: getColorOfRank(lbPos!.vsapm!.position))), + if (oldNerdStats != null) TextSpan(text: "\n${comparef.format(nerdStats.vsapm - oldNerdStats!.vsapm)}", style: TextStyle(color: getDifferenceColor(nerdStats.vsapm - oldNerdStats!.vsapm))), + ] + ))), + angle: 90,positionFactor: 0.5 + ) + ], + ) + ] + ), + ), + ), + ); + } + + Widget manySmalls(){ + return Wrap( + alignment: WrapAlignment.center, + spacing: 10.0, + runSpacing: 10.0, + runAlignment: WrapAlignment.start, + children: [ + GaugetThingy(value: nerdStats.dss, oldValue: oldNerdStats?.dss, min: 0, max: 1.0, tickInterval: .2, label: t.stats.dss.short, sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dss, lbPos: lbPos?.dss), + GaugetThingy(value: nerdStats.dsp, oldValue: oldNerdStats?.dsp, min: 0, max: 1.0, tickInterval: .2, label: t.stats.dsp.short, sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.dsp, lbPos: lbPos?.dsp), + GaugetThingy(value: nerdStats.appdsp, oldValue: oldNerdStats?.appdsp, min: 0, max: 1.2, tickInterval: .2, label: t.stats.appdsp.short, sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.appdsp, lbPos: lbPos?.appdsp), + GaugetThingy(value: nerdStats.cheese, oldValue: oldNerdStats?.cheese, min: -80, max: 80, tickInterval: 40, label: t.stats.cheese.short, sideSize: 128.0, fractionDigits: 2, moreIsBetter: false, lbPos: lbPos?.cheese), + GaugetThingy(value: nerdStats.gbe, oldValue: oldNerdStats?.gbe, min: 0, max: 1.0, tickInterval: .2, label: t.stats.gbe.short, sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.gbe, lbPos: lbPos?.gbe), + GaugetThingy(value: nerdStats.nyaapp, oldValue: oldNerdStats?.nyaapp, min: 0, max: 1.2, tickInterval: .2, label: t.stats.nyaapp.short, sideSize: 128.0, fractionDigits: 3, moreIsBetter: true, avgValue: averages?.nerdStats?.nyaapp, lbPos: lbPos?.nyaapp), + GaugetThingy(value: nerdStats.area, oldValue: oldNerdStats?.area, min: 0, max: 1000, tickInterval: 100, label: t.stats.area.short, sideSize: 128.0, fractionDigits: 1, moreIsBetter: true, avgValue: averages?.nerdStats?.area, lbPos: lbPos?.area), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0), + child: width > 600 ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + big(), + Expanded(child: manySmalls()) + ] + ) : Center( + child: Column( + children: [ + big(), + manySmalls() + ], + ), + ), + ), + ], + ) + ); + } +} \ No newline at end of file diff --git a/lib/widgets/news_thingy.dart b/lib/widgets/news_thingy.dart new file mode 100644 index 0000000..bc7db58 --- /dev/null +++ b/lib/widgets/news_thingy.dart @@ -0,0 +1,183 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tetra_stats/data_objects/news.dart'; +import 'package:tetra_stats/data_objects/news_entry.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/utils/relative_timestamps.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; + +class NewsThingy extends StatelessWidget{ + final News news; + + const NewsThingy(this.news, {super.key}); + + ListTile getNewsTile(NewsEntry news){ + // Individuly handle each entry type + switch (news.type) { + case "leaderboard": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + children: [ + t.newsEntries.leaderboard( + rank: TextSpan(text: news.data["rank"], style: const TextStyle(fontWeight: FontWeight.bold)), + gametype: TextSpan(text: t.gamemodes[news.data["gametype"]], style: const TextStyle(fontWeight: FontWeight.bold)) + ) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + ); + case "personalbest": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + children: [ + t.newsEntries.personalbest( + gametype: TextSpan(text: t.gamemodes[news.data["gametype"]], style: const TextStyle(fontWeight: FontWeight.bold)), + pb: TextSpan(text: switch (news.data["gametype"]){ + "blitz" => NumberFormat.decimalPattern().format(news.data["result"]), + "40l" => get40lTime((news.data["result"]*1000).floor()), + "5mblast" => get40lTime((news.data["result"]*1000).floor()), + "zenith" => "${f2.format(news.data["result"])} m.", + "zenithex" => "${f2.format(news.data["result"])} m.", + _ => "unknown" + }, + style: const TextStyle(fontWeight: FontWeight.bold) + ) + ) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/icons/improvement-local.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + case "badge": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + children: [ + t.newsEntries.badge(badge: TextSpan(text: news.data["label"], style: const TextStyle(fontWeight: FontWeight.bold))) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.network( + kIsWeb ? "https://${webVersionDomain}/oskware_bridge.php?endpoint=TetrioBadge&badge=${news.data["type"]}" : "https://tetr.io/res/badges/${news.data["type"]}.png", + height: 48, + width: 48, + errorBuilder:(context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 32, width: 32); + } + ) + ); + case "rankup": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + children: [ + t.newsEntries.rankup(rank: TextSpan(text: t.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold))) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/tetrio_tl_alpha_ranks/${news.data["rank"]}.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + case "supporter": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + children: [ + t.newsEntries.supporter(s: (p0) => TextSpan(text: p0, style: const TextStyle(fontWeight: FontWeight.bold))) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/icons/supporter-tag.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + case "supporter_gift": + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + children: [ + t.newsEntries.supporter_gift(s: (p0) => TextSpan(text: p0, style: const TextStyle(fontWeight: FontWeight.bold))) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + leading: Image.asset( + "res/icons/supporter-tag.png", + height: 48, + width: 48, + errorBuilder: (context, error, stackTrace) { + return Image.asset("res/icons/kagari.png", height: 64, width: 64); + }, + ), + ); + default: // if type is unknown + return ListTile( + title: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), + children: [ + t.newsEntries.unknown(type: TextSpan(text: news.type, style: const TextStyle(fontWeight: FontWeight.bold))) + ] + ) + ), + subtitle: Text(timestamp(news.timestamp)), + ); + } + } + + @override + Widget build(BuildContext context) { + return Card( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + const Spacer(), + Text(t.news, style: const TextStyle(fontFamily: "Eurostile Round Extended")), + const Spacer() + ] + ), + if (news.news.isEmpty) const Center(child: Text("Empty list")) + else for (NewsEntry entry in news.news) getNewsTile(entry) + ], + ), + ), + ); + } + +} \ No newline at end of file diff --git a/lib/widgets/recent_sp_games.dart b/lib/widgets/recent_sp_games.dart index e3e1b7a..c8446d6 100644 --- a/lib/widgets/recent_sp_games.dart +++ b/lib/widgets/recent_sp_games.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/singleplayer_stream.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; import 'package:tetra_stats/utils/text_shadow.dart'; @@ -36,11 +37,11 @@ class RecentSingleplayerGames extends StatelessWidget{ title: Text( switch (record.gamemode){ "40l" => get40lTime(record.stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(record.stats.score)), + "blitz" => t.stats.blitzScore(p: NumberFormat.decimalPattern().format(record.stats.score)), "5mblast" => get40lTime(record.stats.finalTime.inMicroseconds), String() => "huh", }, - style: const TextStyle(fontSize: 18)), + style: Theme.of(context).textTheme.displayLarge), subtitle: Text(timestamp(record.timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), trailing: SpTrailingStats(record, record.gamemode) ) diff --git a/lib/widgets/search_box.dart b/lib/widgets/search_box.dart deleted file mode 100644 index cf4b81c..0000000 --- a/lib/widgets/search_box.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; - -const int length = 25; -final TextEditingController controller = TextEditingController(); - -class SearchBox extends StatefulWidget { - final Function onSubmit; - final bool bigScreen; - const SearchBox({required this.onSubmit, required this.bigScreen, super.key}); - - @override - State createState() => _SearchBoxState(); -} - -class _SearchBoxState extends State{ - late FocusNode textbotFocus; - - @override - void initState() { - textbotFocus = FocusNode(); - controller.addListener(() { - setState(() {}); - }); - super.initState(); - } - - @override - void dispose(){ - controller.clear(); - textbotFocus.dispose(); - super.dispose(); - } - - Color getColorOfCounter(){ - // if limit was hit - if ((length - controller.text.length) <= 0) return Colors.redAccent; - // if input more than 16 symbols (username length limit) - if ((length - controller.text.length) < 9) return Colors.yellowAccent; - // if we good (we not) - return Colors.grey; - } - - double getFontSizeOfCounter(){ - return (length - controller.text.length) <= 0 ? 24 : 16; - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return Row( - mainAxisSize: MainAxisSize.min, - //alignment: Alignment.centerRight, - children: [ - Expanded( - child: TextField( - controller: controller, - maxLength: length, - focusNode: textbotFocus, - autofocus: true, - autocorrect: false, - enableSuggestions: false, - decoration: InputDecoration( - counter: const Offstage(), - hintText: widget.bigScreen ? t.searchHint : null, - ), - style: const TextStyle(shadows: textShadow, fontSize: 18), - onSubmitted: (String value) { - widget.onSubmit(value); - textbotFocus.unfocus(); - }, - ), - ), - AnimatedDefaultTextStyle( - style: TextStyle( - fontFamily: "Eurostile Round", - fontSize: getFontSizeOfCounter(), - color: getColorOfCounter(), - shadows: textShadow - ), - duration: Durations.short4, - curve: Curves.easeOutCirc, - child: Text("${length - controller.text.length}") - ) - ] - ); - } -} \ No newline at end of file diff --git a/lib/widgets/singleplayer_record.dart b/lib/widgets/singleplayer_record.dart index f717ca2..95532d4 100644 --- a/lib/widgets/singleplayer_record.dart +++ b/lib/widgets/singleplayer_record.dart @@ -1,37 +1,26 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_single.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.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'; -import 'package:tetra_stats/utils/open_in_browser.dart'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/views/singleplayer_record_view.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; -import 'package:tetra_stats/widgets/sp_trailing_stats.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:tetra_stats/views/destination_home.dart'; class SingleplayerRecord extends StatelessWidget { final RecordSingle? record; - final SingleplayerStream? stream; final String? rank; final bool hideTitle; /// Widget that displays data from [record] - const SingleplayerRecord({super.key, required this.record, this.stream, this.rank, this.hideTitle = false}); + const SingleplayerRecord({super.key, required this.record, this.rank, this.hideTitle = false}); @override Widget build(BuildContext context) { if (record == null) return Center(child: Text(t.noRecord, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28))); late MapEntry closestAverageBlitz; late bool blitzBetterThanClosestAverage; - bool? blitzBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+") ? record!.stats.score > blitzAverages[rank]! : null; + bool? blitzBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.score > blitzAverages[rank]! : null; late MapEntry closestAverageSprint; late bool sprintBetterThanClosestAverage; - bool? sprintBetterThanRankAverage = (rank != null && rank != "z" && rank != "x+") ? record!.stats.finalTime < sprintAverages[rank]! : null; + bool? sprintBetterThanRankAverage = (rank != null && rank != "z") ? record!.stats.finalTime < sprintAverages[rank]! : null; if (record!.gamemode == "40l") { closestAverageSprint = sprintAverages.entries.singleWhere((element) => element.value == sprintAverages.values.reduce((a, b) => (a-record!.stats.finalTime).abs() < (b -record!.stats.finalTime).abs() ? a : b)); sprintBetterThanClosestAverage = record!.stats.finalTime < closestAverageSprint.value; @@ -40,114 +29,6 @@ class SingleplayerRecord extends StatelessWidget { blitzBetterThanClosestAverage = record!.stats.score > closestAverageBlitz.value; } - return LayoutBuilder( - builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (record!.gamemode == "40l") Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageSprint.key}.png", height: 96) - ), - if (record!.gamemode == "blitz") Padding(padding: const EdgeInsets.only(right: 8.0), - child: Image.asset("res/tetrio_tl_alpha_ranks/${closestAverageBlitz.key}.png", height: 96) - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (record!.gamemode == "40l" && !hideTitle) Text(t.sprint, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - if (record!.gamemode == "blitz" && !hideTitle) Text(t.blitz, style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: record!.gamemode == "40l" ? get40lTime(record!.stats.finalTime.inMicroseconds) : NumberFormat.decimalPattern().format(record!.stats.score), - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (record!.gamemode == "40l" && (rank != null && rank != "z" && rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.stats.finalTime, sprintAverages[rank]!), verdict: sprintBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if (record!.gamemode == "40l" && (rank == null || rank == "z" || rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableTimeDifference(record!.stats.finalTime, closestAverageSprint.value), verdict: sprintBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageSprint.key.toUpperCase())}\n", style: TextStyle( - color: sprintBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )) - else if (record!.gamemode == "blitz" && (rank != null && rank != "z" && rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.score, blitzAverages[rank]!), verdict: blitzBetterThanRankAverage??false ? t.verdictBetter : t.verdictWorse, rank: rank!.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanRankAverage??false ? Colors.greenAccent : Colors.redAccent - )) - else if (record!.gamemode == "blitz" && (rank == null || rank == "z" || rank != "x+")) TextSpan(text: "${t.verdictGeneral(n: readableIntDifference(record!.stats.score, closestAverageBlitz.value), verdict: blitzBetterThanClosestAverage ? t.verdictBetter : t.verdictWorse, rank: closestAverageBlitz.key.toUpperCase())}\n", style: TextStyle( - color: blitzBetterThanClosestAverage ? Colors.greenAccent : Colors.redAccent - )), - if (record!.rank != -1) TextSpan(text: "№${record!.rank}", style: TextStyle(color: getColorOfRank(record!.rank))), - if (record!.rank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(record!.timestamp)), - ] - ), - ) - ],), - ], - ), - if (record!.gamemode == "40l") Wrap( - alignment: WrapAlignment.spaceBetween, - spacing: 20, - children: [ - StatCellNum(playerStat: record!.stats.piecesPlaced, playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.stats.kpp, playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - ], - ), - if (record!.gamemode == "blitz") Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - StatCellNum(playerStat: record!.stats.level, playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.stats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.stats.spp, playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true) - ], - ), - FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), - LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), - if (record!.gamemode == "40l") Text("${record!.stats.inputs} KP • ${f2.format(record!.stats.kps)} KPS"), - if (record!.gamemode == "blitz") Text("${record!.stats.piecesPlaced} P • ${record!.stats.inputs} KP • ${f2.format(record!.stats.kpp)} KPP • ${f2.format(record!.stats.kps)} KPS"), - if (record != null) Wrap( - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 20, - children: [ - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://tetr.io/#r:${record!.replayId}"));}, child: Text(t.openSPreplay)), - TextButton(onPressed: (){launchInBrowser(Uri.parse("https://inoue.szy.lol/api/replay/${record!.replayId}"));}, child: Text(t.downloadSPreplay)), - ], - ), - if (stream != null && stream!.records.length > 1) for(int i = 1; i < stream!.records.length; i++) ListTile( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SingleplayerRecordView(record: stream!.records[i]))), - leading: Text("#${i+1}", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28, shadows: textShadow, height: 0.9) - ), - title: Text( - switch (stream!.records[i].gamemode){ - "40l" => get40lTime(stream!.records[i].stats.finalTime.inMicroseconds), - "blitz" => t.blitzScore(p: NumberFormat.decimalPattern().format(stream!.records[i].stats.score)), - "5mblast" => get40lTime(stream!.records[i].stats.finalTime.inMicroseconds), - String() => "huh", - }, - style: const TextStyle(fontSize: 18)), - subtitle: Text(timestamp(stream!.records[i].timestamp), style: const TextStyle(color: Colors.grey, height: 0.85)), - trailing: SpTrailingStats(stream!.records[i], stream!.records[i].gamemode) - ) - ] - ), - ), - ); - } - ); + return RecordCard(record, [], record!.gamemode == "40l" ? sprintBetterThanRankAverage : blitzBetterThanRankAverage, record!.gamemode == "40l" ? closestAverageSprint : closestAverageBlitz, record!.gamemode == "40l" ? sprintBetterThanClosestAverage : blitzBetterThanClosestAverage, rank); } } \ No newline at end of file diff --git a/lib/widgets/sp_trailing_stats.dart b/lib/widgets/sp_trailing_stats.dart index fc679c6..c29800c 100644 --- a/lib/widgets/sp_trailing_stats.dart +++ b/lib/widgets/sp_trailing_stats.dart @@ -1,5 +1,6 @@ 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/utils/numers_formats.dart'; import 'package:tetra_stats/utils/relative_timestamps.dart'; @@ -17,27 +18,27 @@ class SpTrailingStats extends StatelessWidget{ crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(switch(gamemode){ - "40l" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS", - "blitz" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS", - "5mblast" => "${record.stats.piecesPlaced} P, ${f2.format(record.stats.pps)} PPS", - "zenith" => "${f2.format(record.aggregateStats.apm)} APM, ${f2.format(record.aggregateStats.pps)} PPS", - "zenithex" => "${f2.format(record.aggregateStats.apm)} APM, ${f2.format(record.aggregateStats.pps)} PPS", + "40l" => "${record.stats.piecesPlaced} ${t.stats.pieces.short}, ${f2.format(record.stats.pps)} ${t.stats.pps.short}", + "blitz" => "${record.stats.piecesPlaced} ${t.stats.pieces.short}, ${f2.format(record.stats.pps)} ${t.stats.pps.short}", + "5mblast" => "${record.stats.piecesPlaced} ${t.stats.pieces.short}, ${f2.format(record.stats.pps)} ${t.stats.pps.short}", + "zenith" => "${f2.format(record.aggregateStats.apm)} ${t.stats.apm.short}, ${f2.format(record.aggregateStats.pps)} ${t.stats.pps.short}", + "zenithex" => "${f2.format(record.aggregateStats.apm)} ${t.stats.apm.short}, ${f2.format(record.aggregateStats.pps)} ${t.stats.pps.short}", String() => "huh" }, style: style, textAlign: TextAlign.right), Text(switch(gamemode){ - "40l" => "${intf.format(record.stats.finessePercentage*100)}% F, ${record.stats.finesse?.faults} FF", - "blitz" => "${intf.format(record.stats.finessePercentage*100)}% F, ${record.stats.finesse?.faults} FF", - "5mblast" => "${intf.format(record.stats.finessePercentage*100)}% F, ${record.stats.finesse?.faults} FF", - "zenith" => "${f2.format(record.stats.cps)} CSP (${f2.format(record.stats.zenith!.peakrank)} peak)", - "zenithex" => "${f2.format(record.stats.cps)} CSP (${f2.format(record.stats.zenith!.peakrank)} peak)", + "40l" => "${intf.format(record.stats.finessePercentage*100)}% ${t.stats.finesse.short}, ${record.stats.finesse?.faults} ${t.stats.finesseFaults.short}", + "blitz" => "${intf.format(record.stats.finessePercentage*100)}% ${t.stats.finesse.short}, ${record.stats.finesse?.faults} ${t.stats.finesseFaults.short}", + "5mblast" => "${intf.format(record.stats.finessePercentage*100)}% ${t.stats.finesse.short}, ${record.stats.finesse?.faults} ${t.stats.finesseFaults.short}", + "zenith" => "${f2.format(record.stats.cps)} ${t.stats.climbSpeed.short} (${f2.format(record.stats.zenith!.peakrank)} ${t.stats.peak})", + "zenithex" => "${f2.format(record.stats.cps)} ${t.stats.climbSpeed.short} (${f2.format(record.stats.zenith!.peakrank)} ${t.stats.peak})", String() => "huh" }, style: style, textAlign: TextAlign.right), Text(switch(gamemode){ - "40l" => "${f2.format(record.stats.kps)} KPS, ${f2.format(record.stats.kpp)} KPP", - "blitz" => "${intf.format(record.stats.spp)} SPP, lvl ${record.stats.level}", - "5mblast" => "${intf.format(record.stats.spp)} SPP, ${record.stats.lines} L", - "zenith" => "${record.stats.kills} KO's, ${getMoreNormalTime(record.stats.finalTime)}", - "zenithex" => "${record.stats.kills} KO's, ${getMoreNormalTime(record.stats.finalTime)}", + "40l" => "${f2.format(record.stats.kps)} ${t.stats.kps.short}, ${f2.format(record.stats.kpp)} ${t.stats.kpp.short}", + "blitz" => "${intf.format(record.stats.spp)} ${t.stats.spp.short}, ${t.stats.level.short} ${record.stats.level}", + "5mblast" => "${intf.format(record.stats.spp)} ${t.stats.spp.short}, ${record.stats.lines} ${t.stats.linesShort}", + "zenith" => "${record.stats.kills} ${t.stats.kos.short}, ${getMoreNormalTime(record.stats.finalTime)}", + "zenithex" => "${record.stats.kills} ${t.stats.kos.short}, ${getMoreNormalTime(record.stats.finalTime)}", String() => "huh" }, style: style, textAlign: TextAlign.right) ], diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart deleted file mode 100644 index f837bb4..0000000 --- a/lib/widgets/stat_sell_num.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/utils/colors_functions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; - -class StatCellNum extends StatelessWidget { - const StatCellNum( - {super.key, - required this.playerStat, - required this.playerStatLabel, - required this.isScreenBig, - this.smallDecimal = false, - this.alertWidgets, - this.fractionDigits, - this.oldPlayerStat, - required this.higherIsBetter, - this.okText, this.alertTitle, this.pos, this.averageStat}); - - final num playerStat; - final num? oldPlayerStat; - final bool higherIsBetter; - final String playerStatLabel; - final String? okText; - final bool isScreenBig; - final bool smallDecimal; - final String? alertTitle; - final List? alertWidgets; - final int? fractionDigits; - final LeaderboardPosition? pos; - final num? averageStat; - - Color getStatColor(){ - if (averageStat == null) return Colors.white; - num percentile = (higherIsBetter ? playerStat / averageStat! : averageStat! / playerStat).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; - } - - @override - Widget build(BuildContext context) { - NumberFormat f = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: fractionDigits ?? 0); - NumberFormat comparef = NumberFormat("+#,###.###;-#,###.###")..maximumFractionDigits = fractionDigits ?? 0; - String formated = f.format(playerStat); - List splited = formated.split(f.symbols.DECIMAL_SEP); - return Column( - children: [ - RichText( - text: TextSpan(text: splited[0], - children: [ - if ((fractionDigits??0) > 0 && splited.elementAtOrNull(1) != null) TextSpan(text: f.symbols.DECIMAL_SEP+splited[1], style: smallDecimal ? const TextStyle(fontFamily: "Eurostile Round", fontSize: 16) : null) - ], - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: isScreenBig ? 32 : 24, - color: getStatColor() - ) - ) - ), - if (oldPlayerStat != null || pos != null) 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))) - ] - ), - ), - alertWidgets == null - ? Text( - playerStatLabel, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - height: 1.1 - ), - ) - : TextButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(alertTitle??playerStatLabel.replaceAll(RegExp(r'\n'), " "), - style: const TextStyle( - fontFamily: "Eurostile Round Extended")), - content: SingleChildScrollView( - child: ListBody(children: alertWidgets!), - ), - actions: [ - TextButton( - child: Text(okText??"OK"), - onPressed: () {Navigator.of(context).pop();} - ) - ], - ) - ); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero)), - child: Text( - playerStatLabel, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - height: 1.1 - ), - )), - ], - ); - } -} diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index bc8f94f..58d3d44 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/glicko.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/utils/numers_formats.dart'; @@ -45,7 +45,7 @@ class TLProgress extends StatelessWidget{ children: [ if (tlData.prevAt > 0) TextSpan(text: "№ ${f0.format(tlData.prevAt)}"), if (tlData.prevAt > 0 && previousRankTRcutoff != null) const TextSpan(text: "\n"), - if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.tr)}) TR"), + if (previousRankTRcutoff != null) TextSpan(text: "${f2.format(previousRankTRcutoff)} (${comparef2.format(previousRankTRcutoff!-tlData.tr)}) ${t.stats.tr.short}"), if ((tlData.prevAt > 0 || previousRankTRcutoff != null) && previousGlickoCutoff != null) const TextSpan(text: "\n"), if (previousGlickoCutoff != null) TextSpan(text: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? t.demotionOnNextLoss : t.numOfdefeats(losses: f2.format((tlData.glicko!-previousGlickoCutoff!)/glickoForWin)), style: TextStyle(color: (tlData.standing > tlData.prevAt || ((tlData.glicko!-previousGlickoCutoff!)/glickoForWin < 0.5 && tlData.percentileRank != "d")) ? Colors.redAccent : null)) ] @@ -59,7 +59,7 @@ class TLProgress extends StatelessWidget{ children: [ if (tlData.nextAt > 0) TextSpan(text: "№ ${f0.format(tlData.nextAt)}"), if (tlData.nextAt > 0 && nextRankTRcutoff != null) const TextSpan(text: "\n"), - if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.tr)}) TR"), + if (nextRankTRcutoff != null) TextSpan(text: "${f2.format(nextRankTRcutoff)} (${comparef2.format(nextRankTRcutoff!-tlData.tr)}) ${t.stats.tr.short}"), if ((tlData.nextAt > 0 || nextRankTRcutoff != null) && nextRankGlickoCutoff != null) const TextSpan(text: "\n"), if (nextRankGlickoCutoff != null) TextSpan(text: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && ((tlData.rank != "x+" && tlData.rank != "z") || tlData.percentileRank != "x+"))) ? t.promotionOnNextWin : t.numOfVictories(wins: f2.format((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin)), style: TextStyle(color: (tlData.standing < tlData.nextAt || ((nextRankGlickoCutoff!-tlData.glicko!)/glickoForWin < 0.5 && tlData.percentileRank != "x+")) ? Colors.greenAccent : null)) ] @@ -74,8 +74,8 @@ class TLProgress extends StatelessWidget{ ranges: [ if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tlData.tr)!, color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross) else if (tlData.standing != -1) LinearGaugeRange(endValue: getBarPosition(), color: Theme.of(context).colorScheme.primary, position: LinearElementPosition.cross), - if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent, position: LinearElementPosition.inside), - if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside) + if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent.withAlpha(175), position: LinearElementPosition.inside), + if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null && previousRankTRcutoff != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent.withAlpha(175), position: LinearElementPosition.inside) ], markerPointers: [ LinearShapePointer(value: (previousRankTRcutoff != null && nextRankTRcutoff != null) ? getBarTR(tlData.tr)! : getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index ac7d4c3..4ab80a9 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -1,21 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/tetra_league.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/main.dart' show prefs; +import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; -var fDiff = NumberFormat("+#,###.####;-#,###.####"); +import 'text_timestamp.dart'; class TLRatingThingy extends StatelessWidget{ final String userID; final TetraLeague tlData; final TetraLeague? oldTl; final double? topTR; + final bool? showPositions; final DateTime? lastMatchPlayed; - const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed}); + const TLRatingThingy({super.key, required this.userID, required this.tlData, this.oldTl, this.topTR, this.lastMatchPlayed, this.showPositions}); @override Widget build(BuildContext context) { @@ -39,15 +40,17 @@ class TLRatingThingy extends StatelessWidget{ ? Image.asset("res/icons/kagari.png", height: 128) // Btw why she wearing Kazamatsuri high school uniform? : Image.asset("res/tetrio_tl_alpha_ranks/${tlData.rank}.png", height: 128), Column( + crossAxisAlignment: bigScreen ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ RichText( + textAlign: bigScreen ? TextAlign.start : TextAlign.center, text: TextSpan( - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white), + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 20, color: Colors.white, height: 0.9), children: (tlData.gamesPlayed > 9) ? switch(prefs.getInt("ratingMode")){ 1 => [ TextSpan(text: formatedGlicko[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), if (formatedGlicko.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedGlicko[1]), - TextSpan(text: " Glicko", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + TextSpan(text: " ${t.stats.glicko.short}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) ], 2 => [ TextSpan(text: "${t.top} ${formatedPercentile[0]}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), @@ -57,22 +60,48 @@ class TLRatingThingy extends StatelessWidget{ _ => [ TextSpan(text: formatedTR[0], style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), if (formatedTR.elementAtOrNull(1) != null) TextSpan(text: decimalSeparator + formatedTR[1]), - TextSpan(text: " TR", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) + TextSpan(text: " ${t.stats.tr.short}", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) ], } : [TextSpan(text: "---\n", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, color: Colors.grey)), TextSpan(text: t.gamesUntilRanked(left: 10-tlData.gamesPlayed), style: const TextStyle(color: Colors.grey, fontSize: 14)),] ) ), - if (oldTl != null) Text( - switch(prefs.getInt("ratingMode")){ - 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} Glicko", - 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", - _ => "${fDiff.format(tlData.tr - oldTl!.tr)} TR" - }, + if (oldTl != null) RichText( textAlign: TextAlign.center, - style: TextStyle( - color: tlData.tr - oldTl!.tr < 0 ? - Colors.red : - Colors.green + softWrap: true, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan(text: switch(prefs.getInt("ratingMode")){ + 1 => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} ${t.stats.glicko.short}", + 2 => "${fDiff.format(tlData.percentile * 100 - oldTl!.percentile * 100)} %", + _ => "${fDiff.format(tlData.tr - oldTl!.tr)} ${t.stats.tr.short}" + }, + style: TextStyle( + color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ + 1 => tlData.glicko! - oldTl!.glicko!, + 2 => tlData.percentile - oldTl!.percentile, + _ => tlData.tr - oldTl!.tr + }) + ), + ), + const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), + TextSpan(text: switch(prefs.getInt("ratingMode")){ + 1 => "${fDiff.format(tlData.tr - oldTl!.tr)} ${t.stats.tr.short}", + _ => "${fDiff.format(tlData.glicko! - oldTl!.glicko!)} ${t.stats.glicko.short}" + }, + style: TextStyle( + color: getDifferenceColor(switch(prefs.getInt("ratingMode")){ + 1 => tlData.tr - oldTl!.tr, + _ => tlData.glicko! - oldTl!.glicko! + }) + ), + ), + const TextSpan(text: " • ", style: TextStyle(color: Colors.grey)), + TextSpan( + text: "${fDiff.format(tlData.rd! - oldTl!.rd!)} ${t.stats.rd.short}", + style: TextStyle(color: getDifferenceColor(oldTl!.rd! - tlData.rd!)) + ) + ], ), ), if (tlData.gamesPlayed > 9) Column( @@ -85,7 +114,7 @@ class TLRatingThingy extends StatelessWidget{ children: [ TextSpan(text: prefs.getInt("ratingMode") == 2 ? "${f2.format(tlData.tr)} TR • % ${t.rank}: ${tlData.percentileRank.toUpperCase()}" : "${t.top} ${f2.format(tlData.percentile * 100)}% (${tlData.percentileRank.toUpperCase()})"), if (tlData.bestRank != "z") const TextSpan(text: " • "), - if (tlData.bestRank != "z") TextSpan(text: "${t.topRank}: ${tlData.bestRank.toUpperCase()}"), + if (tlData.bestRank != "z") TextSpan(text: t.stats.topRank(rank: tlData.bestRank.toUpperCase())), if (topTR != null) TextSpan(text: " (${f2.format(topTR)} TR)"), TextSpan(text: " • ${prefs.getInt("ratingMode") == 1 ? "${f2.format(tlData.tr)} TR • RD: " : "Glicko: ${tlData.glicko != null ? f2.format(tlData.glicko) : "---"}±"}"), TextSpan(text: f2.format(tlData.rd!), style: tlData.decaying ? TextStyle(color: tlData.rd! > 98 ? Colors.red : Colors.yellow) : null), @@ -96,6 +125,20 @@ class TLRatingThingy extends StatelessWidget{ ), ], ), + if (showPositions == true) RichText( + textAlign: bigScreen ? TextAlign.start : TextAlign.center, + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (tlData.standing != -1) TextSpan(text: "№ ${intf.format(tlData.standing)}", style: TextStyle(color: getColorOfRank(tlData.standing))), + if (tlData.standing != -1 || tlData.standingLocal != -1) const TextSpan(text: " • "), + if (tlData.standingLocal != -1) TextSpan(text: "№ ${intf.format(tlData.standingLocal)} ${t.localStanding}", style: TextStyle(color: getColorOfRank(tlData.standingLocal))), + if (tlData.standing != -1 && tlData.standingLocal != -1) const TextSpan(text: " • "), + TextSpan(text: timestamp(tlData.timestamp)), + ] + ), + ), ], ), ], diff --git a/lib/widgets/tl_records_thingy.dart b/lib/widgets/tl_records_thingy.dart new file mode 100644 index 0000000..26e33ab --- /dev/null +++ b/lib/widgets/tl_records_thingy.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/beta_record.dart'; +import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/widgets/beta_league_entry_thingy.dart'; +import 'package:tetra_stats/widgets/future_error.dart'; + +class TLRecords extends StatefulWidget { + final String userID; + + /// Widget, that displays Tetra League records. + /// Accepts list of TL records ([data]) and [userID] of player from the view + const TLRecords(this.userID); + + @override + State createState() => _TLRecordsState(); +} + +class _TLRecordsState extends State { + List records = []; + bool isFetchingRecords = false; + bool reachedEndOfRecords = false; + final StreamController> _recordsStreamController = StreamController>.broadcast(); + Stream> get recordsStream => _recordsStreamController.stream; + String? recordsPrisecter; + late final ScrollController _scrollController; + + @override + void initState(){ + _scrollController = ScrollController(); + _scrollController.addListener(() { + _scrollController.addListener(() { + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.position.pixels; + + if (currentScroll == maxScroll) { + _fetchRecord(widget.userID); + } + }); + }); + _fetchRecord(widget.userID); + super.initState(); + } + + Future _fetchRecord(String userID) async { + if (isFetchingRecords || reachedEndOfRecords) { + // Avoid fetching new data while already fetching + return; + } + try { + isFetchingRecords = true; + + final items = (await teto.fetchTLStream(userID, prisecter: recordsPrisecter)).records; + + if (items.isEmpty) reachedEndOfRecords = true; + records.addAll(items); + if (items.isNotEmpty){ + _recordsStreamController.add(records); + recordsPrisecter = records.last.prisecter.toString(); + } else{ + _recordsStreamController.add([]); + } + } catch (e) { + _recordsStreamController.addError(e); + } finally { + // Set to false when data fetching is complete + isFetchingRecords = false; + } +} + + Future resetRecords(String userID) async { + records.clear(); + recordsPrisecter = null; + reachedEndOfRecords = false; + _fetchRecord(userID); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: recordsStream, + 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 SizedBox( + height: MediaQuery.of(context).size.height - 130, + child: ListView.builder( + controller: _scrollController, + itemCount: records.length, + prototypeItem: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: ListTile( + leading: Text("0"), + title: Text("ehhh...", style: TextStyle(fontSize: 22)), + trailing: SizedBox(height: 36, width: 1), + subtitle: const Text("eh\n...", style: TextStyle(color: Colors.grey, fontSize: 12)), + ), + ), + itemBuilder: (BuildContext context, int index){ + return BetaLeagueEntryThingy(records[index], widget.userID); + } + ), + ); + } + if (snapshot.hasError){ return SizedBox(height: 500, child: Center(child: FutureError(snapshot))); } + return const Center(child: Text("whar?")); + } + }, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 853adde..b676a55 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -1,325 +1,137 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart'; -import 'package:tetra_stats/utils/colors_functions.dart'; -import 'package:tetra_stats/utils/numers_formats.dart'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/widgets/gauget_num.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; -import 'package:tetra_stats/widgets/tl_progress_bar.dart'; -import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; - - -var intFDiff = NumberFormat("+#,###.000;-#,###.000"); - -class TLThingy extends StatefulWidget { - final TetraLeague tl; - final String userID; - final List states; - final bool showTitle; - final bool bot; - final bool guest; - final double? topTR; - final PlayerLeaderboardPosition? lbPositions; - final TetraLeague? averages; - final double? thatRankCutoff; - final double? thatRankCutoffGlicko; - final double? thatRankTarget; - final double? nextRankCutoff; - final double? nextRankCutoffGlicko; - final double? nextRankTarget; - final DateTime? lastMatchPlayed; - const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.guest=false, this.topTR, this.lbPositions, this.averages, this.nextRankCutoff, this.thatRankCutoff, this.thatRankCutoffGlicko, this.nextRankCutoffGlicko, this.nextRankTarget, this.thatRankTarget, this.lastMatchPlayed}); - - @override - State createState() => _TLThingyState(); -} - -class _TLThingyState extends State with TickerProviderStateMixin { - late bool oskKagariGimmick; - late TetraLeague? oldTl; - late TetraLeague currentTl; - late RangeValues _currentRangeValues; - late List sortedStates; - -@override - void initState() { - _currentRangeValues = const RangeValues(0, 1); - sortedStates = widget.states.reversed.toList(); - oldTl = sortedStates.elementAtOrNull(1); - currentTl = widget.tl; - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - String decimalSeparator = f2.symbols.DECIMAL_SEP; - List estTRformated = currentTl.estTr != null ? f2.format(currentTl.estTr!.esttr).split(decimalSeparator) : []; - List estTRaccFormated = currentTl.esttracc != null ? intFDiff.format(currentTl.esttracc!).split(".") : []; - if (currentTl.gamesPlayed == 0) return Center(child: Text(widget.guest ? t.anonTL : widget.bot ? t.botTL : t.neverPlayedTL, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28), textAlign: TextAlign.center,)); - return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth >= 768; - return ListView.builder( - physics: const ClampingScrollPhysics(), - itemCount: 1, - itemBuilder: (BuildContext context, int index) { - return Column( - children: [ - if (widget.showTitle) Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - //if (DateTime.now().isBefore(seasonEnd)) Text(t.seasonEnds(countdown: countdown(seasonLeft))) - //else Text(t.seasonEnded), - if (oldTl != null) Text(t.comparingWith(newDate: timestamp(currentTl.timestamp), oldDate: timestamp(oldTl!.timestamp)), - textAlign: TextAlign.center,), - if (oldTl != null) RangeSlider(values: _currentRangeValues, max: widget.states.length.toDouble(), - labels: RangeLabels( - _currentRangeValues.start.round().toString(), - _currentRangeValues.end.round().toString(), - ), - onChanged: (RangeValues values) { - setState(() { - _currentRangeValues = values; - if (values.start.round() == 0){ - currentTl = widget.tl; - }else{ - currentTl = sortedStates[values.start.round()-1]!; - } - if (values.end.round() == 0){ - oldTl = widget.tl; - }else{ - oldTl = sortedStates[values.end.round()-1]; - } - }); - }, - ), - TLRatingThingy(userID: widget.userID, tlData: currentTl, oldTl: oldTl, topTR: widget.topTR, lastMatchPlayed: widget.lastMatchPlayed), - if (currentTl.gamesPlayed > 9) TLProgress( - tlData: currentTl, - previousRankTRcutoff: widget.thatRankCutoff, - previousGlickoCutoff: widget.thatRankCutoffGlicko, - previousRankTRcutoffTarget: widget.thatRankTarget, - nextRankTRcutoff: widget.nextRankCutoff, - nextRankGlickoCutoff: widget.nextRankCutoffGlicko, - nextRankTRcutoffTarget: widget.nextRankTarget, - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 16, 8, 48), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - if (currentTl.apm != null) StatCellNum(playerStat: currentTl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm, pos: widget.lbPositions?.apm, averageStat: widget.averages?.apm), - if (currentTl.pps != null) StatCellNum(playerStat: currentTl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps, pos: widget.lbPositions?.pps, averageStat: widget.averages?.pps, smallDecimal: false), - if (currentTl.vs != null) StatCellNum(playerStat: currentTl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs, pos: widget.lbPositions?.vs, averageStat: widget.averages?.vs), - if (currentTl.standingLocal > 0) StatCellNum(playerStat: currentTl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), - StatCellNum(playerStat: currentTl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed, pos: widget.lbPositions?.gamesPlayed), - StatCellNum(playerStat: currentTl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon, pos: widget.lbPositions?.gamesWon), - StatCellNum(playerStat: currentTl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null, pos: widget.lbPositions?.winrate, averageStat: widget.averages != null ? widget.averages!.winrate * 100 : null), - ], - ), - ), - if (currentTl.nerdStats != null) - Column( - children: [ - Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 35, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - GaugetNum(playerStat: currentTl.nerdStats!.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.app}") - ], oldPlayerStat: oldTl?.nerdStats?.app, pos: widget.lbPositions?.app, - averageStat: widget.averages?.nerdStats?.app), - GaugetNum(playerStat: currentTl.nerdStats!.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${currentTl.nerdStats!.vsapm}") - ], oldPlayerStat: oldTl?.nerdStats?.vsapm, pos: widget.lbPositions?.vsapm, - averageStat: widget.averages?.nerdStats?.vsapm) - ]), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: currentTl.nerdStats!.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - pos: widget.lbPositions?.dss, - averageStat: widget.averages?.nerdStats?.dss, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dss,), - StatCellNum(playerStat: currentTl.nerdStats!.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - pos: widget.lbPositions?.dsp, - averageStat: widget.averages?.nerdStats?.dsp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${currentTl.nerdStats!.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.dsp,), - StatCellNum(playerStat: currentTl.nerdStats!.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - pos: widget.lbPositions?.appdsp, - averageStat: widget.averages?.nerdStats?.appdsp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${currentTl.nerdStats!.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.appdsp,), - StatCellNum(playerStat: currentTl.nerdStats!.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - pos: widget.lbPositions?.cheese, - //averageStat: rankAverages?.nerdStats?.cheese, TODO: questonable - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${currentTl.nerdStats!.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: false, - oldPlayerStat: oldTl?.nerdStats?.cheese,), - StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - pos: widget.lbPositions?.gbe, - averageStat: widget.averages?.nerdStats?.gbe, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.gbe,), - StatCellNum(playerStat: currentTl.nerdStats!.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - pos: widget.lbPositions?.nyaapp, - averageStat: widget.averages?.nerdStats?.nyaapp, smallDecimal: false, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${currentTl.nerdStats!.nyaapp}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.nyaapp,), - StatCellNum(playerStat: currentTl.nerdStats!.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - pos: widget.lbPositions?.area, - averageStat: widget.averages?.nerdStats?.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${currentTl.nerdStats!.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true, - oldPlayerStat: oldTl?.nerdStats?.area,) - ]), - ) - ], - ), - if (currentTl.estTr != null) - Padding( - padding: const EdgeInsets.fromLTRB(8, 20, 8, 20), - child: Container( - height: 70, - constraints: const BoxConstraints(maxWidth: 500), - child: Stack( - children: [ - Positioned( - left: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.statCellNum.estOfTR, style: const TextStyle(height: 0.1),), - RichText( - text: TextSpan( - text: estTRformated[0], - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), - children: [TextSpan(text: decimalSeparator+estTRformated[1], style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100))] - ), - ), - RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), - children: [ - if (oldTl?.estTr?.esttr != null) TextSpan(text: comparef.format(currentTl.estTr!.esttr - oldTl!.estTr!.esttr), style: TextStyle( - color: oldTl!.estTr!.esttr > currentTl.estTr!.esttr ? Colors.redAccent : Colors.greenAccent - ),), - if (oldTl?.estTr?.esttr != null && widget.lbPositions?.estTr != null) const TextSpan(text: " • "), - if (widget.lbPositions?.estTr != null) TextSpan(text: widget.lbPositions!.estTr!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.estTr!.percentage*100)}%" : "№${widget.lbPositions!.estTr!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.estTr!.position))), - if (widget.lbPositions?.estTr != null || oldTl?.estTr?.esttr != null) const TextSpan(text: " • "), - TextSpan(text: "Glicko: ${f2.format(currentTl.estTr!.estglicko)}") - ] - ), - ), - ],), - ), - Positioned( - right: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(t.statCellNum.accOfEst, style: const TextStyle(height: 0.1),), - RichText( - text: TextSpan( - text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? estTRaccFormated[0] : "---", - style: TextStyle(fontFamily: "Eurostile Round", fontSize: bigScreen ? 36 : 30, fontWeight: FontWeight.w500, color: Colors.white), - children: [ - TextSpan(text: (currentTl.esttracc != null && currentTl.bestRank != "z") ? decimalSeparator+estTRaccFormated[1] : ".---", style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w100)) - ] - ), - ), - if ((oldTl?.esttracc != null || widget.lbPositions != null) && currentTl.bestRank != "z") RichText(text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey, height: 0.5), - children: [ - if (oldTl?.esttracc != null) TextSpan(text: comparef.format(currentTl.esttracc! - oldTl!.esttracc!), style: TextStyle( - color: oldTl!.esttracc! > currentTl.esttracc! ? Colors.redAccent : Colors.greenAccent - ),), - if (oldTl?.esttracc != null && widget.lbPositions?.accOfEst != null) const TextSpan(text: " • "), - if (widget.lbPositions?.accOfEst != null) TextSpan(text: widget.lbPositions!.accOfEst!.position >= 1000 ? "${t.top} ${f2.format(widget.lbPositions!.accOfEst!.percentage*100)}%" : "№${widget.lbPositions!.accOfEst!.position}", style: TextStyle(color: getColorOfRank(widget.lbPositions!.accOfEst!.position))) - ] - ), - ), - ],), - ) - ], - ), - ) - ), - if (currentTl.nerdStats != null) Graphs(currentTl.apm!, currentTl.pps!, currentTl.vs!, currentTl.nerdStats!, currentTl.playstyle!) - ] - ); - }, - ); - }); - } -} +import 'package:flutter/material.dart'; +import 'package:tetra_stats/data_objects/cutoff_tetrio.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/tetra_league.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.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'; +import 'package:tetra_stats/widgets/gauget_thingy.dart'; +import 'package:tetra_stats/widgets/tl_progress_bar.dart'; +import 'package:tetra_stats/widgets/tl_rating_thingy.dart'; + +class TetraLeagueThingy extends StatelessWidget{ + final TetraLeague league; + final TetraLeague? toCompare; + final Cutoffs? cutoffs; + final CutoffTetrio? averages; + final PlayerLeaderboardPosition? lbPos; + final double width; + + const TetraLeagueThingy({super.key, required this.league, this.toCompare, this.cutoffs, this.averages, this.lbPos, this.width = double.infinity}); + + List secondColumn(){ + return [ + TableRow(children: [ + Text(intf.format(league.gamesPlayed), textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18)), + Tooltip( + message: "${t.stats.gp.full}", + child: Text(" ${t.stats.gp.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18)) + ), + if (toCompare != null) Text(" (${comparef2.format(league.gamesPlayed-toCompare!.gamesPlayed)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: Colors.grey)), + if (lbPos != null) Text(lbPos?.gamesPlayed != null ? (lbPos!.gamesPlayed!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.gamesPlayed!.percentage*100)}%)" : " (№ ${lbPos!.gamesPlayed!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.gamesPlayed != null ? getColorOfRank(lbPos!.gamesPlayed!.position) : null)) + ]), + TableRow(children: [ + Text(intf.format(league.gamesWon), textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18)), + Tooltip( + message: "${t.stats.gw.full}", + child: Text(" ${t.stats.gw.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18)) + ), + if (toCompare != null) Text(" (${comparef2.format(league.gamesWon-toCompare!.gamesWon)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: Colors.grey)), + if (lbPos != null) Text(lbPos?.gamesWon != null ? (lbPos!.gamesWon!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.gamesWon!.percentage*100)}%)" : " (№ ${lbPos!.gamesWon!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.gamesWon != null ? getColorOfRank(lbPos!.gamesWon!.position) : null)) + ]), + TableRow(children: [ + Tooltip(child: Text("${league.gxe.isNegative ? "---" : f3.format(league.gxe)}", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.gxe.isNegative ? Colors.grey : Colors.white)), message: "${f2.format(league.s1tr)} S1 TR"), + Tooltip( + message: "${t.stats.glixare.full}", + child: Tooltip(child: Text(" ${t.stats.glixare.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.gxe.isNegative ? Colors.grey : Colors.white)), message: "Glixare") + ), + if (toCompare != null) Text(" (${comparef.format(league.gxe-toCompare!.gxe)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: getDifferenceColor(league.gxe-toCompare!.gxe))), + if (lbPos != null) Text(lbPos?.glixare != null ? (lbPos!.glixare!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.glixare!.percentage*100)}%)" : " (№ ${lbPos!.glixare!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.glixare != null ? getColorOfRank(lbPos!.glixare!.position) : null)) + ]), + ]; + } + + @override + Widget build(BuildContext context) { + print(ranks2.indexOf(league.rank != "z" ? league.rank : league.percentileRank)-1); + return Card( + child: Column( + children: [ + TLRatingThingy(userID: league.id, tlData: league, oldTl: toCompare, showPositions: true), + if (league.gamesPlayed > 9) TLProgress( + tlData: league, + previousRankTRcutoff: cutoffs != null ? cutoffs!.tr[league.rank != "z" ? league.rank : league.percentileRank] : null, + nextRankTRcutoff: cutoffs != null ? cutoffs!.tr[ranks2[ranks2.indexOf(league.rank != "z" ? league.rank : league.percentileRank)-1]] : null, + previousRankTRcutoffTarget: league.rank != "z" ? rankTargets[league.rank] : null, + nextRankTRcutoffTarget: (league.rank != "z" && league.rank != "x+") ? rankTargets[ranks2[ranks2.indexOf(league.rank != "z" ? league.rank : league.percentileRank)-1]] : null, + previousGlickoCutoff: cutoffs != null ? cutoffs!.glicko[league.rank != "z" ? league.rank : league.percentileRank] : null, + nextRankGlickoCutoff: cutoffs != null ? cutoffs!.glicko[ranks2[ranks2.indexOf(league.rank != "z" ? league.rank : league.percentileRank)-1]] : null, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(league.apm != null ? f2.format(league.apm) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)), + Tooltip( + message: "${t.stats.apm.full}${(averages != null) ? "\n${t.rankView.avgForRank(rank: league.percentileRank.toUpperCase())}: ${f2.format(averages!.apm)} ${t.stats.apm.short}" : ""}", + child: Text(" ${t.stats.apm.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.apm != null ? getStatColor(league.apm!, averages?.apm, true) : Colors.grey)) + ), + if (toCompare != null) Text(" (${comparef2.format(league.apm!-toCompare!.apm!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: getDifferenceColor(league.apm!-toCompare!.apm!))), + if (lbPos != null) Text(lbPos?.apm != null ? (lbPos!.apm!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.apm!.percentage*100)}%)" : " (№ ${lbPos!.apm!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.apm != null ? getColorOfRank(lbPos!.apm!.position) : null)) + ]), + TableRow(children: [ + Text(league.pps != null ? f2.format(league.pps) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)), + Tooltip( + message: "${t.stats.pps.full}${(averages != null) ? "\n${t.rankView.avgForRank(rank: league.percentileRank.toUpperCase())}: ${f2.format(averages!.pps)} ${t.stats.pps.short}" : ""}", + child: Text(" ${t.stats.pps.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.pps != null ? getStatColor(league.pps!, averages?.pps, true) : Colors.grey)) + ), + if (toCompare != null) Text(" (${comparef2.format(league.pps!-toCompare!.pps!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: getDifferenceColor(league.pps!-toCompare!.pps!))), + if (lbPos != null) Text(lbPos?.pps != null ? (lbPos!.pps!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.pps!.percentage*100)}%)" : " (№ ${lbPos!.pps!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.pps != null ? getColorOfRank(lbPos!.pps!.position) : null)) + ]), + TableRow(children: [ + Text(league.vs != null ? f2.format(league.vs) : "-.--", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)), + Tooltip( + message: "${t.stats.vs.full}${(averages != null) ? "\n${t.rankView.avgForRank(rank: league.percentileRank.toUpperCase())}: ${f2.format(averages!.vs)} ${t.stats.vs.short}" : ""}", + child: Text(" ${t.stats.vs.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: league.vs != null ? getStatColor(league.vs!, averages?.vs, true) : Colors.grey)) + ), + if (toCompare != null) Text(" (${comparef2.format(league.vs!-toCompare!.vs!)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: getDifferenceColor(league.vs!-toCompare!.vs!))), + if (lbPos != null) Text(lbPos?.vs != null ? (lbPos!.vs!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.vs!.percentage*100)}%)" : " (№ ${lbPos!.vs!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.vs != null ? getColorOfRank(lbPos!.vs!.position) : null)) + ]), + if (width <= 600) TableRow(children: [ + Text(!league.winrate.isNegative ? percentage.format(league.winrate) : "---", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: !league.winrate.isNegative ? Colors.white : Colors.grey)), + Text(" ${t.stats.winrate.short}", style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: !league.winrate.isNegative ? Colors.white : Colors.grey)), + if (toCompare != null) Text(" (${comparef2.format((league.winrate-toCompare!.winrate)*100)})", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: getDifferenceColor(league.winrate-toCompare!.winrate))), + if (lbPos != null) Text(lbPos?.winrate != null ? (lbPos!.winrate!.position >= 1000 ? " (${t.top} ${f2.format(lbPos!.winrate!.percentage*100)}%)" : " (№ ${lbPos!.winrate!.position})") : "(№ ---)", style: TextStyle(color: lbPos?.winrate != null ? getColorOfRank(lbPos!.winrate!.position) : null)) + ]), + if (width <= 400) ...secondColumn() + ], + ), + ), + ), + if (width > 600) GaugetThingy(value: league.winrate, min: 0, max: 1, tickInterval: 0.25, label: "Winrate", sideSize: 128, fractionDigits: 2, moreIsBetter: true, oldValue: toCompare?.winrate, percentileFormat: true, lbPos: lbPos?.winrate), + if (width > 400) Expanded( + child: Center( + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + defaultColumnWidth:const IntrinsicColumnWidth(), + children: secondColumn(), + ), + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index f27e7a3..9362cef 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -1,429 +1,420 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.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/main.dart' show teto; -import 'package:tetra_stats/views/compare_view.dart'; -import 'package:intl/intl.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'dart:developer' as developer; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; -import 'package:tetra_stats/widgets/text_timestamp.dart'; - -const Map 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 -}; - -Future copyToClipboard(String text) async { - await Clipboard.setData(ClipboardData(text: text)); -} - -class UserThingy extends StatelessWidget { - final TetrioPlayer player; - final bool showStateTimestamp; - final Function setState; - - const UserThingy({super.key, required this.player, required this.showStateTimestamp, required this.setState}); - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return LayoutBuilder(builder: (context, constraints) { - bool bigScreen = constraints.maxWidth > 768; - double bannerHeight = bigScreen ? 240 : 120; - double pfpHeight = 128; - int xpTableID = 0; - - while (player.xp > xpTableScuffed.values.toList()[xpTableID]) { - xpTableID++; - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - alignment: Alignment.topCenter, - children: [ - if (player.bannerRevision != null) - Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${player.userId}&rv=${player.bannerRevision}" : "https://tetr.io/user-content/banners/${player.userId}.jpg?rv=${player.bannerRevision}", - fit: BoxFit.cover, - height: bannerHeight, - errorBuilder: (context, error, stackTrace) { - developer.log("Error with building banner image", name: "main_view", error: error, stackTrace: stackTrace); - return Container(); - }, - ), - Padding( - padding: EdgeInsets.fromLTRB(8, player.bannerRevision != null ? bannerHeight / 1.4 : 0, 8, bigScreen ? 16 : 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Wrap( - direction: bigScreen ? Axis.horizontal : Axis.vertical, - alignment: WrapAlignment.spaceBetween, - spacing: bigScreen ? 25 : 0, - //mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.center, - clipBehavior: Clip.hardEdge, - children: [ - Wrap( - direction: bigScreen ? Axis.horizontal : Axis.vertical, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: bigScreen ? 20 : 0, - clipBehavior: Clip.hardEdge, - children: [ - Stack( - alignment: Alignment.topCenter, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(1000), - child: player.role == "banned" - ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) - : player.avatarRevision != null - ? Image.network(kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${player.userId}&rv=${player.avatarRevision}" : "https://tetr.io/user-content/avatars/${player.userId}.jpg?rv=${player.avatarRevision}", - // TODO: osk banner can cause memory leak - fit: BoxFit.fitHeight, height: 128, errorBuilder: (context, error, stackTrace) { - developer.log("Error with building profile picture", name: "main_view", error: error, stackTrace: stackTrace); - return Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight); - }) - : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight), - ), - if (player.verified) - Padding( - padding: EdgeInsets.fromLTRB(pfpHeight - 22, pfpHeight - 32, 0, 0), - child: const Icon(Icons.verified), - ) - ], - ), - Column( - children: [ - Text(player.username, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: bigScreen ? 42 : 28, - shadows: textShadow, - )), - TextButton( - child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)), - onPressed: () { - copyToClipboard(player.userId); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); - }), - ], - ), - ], - ), - showStateTimestamp - ? Text(t.fetchDate(date: timestamp(player.state))) - : Wrap(direction: Axis.horizontal, alignment: WrapAlignment.center, spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, children: [ - FutureBuilder( - future: teto.isPlayerTracking(player.userId), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.data != null && snapshot.data!) { - return Column( - children: [ - IconButton( - icon: const Icon( - Icons.person_remove, - shadows: [ - 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, - ), - ],), - onPressed: () { - teto.deletePlayerToTrack(player.userId).then((value) => setState()); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stoppedBeingTracked))); - }, - ), - Text(t.stopTracking, textAlign: TextAlign.center) - ], - ); - } else { - return Column( - children: [ - IconButton( - icon: const Icon( - Icons.person_add, - shadows: [ - 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, - ), - ],), - onPressed: () { - teto.addPlayerToTrack(player).then((value) => setState()); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked))); - }, - ), - Text(t.track, textAlign: TextAlign.center) - ], - ); - } - } - }), - Column( - children: [ - IconButton( - icon: const Icon( - Icons.balance, - shadows: [ - 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, - ), - ],), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CompareView(greenSide: [player, null, null], redSide: const [null, null, null], greenMode: Mode.player, redMode: Mode.player), - ), - ); - }, - ), - Text(t.compare, textAlign: TextAlign.center) - ], - ) - ]) - ]), - ], - ), - ], - ), - ), - ], - ), - if (!["banned", "p1nkl0bst3r"].contains(player.role)) - Wrap( - // mainAxisSize: MainAxisSize.min, - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, // hard WHAT??? - children: [ - if (!player.level.isNegative && !player.level.isNaN) StatCellNum( - playerStat: player.level, - playerStatLabel: t.statCellNum.xpLevel, - isScreenBig: bigScreen, - alertWidgets: [ - Text( - "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(player.xp)} XP", - style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold) - ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: SfLinearGauge( - minimum: 0, - maximum: 1, - interval: 1, - ranges: [ - LinearGaugeRange(startValue: 0, endValue: player.level - player.level.floor(), color: Colors.cyanAccent), - LinearGaugeRange(startValue: 0, endValue: (player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) - ], - // markerPointers: [LinearShapePointer(value: player.level - player.level.floor(), position: LinearElementPosition.inside, shapeType: LinearShapePointerType.triangle, color: Colors.white, height: 20)], - showTicks: true, - showLabels: false - ), - ), - Text("${t.statCellNum.xpProgress}: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"), - Text("${t.statCellNum.xpFrom0ToLevel(n: xpTableScuffed.keys.toList()[xpTableID])}: ${((player.xp / xpTableScuffed.values.toList()[xpTableID]) * 100).toStringAsFixed(2)} % (${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - player.xp)} ${t.statCellNum.xpLeft})")], - okText: t.popupActions.ok, - higherIsBetter: true, - ), - if (player.gameTime >= Duration.zero) - StatCellNum( - playerStat: player.gameTime.inHours, - playerStatLabel: t.statCellNum.hoursPlayed, - isScreenBig: bigScreen, - alertTitle: t.exactGametime, - alertWidgets: [Text(player.gameTime.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 24),)], - higherIsBetter: true,), - if (player.gamesPlayed >= 0) - StatCellNum( - playerStat: player.gamesPlayed, - isScreenBig: bigScreen, - playerStatLabel: t.statCellNum.onlineGames, - higherIsBetter: true,), - if (player.gamesWon >= 0) - StatCellNum( - playerStat: player.gamesWon, - isScreenBig: bigScreen, - playerStatLabel: t.statCellNum.gamesWon, - higherIsBetter: true,), - if (player.friendCount > 0) - StatCellNum( - playerStat: player.friendCount, - isScreenBig: bigScreen, - playerStatLabel: t.statCellNum.friends, - higherIsBetter: true,), - ], - ), - if (player.role == "banned") Text( - t.bigRedBanned, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontWeight: FontWeight.w900, - color: Colors.red, - fontSize: bigScreen ? 60 : 45, - ), - ), - if (player.role == "p1nkl0bst3r") Text( - t.p1nkl0bst3rAlert, - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - ) - ), - if (player.badstanding != null && player.badstanding!) - Text( - t.bigRedBadStanding, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontWeight: FontWeight.w900, - color: Colors.red, - fontSize: bigScreen ? 60 : 45, - ), - ), - if (player.role != "p1nkl0bst3r") Padding( - padding: EdgeInsets.only(top: bigScreen ? 8 : 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan(text: "", style: const TextStyle( - fontFamily: "Eurostile Round", - fontSize: 16, - color: Colors.white, - ), - children: [ - if (player.country != null) TextSpan(text: "${t.countries[player.country]} • "), - TextSpan(text: "${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${timestamp(player.registrationTime!)}'}"), - if (player.supporterTier > 0) const TextSpan(text: " • "), - if (player.supporterTier > 0) WidgetSpan(child: Icon(player.supporterTier > 1 ? Icons.star : Icons.star_border, color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), - if (player.supporterTier > 0) TextSpan(text: player.supporterTier.toString(), style: TextStyle(color: player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)) - ] - ) - ), - // Text( - // "${player.country != null ? "${t.countries[player.country]} • " : ""}${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " ${t.botCreatedBy} ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? t.notSupporter : t.supporter(tier: player.supporterTier)}", - // textAlign: TextAlign.center, - // style: const TextStyle( - // fontFamily: "Eurostile Round", - // fontSize: 16, - // )), - ) - ], - ), - ), - Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - clipBehavior: Clip.hardEdge, - children: [ - for (var badge in player.badges) - IconButton( - onPressed: () => showDialog( - 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.asset("res/tetrio_badges/${badge.badgeId}.png"), - Text(badge.ts != null - ? t.obtainDate(date: timestamp(badge.ts!)) - : t.assignedManualy), - ], - ) - ], - ), - ), - actions: [ - TextButton( - child: Text(t.popupActions.ok), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ), - tooltip: badge.label, - icon: Image.asset( - "res/tetrio_badges/${badge.badgeId}.png", - height: 32, - width: 32, - errorBuilder: (context, error, stackTrace) { - developer.log("Error with building $badge", name: "main_view", error: error, stackTrace: stackTrace); - return Image.network( - kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBadge&badge=${badge.badgeId}" : "https://tetr.io/res/badges/${badge.badgeId}.png", - height: 32, - width: 32, - errorBuilder:(context, error, stackTrace) { - return Image.asset("res/icons/kagari.png", height: 32, width: 32); - } - ); - }, - )) - ], - ), - ], - ); - }); - } -} +import 'dart:io'; +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/data_objects/tetrio_constants.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/copy_to_clipboard.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; +import 'package:tetra_stats/utils/relative_timestamps.dart'; +import 'package:tetra_stats/utils/text_shadow.dart'; +import 'package:tetra_stats/views/compare_view_tiles.dart'; +import 'package:tetra_stats/widgets/text_timestamp.dart'; +import 'package:transparent_image/transparent_image.dart'; + +Future osksFuture = loadImage(Uri.https("tetr.io", "/user-content/banners/5e32fc85ab319c2ab1beb07c.jpg", {"rv": "1628366386763"})); + +Future loadImage(Uri url) async { + final response = await teto.client.get(url); + return await decodeImageFromList(response.bodyBytes); +} + + Widget createCustomImage(ui.Image image) { + return SizedBox( + width: image.width.toDouble(), + height: image.height.toDouble()/64, + child: CustomPaint( + size: Size(128.0, 128.0), + painter: ImagePainter(image), + ), + ); + } + + class ImagePainter extends CustomPainter { + ImagePainter(ui.Image this.image); + final ui.Image image; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.red + ..strokeWidth = 5 + ..style = PaintingStyle.stroke; + + canvas.translate(-240, 0); + canvas.scale(0.5); + canvas.drawImage(image, Offset.zero, paint); + } + + @override + bool shouldRepaint(ImagePainter oldDelegate) => + image != oldDelegate.image; + } + +class UserThingy extends StatefulWidget { + final TetrioPlayer player; + final bool showStateTimestamp; + final bool initIsTracking; + final Function setState; + + const UserThingy({super.key, required this.player, required this.initIsTracking, required this.showStateTimestamp, required this.setState}); + + @override + State createState() => _UserThingyState(); +} + +class _UserThingyState extends State with SingleTickerProviderStateMixin { + late AnimationController _addToTrackAnimController; + late Animation _addToTrackAnim; + + @override + void initState(){ + _addToTrackAnimController = AnimationController( + value: widget.initIsTracking ? 1.0 : 0.0, + duration: Durations.extralong4, + vsync: this, + ); + _addToTrackAnim = new Tween( + begin: 0.0, + end: 1.0, + ).animate(new CurvedAnimation( + parent: _addToTrackAnimController, + curve: Cubic(.15,-0.40,.86,-0.39), + reverseCurve: Cubic(0,.99,.99,1.01) + )); + + super.initState(); + } + + @override + void dispose() { + _addToTrackAnimController.dispose(); + super.dispose(); + } + + Color roleColor(String role){ + switch (role){ + case "sysop": + return const Color.fromARGB(255, 23, 165, 133); + case "admin": + return const Color.fromARGB(255, 255, 78, 138); + case "mod": + return const Color.fromARGB(255, 204, 128, 242); + case "halfmod": + return const Color.fromARGB(255, 95, 118, 254); + case "bot": + return const Color.fromARGB(255, 60, 93, 55); + case "banned": + return const Color.fromARGB(255, 248, 28, 28); + default: + return Colors.white10; + } + } + + String fontStyle(int length){ + if (length < 10) return "Eurostile Round Extended"; + else if (length < 13) return "Eurostile Round"; + else return "Eurostile Round Condensed"; + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + return LayoutBuilder(builder: (context, constraints) { + double pfpHeight = 128; + int xpTableID = 0; + + while (widget.player.xp > xpTableScuffed.values.toList()[xpTableID]) { + xpTableID++; + } + + return Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Container( + constraints: const BoxConstraints(maxWidth: 960), + height: widget.player.bannerRevision != null ? 218.0 : 138.0, + child: Stack( + children: [ + // Very weird solution to draw only the first frame of the gif + if (widget.player.userId == "5e32fc85ab319c2ab1beb07c") FutureBuilder( + future: osksFuture, + builder: (context, snapshot) { + switch (snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return SizedBox(width: 960); + case ConnectionState.done: + return createCustomImage(snapshot.data!); + } + }, + ) // If not osk, using a normal widget like a normal human being + else if (widget.player.bannerRevision != null) FadeInImage.memoryNetwork( + image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioBanner&user=${widget.player.userId}&rv=${widget.player.bannerRevision}" : "https://tetr.io/user-content/banners/${widget.player.userId}.jpg?rv=${widget.player.bannerRevision}", + placeholder: kTransparentImage, + fit: BoxFit.cover, + height: 120, + fadeInCurve: Easing.standard, fadeInDuration: Durations.long4, + imageErrorBuilder: (context, object, trace){ + return SizedBox(width: 960); + } + ), + Positioned( + top: widget.player.bannerRevision != null ? 90.0 : 10.0, + left: 16.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(1000), + child: widget.player.role == "banned" + ? Image.asset("res/avatars/tetrio_banned.png", fit: BoxFit.fitHeight, height: pfpHeight,) + : widget.player.avatarRevision != null + ? FadeInImage.memoryNetwork(image: kIsWeb ? "https://ts.dan63.by/oskware_bridge.php?endpoint=TetrioProfilePicture&user=${widget.player.userId}&rv=${widget.player.avatarRevision}" : "https://tetr.io/user-content/avatars/${widget.player.userId}.jpg?rv=${widget.player.avatarRevision}", + fit: BoxFit.fitHeight, height: 128, placeholder: kTransparentImage, fadeInCurve: Easing.emphasizedDecelerate, fadeInDuration: Durations.long4) + : Image.asset("res/avatars/tetrio_anon.png", fit: BoxFit.fitHeight, height: pfpHeight), + ) + ), + Positioned( + top: widget.player.bannerRevision != null ? 120.0 : 40.0, + left: 160.0, + child: Tooltip( + message: "${widget.player.userId}\n(${t.copyUserID})", + child: RichText(text: TextSpan(text: widget.player.username, style: TextStyle( + fontFamily: fontStyle(widget.player.username.length), + fontSize: 28, + color: Colors.white + ), + recognizer: TapGestureRecognizer()..onTap = (){ + copyToClipboard(widget.player.userId); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); + } + ) + ) + ), + ), + Positioned( + top: (kIsWeb || !Platform.isAndroid) ? widget.player.bannerRevision != null ? 160.0 : 80.0 : widget.player.bannerRevision != null ? 152.0 : 72.0, + left: 160.0, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Tooltip( + message: t.playerRole[widget.player.role]??"Unknown role ${widget.player.role}", + child: Chip(label: Text(widget.player.role.toUpperCase(), style: const TextStyle(shadows: textShadow),), padding: const EdgeInsets.all(0.0), color: WidgetStatePropertyAll(roleColor(widget.player.role))) + ), + ), + RichText( + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.white), + children: + [ + if (widget.player.friendCount > 0) WidgetSpan(child: Tooltip(message: t.stats.followers, child: Icon(Icons.person)), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (widget.player.friendCount > 0) TextSpan(text: "${intf.format(widget.player.friendCount)} "), + if (widget.player.supporterTier > 0) WidgetSpan(child: Tooltip(message: t.supporter(tier: widget.player.supporterTier), child: Icon(widget.player.supporterTier > 1 ? Icons.star : Icons.star_border, color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)), alignment: PlaceholderAlignment.middle, baseline: TextBaseline.alphabetic), + if (widget.player.supporterTier > 0) TextSpan(text: widget.player.supporterTier.toString(), style: TextStyle(color: widget.player.supporterTier > 1 ? Colors.yellowAccent : Colors.white)), + ] + ) + ) + ], + ), + ), + Positioned( + top: widget.player.bannerRevision != null ? 193.0 : 113.0, + left: 160.0, + child: SizedBox( + width: 270, + child: RichText( + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.white), + children: [ + TextSpan(text: timestamp(widget.player.registrationTime), style: const TextStyle(color: Colors.grey)), + if (widget.player.country != null) TextSpan(text: " • ${t.countries[widget.player.country]}") + ] + ) + ), + ) + ), + Positioned( + top: widget.player.bannerRevision != null ? 126.0 : 46.0, + right: 16.0, + child: RichText( + textAlign: TextAlign.end, + text: TextSpan( + style: const TextStyle(fontFamily: "Eurostile Round", color: Colors.white), + children: [ + TextSpan(text: "Level ${(widget.player.level.isNegative || widget.player.level.isNaN) ? "---" : intf.format(widget.player.level.floor())}", style: TextStyle(decoration: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted, color: (widget.player.level.isNegative || widget.player.level.isNaN) ? Colors.grey : Colors.white), recognizer: (widget.player.level.isNegative || widget.player.level.isNaN) ? null : TapGestureRecognizer()?..onTap = (){ + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text("${t.stats.level.full} ${intf.format(widget.player.level.floor())}", textAlign: TextAlign.center), + content: SingleChildScrollView( + child: ListBody(children: [ + Text( + "${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2).format(widget.player.xp)} ${t.stats.xp.short}", + style: const TextStyle(fontFamily: "Eurostile Round", fontWeight: FontWeight.bold) + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: SfLinearGauge( + minimum: 0, + maximum: 1, + interval: 1, + ranges: [ + LinearGaugeRange(startValue: 0, endValue: widget.player.level - widget.player.level.floor(), color: Colors.cyanAccent), + LinearGaugeRange(startValue: 0, endValue: (widget.player.xp / xpTableScuffed.values.toList()[xpTableID]), color: Colors.redAccent, position: LinearElementPosition.cross) + ], + showTicks: true, + showLabels: false + ), + ), + Text(t.xp.progressToNextLevel(percentage: percentage.format((widget.player.level - widget.player.level.floor())))), + Text(t.xp.progressTowardsGoal(goal: xpTableScuffed.keys.toList()[xpTableID], percentage: percentage.format(widget.player.xp / xpTableScuffed.values.toList()[xpTableID]), left: NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(xpTableScuffed.values.toList()[xpTableID] - widget.player.xp))) + ] + ), + ), + actions: [ + TextButton( + child: Text(t.actions.ok), + onPressed: () {Navigator.of(context).pop();} + ) + ] + ) + ); + }), + const TextSpan(text:"\n"), + TextSpan(text: widget.player.gameTime.isNegative ? "-h --m" : playtime(widget.player.gameTime), style: TextStyle(color: widget.player.gameTime.isNegative ? Colors.grey : Colors.white, decoration: widget.player.gameTime.isNegative ? null : TextDecoration.underline, decorationColor: Colors.white70, decorationStyle: TextDecorationStyle.dotted), recognizer: !widget.player.gameTime.isNegative ? (TapGestureRecognizer()..onTap = (){ + Duration accountAge = DateTime.timestamp().difference(widget.player.registrationTime); + Duration avgGametimeADay = Duration(microseconds: (widget.player.gameTime.inMicroseconds / accountAge.inDays).floor()); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(t.gametime.title, textAlign: TextAlign.center), + content: SingleChildScrollView( + child: Column( + children: [ + RichText(text: TextSpan( + style: TextStyle(fontFamily: "Eurostile Round", color: Colors.white, fontSize: 28), + children: [ + TextSpan(text: "${intf.format(widget.player.gameTime.inHours)}"), + TextSpan(text: ":${nonsecs.format(widget.player.gameTime.inMinutes%60)}:${nonsecs.format(widget.player.gameTime.inSeconds%60)}"), + TextSpan(text: ".${nonsecs3.format(widget.player.gameTime.inMicroseconds%1000000)}", style: TextStyle(fontSize: 14)) + ] + )), + Text(t.gametime.gametimeAday(gametime: playtime(avgGametimeADay))), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + textAlign: TextAlign.center, + t.gametime.breakdown( + years: f4.format(widget.player.gameTime.inSeconds/31536000), + months: f4.format(widget.player.gameTime.inSeconds/2628000), + days: f4.format(widget.player.gameTime.inSeconds/86400), + minutes: f2.format(widget.player.gameTime.inMilliseconds/60000), + seconds: intf.format(widget.player.gameTime.inSeconds) + ) + ), + ) + ] + ), + ), + actions: [ + TextButton( + child: Text(t.actions.ok), + onPressed: () {Navigator.of(context).pop();} + ) + ] + ) + ); + }) : null), + const TextSpan(text:"\n"), + TextSpan(text: widget.player.gamesWon > -1 ? intf.format(widget.player.gamesWon) : "---", style: TextStyle(color: widget.player.gamesWon > -1 ? Colors.white : Colors.grey)), + TextSpan(text: "/${widget.player.gamesPlayed > -1 ? intf.format(widget.player.gamesPlayed) : "---"}", style: const TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)), + ] + ) + ) + ) + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: AnimatedBuilder( + animation: _addToTrackAnim, + builder: (context, child) { + double firstButtonPosition = 0+(_addToTrackAnim.value as double)*25; + double secondButtonPosition = -25+(_addToTrackAnim.value as double)*25; + double firstButtonOpacity = 1-(_addToTrackAnim.value as double)*2; + double secondButtonOpacity = _addToTrackAnim.value*2-1; + return ElevatedButton.icon( + onPressed: (){ + _addToTrackAnimController.value == 1 ? teto.deletePlayerToTrack(widget.player.userId) : teto.addPlayerToTrack(widget.player); + _addToTrackAnim.isCompleted ? _addToTrackAnimController.reverse() : _addToTrackAnimController.forward(); + }, + icon: _addToTrackAnim.value < 0.5 ? Opacity( + opacity: min(1, firstButtonOpacity), + child: Transform.translate( + offset: Offset(0, _addToTrackAnim.status == AnimationStatus.forward ? firstButtonPosition*4 : firstButtonPosition), + child: Transform.rotate( + angle:_addToTrackAnim.status == AnimationStatus.forward ? (_addToTrackAnim.value as double)*2 : 0, + child: const Icon(Icons.person_add), + ), + ), + ) : Container( + transform: Matrix4.translationValues(secondButtonPosition*5, -secondButtonPosition*25, 0), + child: Opacity( + opacity: max(0, min(1, secondButtonOpacity)), + child: Transform.rotate( + angle:_addToTrackAnim.status == AnimationStatus.reverse ? (1-_addToTrackAnim.value as double)*-20 : 0, + child: const Icon(Icons.person_remove) + ) + ) + ), + label: _addToTrackAnim.value < 0.5 ? Container( + transform: Matrix4.translationValues(0, firstButtonPosition, 0), + child: Opacity( + opacity: max(min(1, firstButtonOpacity), 0), + child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.forward ? t.settingsDestination.done : t.track) + ) + ) : Container( + transform: Matrix4.translationValues(0, secondButtonPosition, 0), + child: Opacity( + opacity: max(0, min(1, secondButtonOpacity)), + child: Text(_addToTrackAnimController.isAnimating && _addToTrackAnim.status == AnimationStatus.reverse ? t.settingsDestination.done : t.stopTracking) + ) + ), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomLeft: Radius.circular(12.0)))))); + }, + )), + Expanded( + child: ElevatedButton.icon( + onPressed: (){ + Navigator.push(context, MaterialPageRoute( + builder: (context) => CompareView(widget.player), + ), + ); + }, + icon: const Icon(Icons.balance), + label: Text(t.compare), + style: const ButtonStyle(shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.only(bottomRight: Radius.circular(12.0))))) + ) + ) + ], + ) + ], + ), + ); + }); + } +} diff --git a/lib/widgets/vs_graphs.dart b/lib/widgets/vs_graphs.dart index 0b78adc..04b49ed 100644 --- a/lib/widgets/vs_graphs.dart +++ b/lib/widgets/vs_graphs.dart @@ -1,7 +1,9 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.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/widgets/graphs.dart' show MyRadarChart; -import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; class VsGraphs extends StatelessWidget{ @@ -43,33 +45,25 @@ class VsGraphs extends StatelessWidget{ getTitle: (index, angle) { switch (index) { case 0: - return RadarChartTitle( - text: 'APM', - angle: angle, - positionPercentageOffset: 0.05 - ); + return RadarChartTitle(text: t.stats.apm.short, angle: angle, positionPercentageOffset: 0.05); case 1: - return RadarChartTitle( - text: 'PPS', - angle: angle, - positionPercentageOffset: 0.05 - ); + return RadarChartTitle(text: t.stats.pps.short, angle: angle, positionPercentageOffset: 0.05 ); case 2: - return RadarChartTitle(text: 'VS', angle: angle, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.vs.short, angle: angle, positionPercentageOffset: 0.05); case 3: - return RadarChartTitle(text: 'APP', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.app.short, angle: angle + 180, positionPercentageOffset: 0.05); case 4: - return RadarChartTitle(text: 'DS/S', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.dss.short, angle: angle + 180, positionPercentageOffset: 0.05); case 5: - return RadarChartTitle(text: 'DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.dsp.short, angle: angle + 180, positionPercentageOffset: 0.05); case 6: - return RadarChartTitle(text: 'APP+DS/P', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.appdsp.short, angle: angle + 180, positionPercentageOffset: 0.05); case 7: - return RadarChartTitle(text: 'VS/APM', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.vsapm.short, angle: angle + 180, positionPercentageOffset: 0.05); case 8: - return RadarChartTitle(text: 'Cheese', angle: angle, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.cheese.short, angle: angle, positionPercentageOffset: 0.05); case 9: - return RadarChartTitle(text: 'Gb Eff.', angle: angle, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.gbe.short, angle: angle, positionPercentageOffset: 0.05); default: return const RadarChartTitle(text: ''); } @@ -148,13 +142,13 @@ class VsGraphs extends StatelessWidget{ getTitle: (index, angle) { switch (index) { case 0: - return RadarChartTitle(text: 'Opener',angle: angle, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.opener.short,angle: angle, positionPercentageOffset: 0.05); case 1: - return RadarChartTitle(text: 'Stride', angle: angle, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.stride.short, angle: angle, positionPercentageOffset: 0.05); case 2: - return RadarChartTitle(text: 'Inf Ds', angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.infds.short, angle: angle + 180, positionPercentageOffset: 0.05); case 3: - return RadarChartTitle(text: 'Plonk', angle: angle, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.plonk.short, angle: angle, positionPercentageOffset: 0.05); default: return const RadarChartTitle(text: ''); } @@ -225,13 +219,13 @@ class VsGraphs extends StatelessWidget{ getTitle: (index, angle) { switch (index) { case 0: - return RadarChartTitle(text: t.graphs.attack, angle: 0, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.graphs.attack, angle: 0, positionPercentageOffset: 0.05); case 1: - return RadarChartTitle(text: t.graphs.speed, angle: 0, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.graphs.speed, angle: 0, positionPercentageOffset: 0.05); case 2: - return RadarChartTitle(text: t.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.graphs.defense, angle: angle + 180, positionPercentageOffset: 0.05); case 3: - return RadarChartTitle(text: t.graphs.cheese, angle: 0, positionPercentageOffset: 0.05); + return RadarChartTitle(text: t.stats.graphs.cheese, angle: 0, positionPercentageOffset: 0.05); default: return const RadarChartTitle(text: ''); } diff --git a/lib/widgets/zenith_thingy.dart b/lib/widgets/zenith_thingy.dart index a5a43f3..152d95b 100644 --- a/lib/widgets/zenith_thingy.dart +++ b/lib/widgets/zenith_thingy.dart @@ -1,283 +1,186 @@ import 'package:flutter/material.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/data_objects/record_extras.dart'; +import 'package:tetra_stats/data_objects/record_single.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'; -import 'package:tetra_stats/utils/relative_timestamps.dart'; -import 'package:tetra_stats/utils/text_shadow.dart'; -import 'package:tetra_stats/widgets/finesse_thingy.dart'; -import 'package:tetra_stats/widgets/gauget_num.dart'; -import 'package:tetra_stats/widgets/graphs.dart'; -import 'package:tetra_stats/widgets/lineclears_thingy.dart'; -import 'package:tetra_stats/widgets/stat_sell_num.dart'; +import 'package:tetra_stats/widgets/gauget_thingy.dart'; import 'package:tetra_stats/widgets/text_timestamp.dart'; -class ZenithThingy extends StatefulWidget{ - final RecordSingle? record; - final bool switchable; - final bool initEXvalue; - final RecordSingle? recordEX; - final Function? parentZenithToggle; +class ZenithThingy extends StatelessWidget{ + final RecordSingle? zenith; + final bool old; + final double width; - const ZenithThingy({super.key, this.record, this.recordEX, this.switchable = true, this.parentZenithToggle, this.initEXvalue = false}); + const ZenithThingy({super.key, required this.zenith, this.old = false, this.width = double.infinity}); - @override - State createState() => _ZenithThingyState(); -} - -class _ZenithThingyState extends State { - late RecordSingle? record; - bool ex = false; - - @override - void initState(){ - ex = widget.initEXvalue; - - super.initState(); - if (widget.switchable){ - record = (ex ? widget.recordEX : widget.record); - }else{ - record = widget.record; - ex = widget.record!.gamemode == "zenithex"; - } + List secondColumn(TextStyle style){ + return [ + TableRow(children: [ + Text(intf.format(zenith!.stats.kills), textAlign: TextAlign.right, style: style), + Text(" ${t.stats.kos.short}", style: style) + ]), + TableRow(children: [ + Text(zenith!.stats.topBtB.toString(), textAlign: TextAlign.right, style: style), + Text(" ${t.stats.b2b.short}", style: style) + ]), + TableRow(children: [ + Text(zenith!.stats.garbage.maxspike_nomult.toString(), textAlign: TextAlign.right, style: style), + Text(" ${t.stats.spike}", style: style) + ]), + if (width <= 600) TableRow(children: [ + Text(f2.format(zenith!.stats.zenith!.peakrank), textAlign: TextAlign.right, style: style), + Text(" ${t.stats.peakClimbSpeed.short}", style: style), + ]) + ]; } + List noRecordSecondColumn(TextStyle style){ + return [ + TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: style), + Text(" ${t.stats.kos.short}", style: style) + ]), + TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: style), + Text(" ${t.stats.b2b.short}", style: style) + ]), + TableRow(children: [ + Text("---", textAlign: TextAlign.right, style: style), + Text(" ${t.stats.spike}", style: style) + ]), + if (width <= 600) TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: style), + Text(" ${t.stats.peakClimbSpeed.short}", style: style), + ]) + ]; + } + @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints){ - bool bigScreen = constraints.maxWidth > 768; - if (record == null) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: "--- m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.grey), - ), - ), - TextButton(onPressed: (){ - if (ex){ - ex = false; - }else{ - ex = true; - } - setState(() { - if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); - record = ex ? widget.recordEX : widget.record; - }); - }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), - ], - ), - ); - } - return Padding(padding: const EdgeInsets.only(top: 8.0), + TextStyle tableTextStyle = TextStyle(fontSize: width > 768.0 ? 21 : 18); + TextStyle tableTextStyleMuted = TextStyle(fontSize: width > 768.0 ? 21 : 18, color: Colors.grey); + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), child: Column( children: [ - Text("${t.quickPlay}${ex ? " ${t.expert}" : ""}", style: const TextStyle(height: 0.1, fontFamily: "Eurostile Round Extended", fontSize: 18)), - RichText(text: TextSpan( - text: "${f2.format(record!.stats.zenith!.altitude)} m", - style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 36 : 32, fontWeight: FontWeight.w500, color: Colors.white), - ), - ), - if ((record!.extras as ZenithExtras).mods.isNotEmpty) RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.white), - children: [ - TextSpan(text: "${t.withMods}: "), - for (String mod in (record!.extras as ZenithExtras).mods) TextSpan(text: "${mod.toUpperCase()} "), - ] - ), - ), - RichText( - text: TextSpan( - text: "", - style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), - children: [ - if (record!.rank != -1) TextSpan(text: "№ ${intf.format(record!.rank)}", style: TextStyle(color: getColorOfRank(record!.rank))), - if (record!.rank != -1) const TextSpan(text: " • "), - if (record!.countryRank != -1) TextSpan(text: "№ ${intf.format(record!.countryRank)} local", style: TextStyle(color: getColorOfRank(record!.countryRank))), - if (record!.countryRank != -1) const TextSpan(text: " • "), - TextSpan(text: timestamp(widget.record!.timestamp)), - ] - ), - ), - if (widget.switchable) TextButton(onPressed: (){ - if (ex){ - ex = false; - }else{ - ex = true; - } - setState(() { - if (widget.parentZenithToggle != null) widget.parentZenithToggle!(); - record = ex ? widget.recordEX : widget.record; - }); - }, child: Text(ex ? "Switch to normal" : "Switch to Expert")), - Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 20, + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - StatCellNum(playerStat: record!.aggregateStats.apm, playerStatLabel: t.statCellNum.apm, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.aggregateStats.pps, playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: false), - StatCellNum(playerStat: record!.aggregateStats.vs, playerStatLabel: t.statCellNum.vs, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true, smallDecimal: true), - StatCellNum(playerStat: record!.stats.kills, playerStatLabel: "KO's", isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.stats.cps, playerStatLabel: "Climb speed\n(Peak: ${f2.format(record!.stats.zenith!.peakrank)})", fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true), - StatCellNum(playerStat: record!.stats.topBtB, playerStatLabel: "Top B2B\nchain", isScreenBig: bigScreen, higherIsBetter: true) - ], - ), - FinesseThingy(record?.stats.finesse, record?.stats.finessePercentage), - LineclearsThingy(record!.stats.clears, record!.stats.lines, record!.stats.holds, record!.stats.tSpins), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: 300, - child: Column( + Column( mainAxisSize: MainAxisSize.min, children: [ - Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - const Text("T", style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 65, - height: 1.2, - )), - const Positioned(left: 25, top: 20, child: Text("otal time", style: TextStyle(fontFamily: "Eurostile Round Extended"))), - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text(getMoreNormalTime(record!.stats.finalTime), style: const TextStyle( - shadows: textShadow, - fontFamily: "Eurostile Round Extended", - fontSize: 36, - fontWeight: FontWeight.w500, - color: Colors.white - )), - ) - ], + RichText( + text: TextSpan( + text: zenith != null ? "${f2.format(zenith!.stats.zenith!.altitude)} m" : "--- m", + style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: (zenith != null && !old) ? Colors.white : Colors.grey), + ), ), - Table( - columnWidths: const { - 0: FixedColumnWidth(36) - }, - children: [ - const TableRow( - children: [ - Text("Floor"), - Text("Split", textAlign: TextAlign.right), - Text("Total", textAlign: TextAlign.right), - ] - ), - for (int i = 0; i < record!.stats.zenith!.splits.length; i++) TableRow( - children: [ - Text((i+1).toString()), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]-(i-1 != -1 ? record!.stats.zenith!.splits[i-1] : Duration.zero)) : "--:--.---", textAlign: TextAlign.right), - Text(record!.stats.zenith!.splits[i] != Duration.zero ? getMoreNormalTime(record!.stats.zenith!.splits[i]) : "--:--.---", textAlign: TextAlign.right), - ] - ) - ], + if (zenith != null) RichText( + text: TextSpan( + text: "", + style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, color: Colors.grey), + children: [ + if (zenith!.rank != -1) TextSpan(text: "№ ${intf.format(zenith!.rank)}", style: TextStyle(color: getColorOfRank(zenith!.rank))), + if (zenith!.rank != -1) const TextSpan(text: " • "), + if (zenith!.countryRank != -1) TextSpan(text: "№ ${intf.format(zenith!.countryRank)} local", style: TextStyle(color: getColorOfRank(zenith!.countryRank))), + if (zenith!.countryRank != -1) TextSpan(text: width > 400.0 ? " • " : "\n"), + TextSpan(text: timestamp(zenith!.timestamp)), + ] + ), ), ], ), - ), - ), - Column( - children: [ - Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 35, - crossAxisAlignment: WrapCrossAlignment.start, - //clipBehavior: Clip.hardEdge, - children: [ - GaugetNum(playerStat: record!.aggregateStats.nerdStats.app, playerStatLabel: t.statCellNum.app, higherIsBetter: true, minimum: 0, maximum: 1, ranges: [ - GaugeRange(startValue: 0, endValue: 0.2, color: Colors.red), - GaugeRange(startValue: 0.2, endValue: 0.4, color: Colors.yellow), - GaugeRange(startValue: 0.4, endValue: 0.6, color: Colors.green), - GaugeRange(startValue: 0.6, endValue: 0.8, color: Colors.blue), - GaugeRange(startValue: 0.8, endValue: 1, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.appDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.app}") - ]), - GaugetNum(playerStat: record!.aggregateStats.nerdStats.vsapm, playerStatLabel: "VS / APM", higherIsBetter: true, minimum: 1.8, maximum: 2.4, ranges: [ - GaugeRange(startValue: 1.8, endValue: 2.0, color: Colors.green), - GaugeRange(startValue: 2.0, endValue: 2.2, color: Colors.blue), - GaugeRange(startValue: 2.2, endValue: 2.4, color: Colors.purple), - ], alertWidgets: [ - Text(t.statCellNum.vsapmDescription), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.vsapm}") - ]) - ]), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.center, - spacing: 25, - crossAxisAlignment: WrapCrossAlignment.start, - //clipBehavior: Clip.hardEdge, - children: [ - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dss, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dss, - alertWidgets: [Text(t.statCellNum.dssDescription), - Text("${t.formula}: (VS / 100) - (APM / 60)"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dss}"),], - okText: t.popupActions.ok, - higherIsBetter: true,), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.dsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.dsp, - alertWidgets: [Text(t.statCellNum.dspDescription), - Text("${t.formula}: DS/S / PPS"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.dsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.appdsp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.appdsp, - alertWidgets: [Text(t.statCellNum.appdspDescription), - Text("${t.formula}: APP + DS/P"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.appdsp}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.cheese, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.cheese, - alertWidgets: [Text(t.statCellNum.cheeseDescription), - Text("${t.formula}: (DS/P * 150) + ((VS/APM - 2) * 50) + (0.6 - APP) * 125"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.cheese}"),], - okText: t.popupActions.ok, - higherIsBetter: false), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, - alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: APP * DS/P * 2"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.gbe}"),], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.nyaapp, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.nyaapp, - alertWidgets: [Text(t.statCellNum.nyaappDescription), - Text("${t.formula}: APP - 5 * tan(radians((Cheese Index / -30) + 1))"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.nyaapp}")], - okText: t.popupActions.ok, - higherIsBetter: true), - StatCellNum(playerStat: record!.aggregateStats.nerdStats.area, isScreenBig: bigScreen, fractionDigits: 1, playerStatLabel: t.statCellNum.area, - alertWidgets: [Text(t.statCellNum.areaDescription), - Text("${t.formula}: APM * 1 + PPS * 45 + VS * 0.444 + APP * 185 + DS/S * 175 + DS/P * 450 + Garbage Effi * 315"), - Text("${t.exactValue}: ${record!.aggregateStats.nerdStats.area}"),], - okText: t.popupActions.ok, - higherIsBetter: true) - ]), - ) + if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty && width > 600.0) Container(width: 16.0), + if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty && width > 600.0) for (String mod in (zenith!.extras as ZenithExtras).mods) Image.asset("res/icons/${mod}.png", height: 64.0) ], ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Graphs(record!.aggregateStats.apm, record!.aggregateStats.pps, record!.aggregateStats.vs, record!.aggregateStats.nerdStats, record!.aggregateStats.playstyle), + if (zenith != null && (zenith!.extras as ZenithExtras).mods.isNotEmpty && width <= 600.0) Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (String mod in (zenith!.extras as ZenithExtras).mods) Image.asset("res/icons/${mod}.png", height: 32.0) + ], + ), + if (zenith != null) Row( + children: [ + Expanded( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text(f2.format(zenith!.aggregateStats.apm), textAlign: TextAlign.right, style: tableTextStyle), + Text(" ${t.stats.apm.short}", style: tableTextStyle), + ]), + TableRow(children: [ + Text(f2.format(zenith!.aggregateStats.pps), textAlign: TextAlign.right, style: tableTextStyle), + Text(" ${t.stats.pps.short}", style: tableTextStyle), + ]), + TableRow(children: [ + Text(f2.format(zenith!.aggregateStats.vs), textAlign: TextAlign.right, style: tableTextStyle), + Text(" ${t.stats.vs.short}", style: tableTextStyle), + ]), + if (width <= 600) TableRow(children: [ + Text(f2.format(zenith!.stats.cps), textAlign: TextAlign.right, style: tableTextStyle), + Text(" ${t.stats.climbSpeed.short}", style: tableTextStyle), + ]), + if (width <= 400) ...secondColumn(tableTextStyle).reversed + ], + ), + ), + ), + if (width > 600) GaugetThingy(value: zenith!.stats.cps, min: 0, max: 12, tickInterval: 3, label: t.stats.climbSpeed.gaugetTitle, subString: "${t.stats.peak}: ${f2.format(zenith!.stats.zenith!.peakrank)}", sideSize: 128, fractionDigits: 2, moreIsBetter: true), + if (width > 400) Expanded( + child: Center( + child: Table( + defaultColumnWidth:const IntrinsicColumnWidth(), + children: secondColumn(tableTextStyle), + ), + ), + ) + ], + ) else Row( + children: [ + Expanded( + child: Center( + child: Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: tableTextStyleMuted), + Text(" ${t.stats.apm.short}", style: tableTextStyleMuted), + ]), + TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: tableTextStyleMuted), + Text(" ${t.stats.pps.short}", style: tableTextStyleMuted), + ]), + TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: tableTextStyleMuted), + Text(" ${t.stats.vs.short}", style: tableTextStyleMuted), + ]), + if (width <= 600) TableRow(children: [ + Text("-.--", textAlign: TextAlign.right, style: tableTextStyleMuted), + Text(" ${t.stats.climbSpeed.short}", style: tableTextStyleMuted), + ]) + ], + ), + ), + ), + if (width > 600) GaugetThingy(value: null, min: 0, max: 12, tickInterval: 3, label: t.stats.climbSpeed.gaugetTitle, subString: "${t.stats.peak}: ---", sideSize: 128, fractionDigits: 0, moreIsBetter: true), + if (width > 400) Expanded( + child: Center( + child: Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: noRecordSecondColumn(tableTextStyleMuted), + ), + ), + ) + ], ) - ], - ) - ); - }); + ] + ), + ) + ); } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index e36950e..82d38d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -278,6 +278,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.1" + flutter_layout_grid: + dependency: "direct main" + description: + name: flutter_layout_grid + sha256: "88b4f8484a0874962e27c47733ad256aeb26acc694a9f029edbef771d301885a" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter_lints: dependency: "direct dev" description: @@ -645,6 +653,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" screen_retriever: dependency: transitive description: @@ -922,6 +938,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + transparent_image: + dependency: "direct main" + description: + name: transparent_image + sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f + url: "https://pub.dev" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5b7c6ad..ffffff0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.6.9+35 +version: 2.0.1+42 environment: sdk: '>=3.0.0' @@ -19,6 +19,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + transparent_image: ^2.0.1 cupertino_icons: ^1.0.2 vector_math: any sqflite: ^2.2.8+2 @@ -42,6 +43,7 @@ dependencies: window_manager: ^0.3.7 flutter_markdown: ^0.6.18 flutter_colorpicker: ^1.0.3 + flutter_layout_grid: ^2.0.0 go_router: ^13.0.0 syncfusion_flutter_charts: ^24.2.9 @@ -86,7 +88,7 @@ flutter: - res/avatars/ - res/icons/ - res/tetrio_tl_alpha_ranks/ - - res/tetrio_badges/ + - res/images/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 1f3ba9a..8282d13 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -1,308 +1,27 @@ { "locales(map)": { "en": "English", - "ru": "Russian (Русский)" + "ru-RU": "Russian (Русский)", + "zh-CN": "Simplified Chinese (简体中文)" }, - "tetraLeague": "Tetra League", - "tlRecords": "TL Records", - "history": "History", - "sprint": "40 Lines", - "blitz": "Blitz", - "recent": "Recent", - "recentRuns": "Recent runs", - "blitzScore": "$p points", - "openSPreplay": "Open replay in TETR.IO", - "downloadSPreplay": "Download replay", - "other": "Other", - "distinguishment": "Distinguishment", - "zen": "Zen", - "bio": "Bio", - "news": "News", - "newsParts":{ - "leaderboardStart": "Got ", - "leaderboardMiddle": "on ", - "personalbest": "Got a new PB in ", - "personalbestMiddle": "of ", - "badgeStart": "Obtained a ", - "badgeEnd": "badge", - "rankupStart": "Obtained ", - "rankupMiddle": "${r} rank ", - "rankupEnd": "in Tetra League", - "tetoSupporter": "TETR.IO supporter", - "supporterStart": "Become a ", - "supporterGiftStart": "Received the gift of ", - "unknownNews": "Unknown news of type ${type}" + "gamemodes(map)":{ + "league": "Tetra League", + "zenith": "Quick Play", + "zenithex": "Quick Play Expert", + "40l": "40 Lines", + "blitz": "Blitz", + "5mblast": "5,000,000 Blast", + "zen": "Zen" }, - "openSearch": "Search player", - "closeSearch": "Close search", - "searchHint": "Nickname, ID or Discord userID (with \"ds:\" prefix)", - "refresh": "Refresh", - "fetchAndsaveTLHistory": "Get player history", - "fetchAndSaveOldTLmatches": "Get Tetra League matches history", - "fetchAndsaveTLHistoryResult": "${number} states was found", - "fetchAndSaveOldTLmatchesResult": "${number} matches was found", - "showStoredData": "Show stored data", - "statsCalc": "Stats Calculator", - "settings": "Settings", - "track": "Track", - "stopTracking": "Stop\ntracking", - "becameTracked": "Added to tracking list!", - "compare": "Compare", - "stoppedBeingTracked": "Removed from tracking list!", - "tlLeaderboard": "Tetra League leaderboard", - "noRecords": "No records", - "noOldRecords": { - "zero": "No records", - "one": "Only $n record", - "two": "Only $n records", - "few": "Only $n records", - "many": "Only $n records", - "other": "Only $n records" - }, - "noRecord": "No record", - "botRecord": "Bots are not allowed to set records", - "anonRecord": "Guests are not allowed to set records", - "notEnoughData": "Not enough data", - "noHistorySaved": "No history saved", - "pseudoTooltipHeaderInit": "Hover over point", - "pseudoTooltipFooterInit": "to see detailed data", - "obtainDate": "Obtained ${date}", - "fetchDate": "Fetched ${date}", - "exactGametime": "Exact gametime", - "bigRedBanned": "BANNED", - "normalBanned": "Banned", - "bigRedBadStanding": "BAD STANDING", - "copiedToClipboard": "Copied to clipboard!", - "playerRoleAccount": " account ", - "wasFromBeginning": "that was from very beginning", - "created": "created", - "botCreatedBy": "by", - "notSupporter": "Not a supporter", - "assignedManualy": "That badge was assigned manualy by TETR.IO admins", - "supporter": "Supporter tier ${tier}", - "comparingWith": "Data from ${newDate} comparing with ${oldDate}", - "top": "Top", - "topRank": "Top rank", - "verdictGeneral": "$n $verdict than $rank rank average", - "verdictBetter": "better", - "verdictWorse": "worse", - "smooth": "Smooth", - "postSeason": "Off-season", - "seasonStarts": "Season starts in:", - "nanow": "Not avaliable for now...", - "seasonEnds": "Season ends in ${countdown}", - "seasonEnded": "Season has ended", - "gamesUntilRanked": "${left} games until being ranked", - "numOfVictories": "~${wins} victories", - "promotionOnNextWin": "Promotion on next win", - "numOfdefeats": "~${losses} defeats", - "demotionOnNextLoss": "Demotion on next loss", - "nerdStats": "Nerd Stats", - "playersYouTrack": "Players you track", - "formula": "Formula", - "exactValue": "Exact value", - "neverPlayedTL": "That user never played Tetra League", - "botTL": "Bots are not allowed to play Tetra League", - "anonTL": "Guests are not allowed to play Tetra League", - "quickPlay": "Quick Play", - "expert": "Expert", - "withMods": "With mods", - "withModsPlural":{ - "zero": "with $n mods", - "one": "with $n mod", - "two": "with $n mods", - "few": "with $n mods", - "many": "with $n mods", - "other": "with $n mods" - }, - "exportDB": "Export local database", - "exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.", - "desktopExportAlertTitle": "Desktop export", - "desktopExportText": "It seems like you using this app on desktop. Check your documents folder, you should find \"TetraStats.db\". Copy it somewhere", - "androidExportAlertTitle": "Android export", - "androidExportText": "Exported.\n${exportedDB}", - "importDB": "Import local database", - "importDBDescription": "Restore your backup. Notice that already stored database will be overwritten.", - "importWrongFileType": "Wrong file type", - "importCancelled": "Operation was cancelled", - "importSuccess": "Import successful", - "yourID": "Your TETR.IO account", - "yourIDAlertTitle": "Your nickname in TETR.IO", - "yourIDText": "When app loads, it will retrieve data for this account", - "language": "Language", - "updateInBackground": "Update stats in the background", - "updateInBackgroundDescription": "While Tetra Stats is running, it can update stats of the current player when cache expires", - "customization": "Customization", - "customizationDescription": "Change appearance of different things in Tetra Stats UI", - "oskKagari": "Osk Kagari gimmick", - "oskKagariDescription": "If on, osk's rank on main view will be rendered as :kagari:", - "AccentColor": "Accent color", - "AccentColorDescription": "Almost all interactive UI elements highlighted with this color", - "timestamps": "Timestamps", - "timestampsDescription": "You can choose, in which way timestamps shows time", - "timestampsAbsoluteGMT": "Absolute (GMT)", - "timestampsAbsoluteLocalTime": "Absolute (Your timezone)", - "timestampsRelative": "Relative", - "rating": "Main representation of rating", - "ratingDescription": "TR is not linear, while Glicko does not have boundaries and percentile is volatile", - "ratingLBposition": "LB position", - "sheetbotGraphs": "Sheetbot-like behavior for radar graphs", - "sheetbotGraphsDescription": "If on, points on the graphs can appear on the opposite half of the graph if value is negative", - "lbStats": "Show leaderboard based stats", - "lbStatsDescription": "That will impact on loading times, but will allow you to see position on LB by stats and comparison with average values", - "aboutApp": "About app", - "aboutAppText": "${appName} (${packageName}) Version ${version} Build ${buildNumber}\n\nDeveloped by dan63047\nFormulas provided by kerrmunism\nHistory provided by p1nkl0bst3r\nTETR.IO replay grabber API by szy", - "stateViewTitle": "${nickname} account on ${date}", - "statesViewTitle": "${number} states of ${nickname} account", - "matchesViewTitle": "${nickname} TL matches", - "statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно", - "stateRemoved": "${date} state was removed from database!", - "matchRemoved": "${date} match was removed from database!", - "viewAllMatches": "View all matches", - "trackedPlayersViewTitle": "Stored data", - "trackedPlayersZeroEntrys": "Empty list. Press \"Track\" button in previous view to add current player here", - "trackedPlayersOneEntry": "There is only one player", - "trackedPlayersManyEntrys": "There are ${numberOfPlayers} players", - "trackedPlayersEntry": "${nickname}: ${numberOfStates} states", - "trackedPlayersDescription": "From ${firstStateDate} until ${lastStateDate}", - "trackedPlayersStatesDeleted": "${nickname} states was removed from database!", - "duplicatedFix": "Remove duplicated TL mathces", - "compressDB": "Compress DB", - "SpaceSaved": "Space saved: ${size}", - "averageXrank": "Average ${rankLetter} rank", - "vs": "vs", - "inTLmatch": "in TL match", - "downloadReplay": "Download .ttrm replay", - "openReplay": "Open replay in TETR.IO", - "replaySaved": "Replay saved to ${path}", - "match": "Match", - "timeWeightedmatch": "Match (time-weighted)", - "roundNumber": "Round $n", - "statsFor": "Stats for", - "numberOfRounds": "Number of rounds", - "matchLength": "Match Length", - "roundLength": "Round Length", - "matchStats": "Match stats", - "timeWeightedmatchStats": "Time-weighted match stats", - "replayIssue": "Can't process replay", - "matchIsTooOld": "Replay is not available", - "winner": "Winner", - "registred": "Registred", - "playedTL": "Played Tetra League", - "winChance": "Win Chance", - "byGlicko": "By Glicko", - "byEstTR": "By Est. TR", - "compareViewNoValues": "Please, enter username, user ID, APM-PPS-VS values (divider doesn't matter, only order matter) or $avgR (where R is rank) to both fields", - "compareViewWrongValue": "Falied to assign ${value}", - "mostRecentOne": "Most recent one", - "yes": "Yes", - "no": "No", - "daysLater": "days later", - "dayseBefore": "days before", - "fromBeginning": "From beginning", - "calc": "Calc", - "calcViewNoValues": "Enter values to calculate the stats", - "rankAveragesViewTitle": "Ranks cutoffs", - "sprintAndBlitsViewTitle": "40 lines and Blitz averages", - "sprintAndBlitsRelevance": "Relevance: ${date}", - "rank": "Rank", - "averages": "Averages", - "lbViewZeroEntrys": "Empty list", - "lbViewOneEntry": "There is only one player", - "lbViewManyEntrys": "There are ${numberOfPlayers}", - "everyoneAverages": "Values for leaderboard", - "sortBy": "Sort by", - "reversed": "Reversed", - "country": "Country", - "rankAverages": "Values for $rank rank", - "players":{ - "zero": "$n players", - "one": "$n player", - "two": "$n players", - "few": "$n players", - "many": "$n players", - "other": "$n players" - }, - "games": { - "zero": "$n games", - "one": "$n game", - "two": "$n games", - "few": "$n games", - "many": "$n games", - "other": "$n games" - }, - "gamesPlayed": "$games played", - "chart": "Chart", - "entries": "Entries", - "minimums": "Minimums", - "maximums": "Maximums", - "lowestValues": "Lowest Values", - "averageValues": "Average Values", - "highestValues": "Highest Values", - "forPlayer": "for player $username", - "currentAxis": "$axis axis:", - "p1nkl0bst3rAlert": "That data was retrived from third party API maintained by p1nkl0bst3r", - "notForWeb": "Function is not available for web version", - "graphs": { - "attack": "Attack", - "speed": "Speed", - "defense": "Defense", - "cheese": "Cheese" - }, - "statCellNum":{ - "xpLevel": "XP Level", - "xpProgress": "Progress to next level", - "xpFrom0ToLevel": "Progress from 0 XP to level $n", - "xpLeft": "XP left", - "hoursPlayed": "Hours\nPlayed", - "onlineGames": "Online\nGames", - "gamesWon": "Games\nWon", - "totalGames": "Total Games Played", - "totalWon": "Total Games Won", - "friends": "Friends", - "apm": "Attack\nPer Minute", - "vs": "Versus\nScore", - "recordLB": "Leaderboard placement", - "lbp": "Leaderboard\nplacement", - "lbpShort": "№ in LB", - "lbpc": "Country LB\nplacement", - "lbpcShort": "№ in local LB", - "gamesPlayed": "Games\nplayed", - "gamesWonTL": "Games\nWon", - "winrate": "Winrate", - "level": "Level", - "score": "Score", - "spp": "Score\nPer Piece", - "pieces": "Pieces\nPlaced", - "pps": "Pieces\nPer Second", - "finesseFaults": "Finesse\nFaults", - "finessePercentage": "Finesse\nPercentage", - "keys": "Key\nPresses", - "kpp": "KP Per\nPiece", - "kps": "KP Per\nSecond", - "tr": "Tetra Rating", - "rd": "Rating Deviation", - "app": "Attack Per Piece", - "appDescription": "(Abbreviated as APP) Main efficiency metric. Tells how many attack you producing per piece", - "vsapmDescription": "Basically, tells how much and how efficient you using garbage in your attacks", - "dss": "Downstack\nPer Second", - "dssDescription": "(Abbreviated as DS/S) Downstack per Second measures how many garbage lines you clear in a second.", - "dsp": "Downstack\nPer Piece", - "dspDescription": "(Abbreviated as DS/P) Downstack per Piece measures how many garbage lines you clear per piece.", - "appdsp": "APP + DS/P", - "appdspDescription": "Just a sum of Attack per Piece and Downstack per Piece.", - "cheese": "Cheese\nIndex", - "cheeseDescription": "(Abbreviated as Cheese) Cheese Index is an approximation how much clean / cheese garbage player sends. Lower = more clean. Higher = more cheese.\nInvented by kerrmunism", - "gbe": "Garbage\nEfficiency", - "gbeDescription": "(Abbreviated as Gb Eff.) Garbage Efficiency measures how well player uses their garbage. Higher = better or they use their garbage more. Lower = they mostly send their garbage back at cheese or rarely clear garbage.\nInvented by Zepheniah and Dragonboy.", - "nyaapp": "Weighted\nAPP", - "nyaappDescription": "(Abbreviated as wAPP) Essentially, a measure of your ability to send cheese while still maintaining a high APP.\nInvented by Wertj.", - "area": "Area", - "areaDescription": "How much space your shape takes up on the graph, if you exclude the cheese and vs/apm sections", - "estOfTR": "Estimated TR", - "estOfTRShort": "Est. TR", - "accOfEst": "Accuracy", - "accOfEstShort": "Acc." + "destinations": { + "home": "Home", + "graphs": "Graphs", + "leaderboards": "Leaderboards", + "cutoffs": "Cutoffs", + "calc": "Calculator", + "info": "Info Center", + "data": "Saved Data", + "settings": "Settings" }, "playerRole(map)": { "user": "User", @@ -314,40 +33,110 @@ "halfmod": "Community moderator", "anon": "Anonymous" }, - "numOfGameActions":{ - "pc": "All Clears", - "hold": "Holds", - "inputs": { - "zero": "$n key presses", - "one": "$n key press", - "two": "$n key presses", - "few": "$n key presses", - "many": "$n key presses", - "other": "$n key presses" - }, - "tspinsTotal": { - "zero": "$n T-spins total", - "one": "$n T-spin total", - "two": "$n T-spins total", - "few": "$n T-spins total", - "many": "$n T-spins total", - "other": "$n T-spins total" - }, - "lineClears": { - "zero": "$n lines cleared", - "one": "$n line cleared", - "two": "$n lines cleared", - "few": "$n lines cleared", - "many": "$n lines cleared", - "other": "$n lines cleared" - } + "goBackButton": "Go Back", + "nanow": "Not avaliable for now...", + "seasonEnds": "Season ends in ${countdown}", + "seasonEnded": "Season has ended", + "overallPB": "Overall PB: $pb m", + "gamesUntilRanked": "${left} games until being ranked", + "numOfVictories": "~${wins} victories", + "promotionOnNextWin": "Promotion on next win", + "numOfdefeats": "~${losses} defeats", + "demotionOnNextLoss": "Demotion on next loss", + "records": "Records", + "nerdStats": "Nerd Stats", + "playstyles": "Playstyles", + "horoscopes": "Horoscopes", + "relatedAchievements": "Related Achievements", + "season": "Season", + "smooth": "Smooth", + "dateAndTime": "Date & Time", + "TLfullLBnote": "Heavy, but allows you to sort players by their stats and filter them by ranks", + "rank": "Rank", + "verdictGeneral": "$n $verdict of $rank rank avg", + "verdictBetter": "ahead", + "verdictWorse": "behind", + "localStanding": "local", + "xp": { + "title": "XP Level", + "progressToNextLevel": "Progress to next level: $percentage", + "progressTowardsGoal": "Progress from 0 XP to level $goal: $percentage ($left XP left)" }, - "popupActions":{ - "cancel": "Cancel", - "submit": "Submit", - "ok": "OK" + "gametime":{ + "title": "Exact gametime", + "gametimeAday": "$gametime a day in average", + "breakdown": "It's $years years,\nor $months months,\nor $days days,\nor $minutes minutes\nor $seconds seconds" }, - "errors":{ + "track": "Track", + "stopTracking": "Stop tracking", + "supporter": "Supporter tier ${tier}", + "comparingWith": "Data from ${newDate} comparing with ${oldDate}", + "compare": "Compare", + "comparison": "Comparison", + "enterUsername": "Enter username or \\$avgX (where X is rank)", + "general": "General", + "badges": "Badges", + "obtainDate": "Obtained ${date}", + "assignedManualy": "That badge was assigned manualy by TETR.IO admins", + "distinguishment": "Distinguishment", + "banned": "Banned", + "bannedSubtext": "Bans are placed when TETR.IO rules or terms of service are broken", + "badStanding": "Bad standing", + "badStandingSubtext": "One or more recent bans on record", + "botAccount": "Bot account", + "botAccountSubtext": "Operated by $botMaintainers", + "copiedToClipboard": "Copied to clipboard!", + "bio": "Bio", + "news": "News", + "matchResult": { + "victory": "Victory", + "defeat": "Defeat", + "tie": "Tie", + "dqvictory": "Opponent was DQ'ed", + "dqdefeat": "Disqualified", + "nocontest": "No Contest", + "nullified": "Nullified" + }, + "distinguishments": { + "noHeader": "Header is missing", + "noFooter": "Footer is missing", + "twc": "TETR.IO World Champion", + "twcYear": "$year TETR.IO World Championship" + }, + "newsEntries":{ + "leaderboard(rich)": "Got № $rank in $gametype", + "personalbest(rich)": "Got a new PB in $gametype of $pb", + "badge(rich)": "Obtained a $badge badge", + "rankup(rich)": "Obtained $rank in Tetra League", + "supporter(rich)": "Became a ${s(TETR.IO supporter)}", + "supporter_gift(rich)": "Received the gift of ${s(TETR.IO supporter)}", + "unknown(rich)": "Unknown news of type $type" + }, + "rankupMiddle": "${r} rank", + "copyUserID": "Click to copy user ID", + "searchHint": "Username or ID", + "navMenu": "Navigation menu", + "navMenuTooltip": "Open navigation menu", + "refresh": "Refresh data", + "searchButton": "Search", + "trackedPlayers": "Tracked Players", + "standing": "Standing", + "previousSeasons": "Previous Seasons", + "recent": "Recent", + "top": "Top", + "noRecord": "No record", + "sprintAndBlitsRelevance": "Relevance: ${date}", + "snackBarMessages":{ + "stateRemoved": "${date} state was removed from database!", + "matchRemoved": "${date} match was removed from database!", + "notForWeb": "Function is not available for web version", + "importSuccess": "Import successful", + "importCancelled": "Import was cancelled" + }, + "errors": { + "noRecords": "No records", + "notEnoughData": "Not enough data", + "noHistorySaved": "No history saved", "connection": "Some issue with connection: ${code} ${message}", "noSuchUser": "No such user", "noSuchUserSub": "Either you mistyped something, or the account no longer exists", @@ -375,8 +164,509 @@ "replayExpired": "Replay expired and not available anymore", "replayRejected": "Third party API blocked your IP address" }, + "actions":{ + "cancel": "Cancel", + "submit": "Submit", + "ok": "OK", + "apply": "Apply", + "refresh": "Refresh" + }, + "graphsDestination": { + "fetchAndsaveTLHistory": "Get player history", + "fetchAndSaveOldTLmatches": "Get Tetra League matches history", + "fetchAndsaveTLHistoryResult": "${number} states was found", + "fetchAndSaveOldTLmatchesResult": "${number} matches was found", + "gamesPlayed": "$games played", + "dateAndTime": "Date & Time", + "filterModaleTitle": "Filter ranks on graph" + }, + "filterModale":{ + "all": "All" + }, + "cutoffsDestination": { + "title": "Tetra League State", + "relevance": "as of $timestamp", + "actual": "Actual", + "target": "Target", + "cutoffTR": "Cutoff TR", + "targetTR": "Target TR", + "state": "State", + "advanced": "Advanced", + "players": "Players ($n)", + "moreInfo": "More Info", + "NumberOne": "№ 1 is $tr TR", + "inflated": "Inflated on $tr TR", + "notInflated": "Not inflated", + "deflated": "Deflated on $tr TR", + "notDeflated": "Not deflated", + "wellDotDotDot": "Well...", + "fromPlace": "from № $n", + "viewButton": "View" + }, + "rankView":{ + "rankTitle": "$rank rank data", + "everyoneTitle": "Entire leaderboard", + "trRange": "TR Range", + "supposedToBe": "Supposed to be", + "gap": "$value gap", + "trGap": "$value TR gap", + "deflationGap": "Deflation gap", + "inflationGap": "Inflation gap", + "LBposRange": "LB pos range", + "overpopulated": "Overpopulated by a $players", + "underpopulated": "Underpopulated by a $players", + "PlayersEqualSupposedToBe": "cute", + "avgStats": "Average Stats", + "avgForRank": "Average for $rank rank", + "avgNerdStats": "Average Nerd Stats", + "minimums": "Minimums", + "maximums": "Maximums" + }, + "stateView": { + "title": "State from $date" + }, + "tlMatchView": { + "match": "Match", + "vs": "vs", + "winner": "Winner", + "roundNumber": "Round $n", + "statsFor": "Stats for", + "numberOfRounds": "Number of rounds", + "matchLength": "Match Length", + "roundLength": "Round Length", + "matchStats": "Match stats", + "downloadReplay": "Download .ttrm replay", + "openReplay": "Open replay in TETR.IO" + }, + "calcDestination": { + "placeholders": "Enter your $stat", + "tip": "Enter values and press \"Calc\" to see Nerd Stats for them", + "statsCalcButton": "Calc", + "damageCalcTip": "Click on the actions on the left to add them here", + "actions": "Actions", + "results": "Results", + "rules": "Rules", + "noSpinClears": "No Spin Clears", + "spins": "Spins", + "miniSpins": "Mini spins", + "noLineclear": "No lineclear (Break Combo)", + "custom": "Custom", + "multiplier": "Multiplier", + "pcDamage": "Perfect Clear Damage", + "comboTable": "Combo Table", + "b2bChaining": "Back-To-Back Chaining", + "surgeStartAtB2B": "Starts at B2B", + "surgeStartAmount": "Start amount", + "totalDamage": "Total damage", + "lineclears": "Lineclears", + "combo": "Combo", + "surge": "Surge", + "pcs": "PCs" + }, + "infoDestination": { + "title": "Information Center", + "sprintAndBlitzAverages": "40 Lines & Blitz Averages", + "sprintAndBlitzAveragesDescription": "Since calculating 40 Lines & Blitz averages is tedious process, it gets updated only once in a while. Click on the title of this card to see the full 40 Lines & Blitz averages table", + "tetraStatsWiki": "Tetra Stats Wiki", + "tetraStatsWikiDescription": "Find more information about Tetra Stats functions and statictic, that it provides", + "about": "About Tetra Stats", + "aboutDescription": "Developed by dan63\n" + }, + "leaderboardsDestination": { + "title": "Leaderboards", + "tl": "Tetra League (Current Season)", + "fullTL": "Tetra League (Current Season, full one)", + "ar": "Acievement Points" + }, + "savedDataDestination": { + "title": "Saved Data", + "tip": "Select nickname on the left to see data assosiated with it", + "seasonTLstates": "S$s TL States", + "TLrecords": "TL Records" + }, + "settingsDestination": { + "title": "Settings", + "general": "General", + "customization": "Customization", + "database": "Local database", + "checking": "Checking...", + "enterToSubmit": "Press Enter to submit", + "account": "Your account in TETR.IO", + "accountDescription": "Stats of that player will be loaded initially right after launching this app. By default it loads my (dan63) stats. To change that, enter your nickname here.", + "done": "Done!", + "noSuchAccount": "No such account", + "language": "Language", + "languageDescription": "Tetra Stats was translated on $languages. By default, app will pick your system one or English, if locale of your system isn't avaliable.", + "languages(plural)": { + "zero": "zero languages", + "one": "$n language", + "two": "$n languages", + "few": "$n languages", + "many": "$n languages", + "other": "$n languages" + }, + "updateInTheBackground": "Update data in the background", + "updateInTheBackgroundDescription": "If on, Tetra Stats will attempt to retrieve new info once cache expires. Usually that happen every 5 minutes", + "compareStats": "Compare TL stats with rank averages", + "compareStatsDescription": "If on, Tetra Stats will provide additional metrics, which allow you to compare yourself with average player on your rank. The way you'll see it — stats will be highlited with corresponding color, hover over them with cursor for more info.", + "showPosition": "Show position on leaderboard by stats", + "showPositionDescription": "This can take some time (and traffic) to load, but will allow you to see your position on the leaderboard, sorted by a stat", + "accentColor": "Accent color", + "accentColorDescription": "That color is seen across this app and usually highlites interactive UI elements.", + "accentColorModale": "Pick an accent color", + "timestamps": "Timestamps format", + "timestampsDescriptionPart1": "You can choose, in which way timestamps shows time. By default, they show time in GMT timezone, formatted according to chosen locale, example: $d.", + "timestampsDescriptionPart2": "There is also:\n• Locale formatted in your timezone: $y\n• Relative timestamp: $r", + "timestampsAbsoluteGMT": "Absolute (GMT)", + "timestampsAbsoluteLocalTime": "Absolute (Your timezone)", + "timestampsRelative": "Relative", + "sheetbotLikeGraphs": "Sheetbot-like behavior for radar graphs", + "sheetbotLikeGraphsDescription": "Altough it was considered by me, that the way graphs work in SheetBot is not very correct, some people were confused to see, that -0.5 stride dosen't look the way it looks on SheetBot graph. Hence, he we are: if this toggle is on, points on the graphs can appear on the opposite half of the graph if value is negative.", + "oskKagariGimmick": "Osk-Kagari gimmick", + "oskKagariGimmickDescription": "If on, instead of osk's rank, :kagari: will be rendered.", + "bytesOfDataStored": "of data stored", + "TLrecordsSaved": "Tetra League records saved", + "TLplayerstatesSaved": "Tetra League playerstates saved", + "fixButton": "Fix", + "compressButton": "Compress", + "exportDB": "Export local database", + "desktopExportAlertTitle": "Desktop export", + "desktopExportText": "It seems like you using this app on desktop. Check your documents folder, you should find \"TetraStats.db\". Copy it somewhere", + "androidExportAlertTitle": "Android export", + "androidExportText": "Exported.\n${exportedDB}", + "importDB": "Import local database", + "importDBDescription": "Restore your backup. Notice that already stored database will be overwritten.", + "importWrongFileType": "Wrong file type" + }, + "homeNavigation": { + "overview": "Overview", + "standing": "Standing", + "seasons": "Seasons", + "mathces": "Matches", + "pb": "PB", + "normal": "Normal", + "expert": "Expert", + "expertRecords": "Ex Records" + }, + "graphsNavigation": { + "history": "Player History", + "league": "League State", + "cutoffs": "Cutoffs History" + }, + "calcNavigation": { + "stats": "Stats Calculator", + "damage": "Damage Calculator" + }, + "firstTimeView": { + "welcome": "Welcome to Tetra Stats", + "description": "Service, that allows you to keep track of various statistics for TETR.IO", + "nicknameQuestion": "What's your nickname?", + "inpuntHint": "Type it here... (3-16 symbols)", + "emptyInputError": "Can't submit an empty string", + "niceToSeeYou": "Nice to see you, $n", + "letsTakeALook": "Let's take a look at your stats...", + "skip": "Skip" + }, + "aboutView": { + "title": "About Tetra Stats", + "about": "Tetra Stats is a service, that works with TETR.IO Tetra Channel API, providing data from it and calculating some addtitional metrics, based on this data. Service allows user to track their progress in Tetra League with \"Track\" function, which records every Tetra League change into local database (not automatically, you have to visit service from time to time), so these changes could be looked through graphs.\n\nBeanserver blaster is a part of a Tetra Stats, that decoupled into a serverside script. It provides full Tetra League leaderboard, allowing Tetra Stats to sort leaderboard by any metric and build scatter chart, that allows user to analyse Tetra League trends. It also provides history of Tetra League ranks cutoffs, which can be viewed by user via graph as well.\n\nThere is a plans to add replay analysis and tournaments history, so stay tuned!\n\nService is not associated with TETR.IO or osk in any capacity.", + "appVersion": "App Version", + "build": "Build $build", + "GHrepo": "GitHub Repository", + "submitAnIssue": "Submit an issue", + "credits": "Credits", + "authorAndDeveloper": "Autor & developer", + "providedFormulas": "Provided formulas", + "providedS1history": "Provided S1 history", + "inoue": "Inoue (replay grabber)", + "zhCNlocale": "Simplfied Chinese locale", + "supportHim": "Support him!" + }, + "stats": { + "registrationDate": "Registration Date", + "gametime": "Time Played", + "ogp": "Online Games Played", + "ogw": "Online Games Won", + "followers": "Followers", + "xp": { + "short": "XP", + "full": "Experience Points" + }, + "tr": { + "short": "TR", + "full": "Tetra Rating" + }, + "glicko": { + "short": "Glicko", + "full": "Glicko" + }, + "rd":{ + "short": "RD", + "full": "Rating Deviation" + }, + "glixare": { + "short": "GXE", + "full": "GLIXARE" + }, + "s1tr": { + "short": "S1 TR", + "full": "Season 1 like TR" + }, + "gp":{ + "short": "GP", + "full": "Games Played" + }, + "gw":{ + "short": "GW", + "full": "Games Won" + }, + "winrate":{ + "short": "WR%", + "full": "Win Rate" + }, + "apm":{ + "short": "APM", + "full": "Attack Per Minute" + }, + "pps":{ + "short": "PPS", + "full": "Pieces Per Second" + }, + "vs":{ + "short": "VS", + "full": "Versus Score" + }, + "app":{ + "short": "APP", + "full": "Attack Per Piece" + }, + "vsapm":{ + "short": "VS/APM", + "full": "VS / APM" + }, + "dss":{ + "short": "DS/S", + "full": "Downstack Per Second" + }, + "dsp":{ + "short": "DS/P", + "full": "Downstack Per Piece" + }, + "appdsp": { + "short": "APP+DSP", + "full": "APP + DSP" + }, + "cheese":{ + "short": "Cheese", + "full": "Cheese Index" + }, + "gbe":{ + "short": "GbE", + "full": "Garbage Efficiency" + }, + "nyaapp":{ + "short": "wAPP", + "full": "Weighted APP" + }, + "area":{ + "short": "Area", + "full": "Area" + }, + "etr":{ + "short": "eTR", + "full": "Estimated TR" + }, + "etracc":{ + "short": "±eTR", + "full": "Accuracy of Estimated TR" + }, + "opener":{ + "short": "Opener", + "full": "Opener" + }, + "plonk":{ + "short": "Plonk", + "full": "Plonk" + }, + "stride":{ + "short": "Stride", + "full": "Stride" + }, + "infds":{ + "short": "Inf. DS", + "full": "Infinite Downstack" + }, + "altitude": { + "short": "m", + "full": "Altitude" + }, + "climbSpeed": { + "short": "CSP", + "full": "Climb Speed", + "gaugetTitle": "Climb\nSpeed" + }, + "peakClimbSpeed": { + "short": "Peak CSP", + "full": "Peak Climb Speed", + "gaugetTitle": "Peak" + }, + "kos": { + "short": "KO's", + "full": "Knockouts" + }, + "b2b":{ + "short": "B2B", + "full": "Back-To-Back" + }, + "finesse":{ + "short": "F", + "full": "Finesse", + "widgetTitle": "inesse" + }, + "finesseFaults":{ + "short": "FF", + "full": "Finesse Faults" + }, + "totalTime":{ + "short": "Time", + "full": "Total Time", + "widgetTitle": "otal Time" + }, + "level": { + "short": "Lvl", + "full": "Level" + }, + "pieces": { + "short": "P", + "full": "Pieces" + }, + "spp": { + "short": "SPP", + "full": "Score Per Piece" + }, + "kp": { + "short": "KP", + "full": "Key presses" + }, + "kpp": { + "short": "KPP", + "full": "Key presses Per Piece" + }, + "kps": { + "short": "KPS", + "full": "Key presses Per Second" + }, + "blitzScore": "$p points", + "levelUpRequirement": "Level up requirement: $p", + "piecesTotal": "Total pieces placed", + "piecesWithPerfectFinesse": "Placed with perfect finesse", + "score": "Score", + "lines": "Lines", + "linesShort": "L", + "pcs": "Perfect Clears", + "holds": "Holds", + "spike": "Top Spike", + "top": "Top $percentage", + "topRank": "Top rank: $rank", + "floor": "Floor", + "split": "Split", + "total": "Total", + "sent": "Sent", + "received": "Received", + "placement": "Placement", + "peak": "Peak", + "qpWithMods(plural)": { + "one": "With 1 mod", + "two": "With $n mods", + "few": "With $n mods", + "many": "With $n mods", + "other": "With $n mods" + }, + "inputs(plural)": { + "zero": "$n key presses", + "one": "$n key press", + "two": "$n key presses", + "few": "$n key presses", + "many": "$n key presses", + "other": "$n key presses" + }, + "tspinsTotal(plural)": { + "zero": "$n T-spins total", + "one": "$n T-spin total", + "two": "$n T-spins total", + "few": "$n T-spins total", + "many": "$n T-spins total", + "other": "$n T-spins total" + }, + "linesCleared(plural)": { + "zero": "$n lines cleared", + "one": "$n line cleared", + "two": "$n lines cleared", + "few": "$n lines cleared", + "many": "$n lines cleared", + "other": "$n lines cleared" + }, + "graphs": { + "attack": "Attack", + "speed": "Speed", + "defense": "Defense", + "cheese": "Cheese" + }, + "players(plural)":{ + "zero": "$n players", + "one": "$n player", + "two": "$n players", + "few": "$n players", + "many": "$n players", + "other": "$n players" + }, + "games(plural)": { + "zero": "$n games", + "one": "$n game", + "two": "$n games", + "few": "$n games", + "many": "$n games", + "other": "$n games" + }, + "lineClear": { + "single": "Single", + "double": "Double", + "triple": "Triple", + "quad": "Quad", + "penta": "Penta", + "hexa": "Hexa", + "hepta": "Hepta", + "octa": "Octa", + "ennea": "Ennea", + "deca": "Deca", + "hendeca": "Hendeca", + "dodeca": "Dodeca", + "triadeca": "Triadeca", + "tessaradeca": "Tessaradeca", + "pentedeca": "Pentedeca", + "hexadeca": "Hexadeca", + "heptadeca": "Heptadeca", + "octadeca": "Octadeca", + "enneadeca": "Enneadeca", + "eicosa": "Eicosa", + "kagaris": "Kagaris" + }, + "lineClears": { + "zero": "Zeros", + "single": "Singles", + "double": "Doubles", + "triple": "Triples", + "quad": "Quads", + "penta": "Pentas" + }, + "mini": "Mini", + "tSpin": "T-spin", + "tSpins": "T-spins", + "spin": "Spin", + "spins": "Spins" + }, "countries(map)": { - "": "Not selected", + "": "Worldwide", "AF": "Afghanistan", "AX": "\u00c5land Islands", @@ -662,4 +952,4 @@ "XX": "Unknown", "XM": "The Moon" } - } \ No newline at end of file + } diff --git a/res/i18n/strings_ru-RU.i18n.json b/res/i18n/strings_ru-RU.i18n.json new file mode 100644 index 0000000..1acb9aa --- /dev/null +++ b/res/i18n/strings_ru-RU.i18n.json @@ -0,0 +1,929 @@ +{ + "locales(map)": { + "en": "Английский (English)", + "ru-RU": "Русский", + "zh-CN": "Упрощенный Китайский (简体中文)" + }, + "gamemodes(map)": { + "league": "Тетра Лига", + "zenith": "Quick Play", + "zenithex": "Quick Play Expert", + "40l": "40 линий", + "blitz": "Блиц", + "5mblast": "5 000 000 бласт", + "zen": "Дзен" + }, + "destinations": { + "home": "Дом", + "graphs": "Графики", + "leaderboards": "Таблицы лидеров", + "cutoffs": "Требования рангов", + "calc": "Калькулятор", + "info": "Инфо-центр", + "data": "Сохранённые данные", + "settings": "Настройки" + }, + "playerRole(map)": { + "user": "Пользователь", + "banned": "Заблокированный пользователь", + "bot": "Бот", + "sysop": "Системный оператор", + "admin": "Администратор", + "mod": "Модератор", + "halfmod": "Модератор сообщества", + "anon": "Аноним" + }, + "goBackButton": "Назад", + "nanow": "Сейчас недоступно...", + "seasonEnds": "Сезон закончится через ${countdown}", + "seasonEnded": "Сезон завершён", + "overallPB": "Абсолютный рекорд: $pb м", + "gamesUntilRanked": "${left} матчей до получения рейтинга", + "numOfVictories": "~${wins} побед", + "promotionOnNextWin": "Повышение после следующей победы", + "numOfdefeats": "~${losses} поражений", + "demotionOnNextLoss": "Понижение после следующего поражения", + "records": "Записи", + "nerdStats": "Для Задротов", + "playstyles": "Стили игры", + "horoscopes": "Гороскопы", + "relatedAchievements": "Достижения режима", + "season": "Сезон", + "smooth": "Сглаживание", + "dateAndTime": "Дата и время", + "TLfullLBnote": "Большая, но позволяет сортировать игроков по их статам и фильтровать их по рангам", + "rank": "Ранг", + "verdictGeneral": "На $n $verdict среднего $rank", + "verdictBetter": "впереди", + "verdictWorse": "позади", + "localStanding": "по стране", + "xp": { + "title": "Уровень Опыта", + "progressToNextLevel": "Прогресс до следующего уровня: $percentage", + "progressTowardsGoal": "Прогресс с 0 XP до уровня $goal: $percentage ($left XP осталось)" + }, + "gametime": { + "title": "Времени проведено в игре", + "gametimeAday": "$gametime в день в среднем", + "breakdown": "Это $years лет,\nили $months месяцев,\nили $days дней,\nили $minutes минут\nили $seconds секунд" + }, + "track": "Отслеживать", + "stopTracking": "Не отслеживать", + "supporter": "Спонсор ${tier}-го уровня", + "comparingWith": "Данные от ${newDate} в сравнении с данными от ${oldDate}", + "compare": "Сравнить", + "comparison": "Сравнение", + "enterUsername": "Введите ник или \\$avgX (где X это ранг)", + "general": "Основное", + "badges": "Значки", + "obtainDate": "Получен ${date}", + "assignedManualy": "Этот значок был присвоен вручную администрацией TETR.IO", + "distinguishment": "Заслуга", + "banned": "Забанен", + "bannedSubtext": "Баны выдаются в случаях нарушений правил TETR.IO", + "badStanding": "Плохая репутация", + "badStandingSubtext": "Один или более банов на счету", + "botAccount": "Бот аккаунт", + "botAccountSubtext": "Операторы: $botMaintainers", + "copiedToClipboard": "Скопировано в буфер обмена!", + "bio": "Биография", + "news": "Новости", + "matchResult": { + "victory": "Победа", + "defeat": "Поражение", + "tie": "Ничья", + "dqvictory": "Оппонент дисквалифицирован", + "dqdefeat": "Дисквалифицирован", + "nocontest": "Без согласия", + "nullified": "Отменен" + }, + "distinguishments": { + "noHeader": "Заголовок отсутствует", + "noFooter": "Подзаголовок отсуствует", + "twc": "Чемпион мира TETR.IO", + "twcYear": "Чемпионат мира по TETR.IO $year года" + }, + "newsEntries": { + "leaderboard(rich)": "Заработал №$rank в режиме $gametype", + "personalbest(rich)": "Новый ЛР в $gametype: $pb", + "badge(rich)": "Заработал значок $badge", + "rankup(rich)": "Заработал $rank в Тетра Лиге", + "supporter(rich)": "Стал ${s(спонсором TETR.IO)}", + "supporter_gift(rich)": "Получил ${s(спонсорку TETR.IO)} в качестве подарка", + "unknown(rich)": "Неизвестная новость типа $type" + }, + "rankupMiddle": "${r} ранг", + "copyUserID": "Нажмите, чтобы скопировать ID", + "searchHint": "Никнейм или ID", + "navMenu": "Меню навигации", + "navMenuTooltip": "Открыть меню навигации", + "refresh": "Обновить данные", + "searchButton": "Искать", + "trackedPlayers": "Отслеживаемые игроки", + "standing": "Положение", + "previousSeasons": "Предыдущие сезоны", + "recent": "Недавние", + "top": "Топ", + "noRecord": "Нет записи", + "sprintAndBlitsRelevance": "Актуальность: ${date}", + "snackBarMessages": { + "stateRemoved": "Состояние от ${date} удалено из базы данных!", + "matchRemoved": "Матч от ${date} удален из базы данных!", + "notForWeb": "Функция недоступна для веб-версии", + "importSuccess": "Импорт выполнен успешно", + "importCancelled": "Импорт был отменен" + }, + "errors": { + "noRecords": "Нет записей", + "notEnoughData": "Недостаточно данных", + "noHistorySaved": "Нет сохраненной истории", + "connection": "Проблема с подключением: ${code} ${message}", + "noSuchUser": "Нет такого пользователя", + "noSuchUserSub": "Либо вы опечатались, либо аккаунт больше не существует", + "discordNotAssigned": "К данному Discord ID не привязан аккаунт", + "discordNotAssignedSub": "Убедитесь, что указан правильный ID", + "history": "История этого игрока отсутствует", + "actionSuggestion": "Возможно, вы хотите", + "p1nkl0bst3rTLmatches": "Матчей Тетра Лиги не найдено", + "clientException": "Нет подключения к Интернету", + "forbidden": "Ваш IP-адрес заблокирован", + "forbiddenSub": "Если вы используете VPN или Proxy, выключите его. Если это не помогает, свяжитесь с $nickname", + "tooManyRequests": "Слишком много запросов", + "tooManyRequestsSub": "Повторите попытку позже", + "internal": "Что-то случилось на стороне tetr.io", + "internalSub": "Скорее всего, osk уже в курсе", + "internalWebVersion": "Что-то случилось на стороне TETR.IO (или у oskware_bridge, я хз)", + "internalWebVersionSub": "Если на osk status page нет сообщений о проблемах, дайте знать dan63047", + "oskwareBridge": "Что-то случилось с oskware_bridge", + "oskwareBridgeSub": "Дайте знать dan63047", + "p1nkl0bst3rForbidden": "Сторонний API заблокировал ваш IP-адрес", + "p1nkl0bst3rTooManyRequests": "Слишком много запросов к стороннему API. Попробуйте позже", + "p1nkl0bst3rinternal": "Что-то случилось на стороне p1nkl0bst3r", + "p1nkl0bst3rinternalWebVersion": "Что-то случилось на стороне p1nkl0bst3r (или на oskware_bridge, я хз)", + "replayAlreadySaved": "Повтор уже был сохранен", + "replayExpired": "Повтор истек и больше не доступен", + "replayRejected": "Сторонний API заблокировал ваш IP-адрес" + }, + "actions": { + "cancel": "Отменить", + "submit": "Подтвердить", + "ok": "ОК", + "apply": "Применить", + "refresh": "Обновить" + }, + "graphsDestination": { + "fetchAndsaveTLHistory": "Получить историю игрока", + "fetchAndSaveOldTLmatches": "Получить историю матчей Тетра Лиги", + "fetchAndsaveTLHistoryResult": "${number} состояний было найдено", + "fetchAndSaveOldTLmatchesResult": "${number} матчей было найдено", + "gamesPlayed": "$games сыграно", + "dateAndTime": "Дата и время", + "filterModaleTitle": "Фильтровать график по рангам" + }, + "filterModale": { + "all": "Все" + }, + "cutoffsDestination": { + "title": "Состояние Тетра Лиги", + "relevance": "на момент $timestamp", + "actual": "Требование", + "target": "Цель", + "cutoffTR": "Треб. TR", + "targetTR": "Целевой TR", + "state": "Состояние", + "advanced": "Продвинутая", + "players": "Игроков ($n)", + "moreInfo": "Подробнее", + "NumberOne": "№ 1 - $tr TR", + "inflated": "Инфляция - $tr TR", + "notInflated": "Нет инфляции", + "deflated": "Дефляция - $tr TR", + "notDeflated": "Нет дефляции", + "wellDotDotDot": "Ну-у...", + "fromPlace": "от № $n", + "viewButton": "Посмотреть" + }, + "rankView": { + "rankTitle": "Данные $rank ранга", + "everyoneTitle": "Вся таблица", + "trRange": "Диапазон TR", + "supposedToBe": "Должен быть", + "gap": "промежуток в $value", + "trGap": "промежуток в $value TR", + "deflationGap": "Зона дефляции", + "inflationGap": "Зона инфляции", + "LBposRange": "Диапазон по позициям", + "overpopulated": "Переполнен $players", + "underpopulated": "Не хватает $players", + "PlayersEqualSupposedToBe": "лол", + "avgStats": "Средние значения", + "avgForRank": "Среднее для $rank ранга", + "avgNerdStats": "Средние задротские значения", + "minimums": "Минимумы", + "maximums": "Максимумы" + }, + "stateView": { + "title": "Состояние от $date" + }, + "tlMatchView": { + "match": "Матч", + "vs": "против", + "winner": "Победитель", + "roundNumber": "Раунд $n", + "statsFor": "Статистика для", + "numberOfRounds": "Количество раундов", + "matchLength": "Продолжительность матча", + "roundLength": "Продолжительность раунда", + "matchStats": "Статистика матча", + "downloadReplay": "Скачать .ttrm повтор", + "openReplay": "Открыть повтор в TETR.IO" + }, + "calcDestination": { + "placeholders": "Введите ваш $stat", + "tip": "Введите значения и нажмите \"Считать\", чтобы увидеть статистику для задротов", + "statsCalcButton": "Считать", + "damageCalcTip": "Нажмите на действия слева, чтобы добавить их сюда", + "actions": "Действия", + "results": "Результаты", + "rules": "Правила", + "noSpinClears": "Без спинов", + "spins": "Спины", + "miniSpins": "Мини спины", + "noLineclear": "0 линий (сброс комбо)", + "custom": "Custom", + "multiplier": "Множитель", + "pcDamage": "PC урон", + "comboTable": "Таблица комбо", + "b2bChaining": "Таблица комбо", + "surgeStartAtB2B": "Начинается с B2B", + "surgeStartAmount": "Начинается с", + "totalDamage": "Всего урона", + "lineclears": "Lineclears", + "combo": "Комбо", + "surge": "Surge", + "pcs": "PCs" + }, + "infoDestination": { + "title": "Информационный Центр", + "sprintAndBlitzAverages": "Средние значения для 40 линий и блиц", + "sprintAndBlitzAveragesDescription": "Поскольку считать средние значения 40 линий и Блиц неудобно, они обновляется довольно редко. Кликните по названию этой карточки, чтобы увидеть таблицу средних значений 40 линий и Блиц", + "tetraStatsWiki": "Tetra Stats Вики", + "tetraStatsWikiDescription": "Узнайте больше о функциях Tetra Stats и статистике, что он предоставляет", + "about": "О Tetra Stats", + "aboutDescription": "Разработано dan63\n" + }, + "leaderboardsDestination": { + "title": "Таблицы лидеров", + "tl": "Тетра Лига (Текущий сезон)", + "fullTL": "Тетра Лига (Текущий сезон, вся за раз)", + "ar": "Очки достижений" + }, + "savedDataDestination": { + "title": "Сохранённые данные", + "tip": "Выберите никнейм слева, чтобы увидеть данные ассоциированные с ним", + "seasonTLstates": "TL $s сезона", + "TLrecords": "Записи TL" + }, + "settingsDestination": { + "title": "Настройки", + "general": "Общие", + "customization": "Кастомизация", + "database": "Локальная база данных", + "checking": "Проверяем...", + "enterToSubmit": "Enter, чтобы подтвердить", + "account": "Ваш аккаунт в TETR.IO", + "accountDescription": "Статистика этого игрока будет загружена сразу после запуска приложения. По умолчанию программа загружает мою (dan63) статистику. Чтобы изменить это, введите свой ник.", + "done": "Готово!", + "noSuchAccount": "Нет такого аккаунта", + "language": "Язык", + "languageDescription": "Tetra Stats был переведен на $languages. По умолчанию приложение выберет язык системы или Английский, если перевода на язык системы нету.", + "languages(plural)": { + "zero": "ноль языков", + "one": "$n язык", + "two": "$n языка", + "few": "$n языка", + "many": "$n языков", + "other": "$n языков" + }, + "updateInTheBackground": "Обновлять данные в фоновом режиме", + "updateInTheBackgroundDescription": "Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кэш истекает. Обычно это происходит каждые 5 минут", + "compareStats": "Сравнивать статистику со средними значениями ранга", + "compareStatsDescription": "Если включено, Tetra Stats загрузит средние значения и будет сравнивать вас со средними значениями вашего ранга. В результате этого почти каждый пункт статистики обретёт цвет, наводите курсор, что-бы узнать больше.", + "showPosition": "Показывать позиции по статам", + "showPositionDescription": "На загрузку потребуется немного времени (и трафика), но зато вы сможете видеть своё положение в таблице Тетра Лиги, отсортированной по статам", + "accentColor": "Цветовой акцент", + "accentColorDescription": "Этот цвет подчёркивает интерактивные элементы интерфейса.", + "accentColorModale": "Выберите цвет акцента", + "timestamps": "Формат отметок времени", + "timestampsDescriptionPart1": "Вы можете выбрать вид отметок времени. По умолчанию показывается дата и время по Гринвичу, форматированная в соответствии с выбранной локалью. Пример: $d.", + "timestampsDescriptionPart2": "Также можно выбрать:\n• Дата и время в вашем часовом поясе: $y\n• Относительные отметки времени: $r", + "timestampsAbsoluteGMT": "Абсолютные (по Гринвичу)", + "timestampsAbsoluteLocalTime": "Абсолютные (ваша временная зона)", + "timestampsRelative": "Относительные", + "sheetbotLikeGraphs": "Графики-радары как у sheetBot", + "sheetbotLikeGraphsDescription": "Хоть и несмотря на то, что я считаю поведение графиков sheetBot-а не совсем корректным, некоторые пользователи были в замешательстве от того, что -0,5 страйд не выглядит так, как на графике sheetBot-а. Поэтому вот моё решение: если тумблер включен, точки графика могут появляться на противоположенной стороне графика если значение со знаком минус.", + "oskKagariGimmick": "\"Оск Кагари\" прикол", + "oskKagariGimmickDescription": "Если включено, вместо настоящего ранга оска будет рендерится :kagari:.", + "bytesOfDataStored": "данных сохранено", + "TLrecordsSaved": "записей о матчах Тетра Лиги сохранено", + "TLplayerstatesSaved": "состояний Тетра Лиги сохранено", + "fixButton": "Исправить", + "compressButton": "Сжать", + "exportDB": "Экспортировать локальную базу данных", + "desktopExportAlertTitle": "Экспорт на десктопе", + "desktopExportText": "Похоже, вы используете десктопную версию. Проверьте папку \"Документы\", там вы должны найти файл \"TetraStats.db\". Скопируйте его куда-нибудь", + "androidExportAlertTitle": "Экспорт на Android", + "androidExportText": "Экспортировано.\n${exportedDB}", + "importDB": "Импортировать локальную базу данных", + "importDBDescription": "Восстановите свою резеврную копию. Обратите внимание, что текущая база данных будет перезаписана.", + "importWrongFileType": "Неверный тип файла" + }, + "homeNavigation": { + "overview": "Обзор", + "standing": "Положение", + "seasons": "Сезоны", + "mathces": "Матчи", + "pb": "Рекорд", + "normal": "Обычный", + "expert": "Эксперт", + "expertRecords": "Записи EX" + }, + "graphsNavigation": { + "history": "История игрока", + "league": "Состояние Лиги", + "cutoffs": "История рангов" + }, + "calcNavigation": { + "stats": "Калькулятор статистики", + "damage": "Калькулятор урона" + }, + "firstTimeView": { + "welcome": "Добро пожаловать в Tetra Stats", + "description": "Сервис, который позволяет просматривать статистику в TETR.IO", + "nicknameQuestion": "Введите свой ник", + "inpuntHint": "(3-16 символов)", + "emptyInputError": "Строка пуста", + "niceToSeeYou": "Приятно познакомиться, $n", + "letsTakeALook": "Давайте же посмотрим на ваши статы...", + "skip": "Пропустить" + }, + "aboutView": { + "title": "О Tetra Stats", + "about": "Tetra Stats — это сервис, который работает с TETR.IO Tetra Channel API, показывает данные оттуда и считает дополнительную статистику, основанную на этих данных. Сервис позволяет отслеживать прогресс в Тетра Лиге с помощью функции \"Отслеживать\", которая записывает каждое изменение в Лиге в локальную базу данных (не автоматически, вы должны вручную посещать свой профиль), что позволяет потом просматривать изменения с помощью графиков.\n\nBeanserver blaster — серверная часть Tetra Stats. Она собирает полную таблицу игроков Тетра Лиги, благодаря чему сортировать эту таблицу по любой метрике и строить точечную диаграмму, что позволяет анализировать тренды Лиги. Также она предоставляет историю требований рангов, которую тоже можно посмотреть на графике.\n\nВ будущем планируется добавить анализ повторов и историю турниров, так что оставайтесь на связи.\n\nСервис ни коим образом не ассоциируется с TETR.IO или osk.", + "appVersion": "Версия приложения", + "build": "Сборка $build", + "GHrepo": "Репозиторий на GitHub", + "submitAnIssue": "Сообщить об ошибке", + "credits": "Благодарности", + "authorAndDeveloper": "Автор и разработчик", + "providedFormulas": "Предоставил формулы", + "providedS1history": "Предоставляет историю первого сезона лиги", + "inoue": "Inoue (достаёт повторы)", + "zhCNlocale": "Перевёл на упрощённый китайский", + "supportHim": "Поддержите его!" + }, + "stats": { + "registrationDate": "Дата регистрации", + "gametime": "Время в игре", + "ogp": "Онлайн игр", + "ogw": "Онлайн побед", + "followers": "Подписчиков", + "xp": { + "short": "Опыт", + "full": "Очки опыта" + }, + "tr": { + "short": "TR", + "full": "Тетра Рейтинг" + }, + "glicko": { + "short": "Glicko", + "full": "Glicko" + }, + "rd": { + "short": "RD", + "full": "Отклонение Рейтинга" + }, + "glixare": { + "short": "GXE", + "full": "GLIXARE" + }, + "s1tr": { + "short": "S1 TR", + "full": "TR как в первом сезоне" + }, + "gp": { + "short": "GP", + "full": "Матчей" + }, + "gw": { + "short": "GW", + "full": "Побед" + }, + "winrate": { + "short": "WR%", + "full": "Процент побед" + }, + "apm": { + "short": "APM", + "full": "Атаки в Минуту" + }, + "pps": { + "short": "PPS", + "full": "Фигур в Секунду" + }, + "vs": { + "short": "VS", + "full": "Показатель Versus" + }, + "app": { + "short": "APP", + "full": "Атаки на Фигуру" + }, + "vsapm": { + "short": "VS/APM", + "full": "VS / APM" + }, + "dss": { + "short": "DS/S", + "full": "Спуск в секунду" + }, + "dsp": { + "short": "DS/P", + "full": "Спуск на фигуру" + }, + "appdsp": { + "short": "APP+DSP", + "full": "APP + DSP" + }, + "cheese": { + "short": "Cheese", + "full": "Индекс Сыра" + }, + "gbe": { + "short": "GbE", + "full": "Эффективность Мусора" + }, + "nyaapp": { + "short": "wAPP", + "full": "Weighted APP" + }, + "area": { + "short": "Area", + "full": "Area" + }, + "etr": { + "short": "eTR", + "full": "Расчётный TR" + }, + "etracc": { + "short": "±eTR", + "full": "Точность расчёта" + }, + "opener": { + "short": "Opener", + "full": "Опенер" + }, + "plonk": { + "short": "Plonk", + "full": "Плонк" + }, + "stride": { + "short": "Stride", + "full": "Страйд" + }, + "infds": { + "short": "Inf. DS", + "full": "Бесконечный спуск" + }, + "altitude": { + "short": "м", + "full": "Высота" + }, + "climbSpeed": { + "short": "CSP", + "full": "Скорость подъёма", + "gaugetTitle": "Скорость\nПодъёма" + }, + "peakClimbSpeed": { + "short": "Пик CSP", + "full": "Пиковая скорость подъёма", + "gaugetTitle": "Пик" + }, + "kos": { + "short": "KO's", + "full": "Выбил" + }, + "b2b": { + "short": "B2B", + "full": "Back-To-Back" + }, + "finesse": { + "short": "F", + "full": "Техника", + "widgetTitle": "Техника" + }, + "finesseFaults": { + "short": "FF", + "full": "Ошибок техники" + }, + "totalTime": { + "short": "Время", + "full": "Общее время", + "widgetTitle": "Общее время" + }, + "level": { + "short": "Лвл", + "full": "Уровень" + }, + "pieces": { + "short": "P", + "full": "Фигур" + }, + "spp": { + "short": "SPP", + "full": "Очков на Фигуру" + }, + "kp": { + "short": "KP", + "full": "Нажатий клавиш" + }, + "kpp": { + "short": "KPP", + "full": "Нажатий клавиш на Фигуру" + }, + "kps": { + "short": "KPS", + "full": "Нажатий клавиш в Секунду" + }, + "blitzScore": "$p очков", + "levelUpRequirement": "Очков для повышения уровня: $p", + "piecesTotal": "Всего фигур установлено", + "piecesWithPerfectFinesse": "Установлено с идеальной техникой", + "score": "Счёт", + "lines": "Линий", + "linesShort": "L", + "pcs": "Perfect Clears", + "holds": "Holds", + "spike": "Top Spike", + "top": "Топ $percentage", + "topRank": "Топ ранг: $rank", + "floor": "Этаж", + "split": "Сектор", + "total": "Всего", + "sent": "Отправлено", + "received": "Получено", + "placement": "Положение", + "peak": "Пик", + "qpWithMods(plural)": { + "one": "С 1 модом", + "two": "С $n модами", + "few": "С $n модами", + "many": "С $n модами", + "other": "С $n модами" + }, + "inputs(plural)": { + "zero": "$n нажатий клавиш", + "one": "$n нажатие клавиш", + "two": "$n нажатия клавиш", + "few": "$n нажатия клавиш", + "many": "$n нажатий клавиш", + "other": "$n нажатий клавиш" + }, + "tspinsTotal(plural)": { + "zero": "$n T-спинов всего", + "one": "Всего $n T-спин", + "two": "$n T-спина всего", + "few": "$n T-спина всего", + "many": "$n T-спинов всего", + "other": "$n T-спинов всего" + }, + "linesCleared(plural)": { + "zero": "$n линий очищено", + "one": "$n линия очищена", + "two": "$n линии очищено", + "few": "$n линии очищено", + "many": "$n линий очищено", + "other": "$n линий очищено" + }, + "graphs": { + "attack": "Атака", + "speed": "Скорость", + "defense": "Оборона", + "cheese": "Сыр" + }, + "players(plural)": { + "zero": "$n игроков", + "one": "$n игрок", + "two": "$n игрока", + "few": "$n игрока", + "many": "$n игроков", + "other": "$n игроков" + }, + "games(plural)": { + "zero": "$n игр", + "one": "$n игра", + "two": "$n игры", + "few": "$n игры", + "many": "$n игр", + "other": "$n игр" + }, + "lineClear": { + "single": "Single", + "double": "Double", + "triple": "Triple", + "quad": "Quad", + "penta": "Penta", + "hexa": "Hexa", + "hepta": "Hepta", + "octa": "Octa", + "ennea": "Ennea", + "deca": "Deca", + "hendeca": "Hendeca", + "dodeca": "Dodeca", + "triadeca": "Triadeca", + "tessaradeca": "Tessaradeca", + "pentedeca": "Pentedeca", + "hexadeca": "Hexadeca", + "heptadeca": "Heptadeca", + "octadeca": "Octadeca", + "enneadeca": "Enneadeca", + "eicosa": "Eicosa", + "kagaris": "Kagaris" + }, + "lineClears": { + "zero": "Zeros", + "single": "Singles", + "double": "Doubles", + "triple": "Triples", + "quad": "Quads", + "penta": "Pentas" + }, + "mini": "Mini", + "tSpin": "T-spin", + "tSpins": "T-spins", + "spin": "Spin", + "spins": "Spins" + }, + "countries(map)": { + "": "Во всем мире", + "AF": "Афганистан", + "AX": "Аландские острова", + "AL": "Албания", + "DZ": "Алжир", + "AS": "Американское Самоа", + "AD": "Андорра", + "AO": "Ангола", + "AI": "Ангилья", + "AQ": "Антарктида", + "AG": "Антигуа и Барбуда", + "AR": "Аргентина", + "AM": "Армения", + "AW": "Аруба", + "AU": "Австралия", + "AT": "Австрия", + "AZ": "Азербайджан", + "BS": "Багамские острова", + "BH": "Бахрейн", + "BD": "Бангладеш", + "BB": "Барбадос", + "BY": "Беларусь", + "BE": "Бельгия", + "BZ": "Белиз", + "BJ": "Бенин", + "BM": "Бермуды", + "BT": "Бутан", + "BO": "Боливия, Многонациональное Государство", + "BA": "Босния и Герцеговина", + "BW": "Ботсвана", + "BV": "Остров Буве", + "BR": "Бразилия", + "IO": "Британская территория в Индийском океане", + "BN": "Бруней-Даруссалам", + "BG": "Болгария", + "BF": "Буркина-Фасо", + "BI": "Бурунди", + "KH": "Камбоджа", + "CM": "Камерун", + "CA": "Канада", + "CV": "Кабо-Верде", + "BQ": "Карибские Нидерланды", + "KY": "Каймановы острова", + "CF": "Центральноафриканская Республика", + "TD": "Чад", + "CL": "Чили", + "CN": "Китай", + "CX": "Остров Рождества", + "CC": "Кокосовые острова", + "CO": "Колумбия", + "KM": "Коморские острова", + "CG": "Конго", + "CD": "Конго, Демократическая Республика", + "CK": "Острова Кука", + "CR": "Коста-Рика", + "CI": "Берег Слоновой Кости", + "HR": "Хорватия", + "CU": "Куба", + "CW": "Кюрасао", + "CY": "Кипр", + "CZ": "Чешская Республика", + "DK": "Дания", + "DJ": "Джибути", + "DM": "Доминика", + "DO": "Доминиканская Республика", + "EC": "Эквадор", + "EG": "Египет", + "SV": "Сальвадор", + "GB-ENG": "Англия", + "GQ": "Экваториальная Гвинея", + "ER": "Эритрея", + "EE": "Эстония", + "ET": "Эфиопия", + "EU": "Европа", + "FK": "Фолклендские (Мальвинские) острова", + "FO": "Фарерские острова", + "FJ": "Фиджи", + "FI": "Финляндия", + "FR": "Франция", + "GF": "Французская Гвиана", + "PF": "Французская Полинезия", + "TF": "Южные территории Франции", + "GA": "Габон", + "GM": "Гамбия", + "GE": "Грузия", + "DE": "Германия", + "GH": "Гана", + "GI": "Гибралтар", + "GR": "Греция", + "GL": "Гренландия", + "GD": "Гренада", + "GP": "Гваделупа", + "GU": "Гуам", + "GT": "Гватемала", + "GG": "Гернси", + "GN": "Гвинея", + "GW": "Гвинея-Бисау", + "GY": "Гайана", + "HT": "Гаити", + "HM": "Остров Херд и острова Макдональд", + "VA": "Святой Престол (государство-городок Ватикан)", + "HN": "Гондурас", + "HK": "Гонконг", + "HU": "Венгрия", + "IS": "Исландия", + "IN": "Индия", + "ID": "Индонезия", + "IR": "Иран, Исламская Республика", + "IQ": "Ирак", + "IE": "Ирландия", + "IM": "Остров Мэн", + "IL": "Израиль", + "IT": "Италия", + "JM": "Ямайка", + "JP": "Япония", + "JE": "Джерси", + "JO": "Иордания", + "KZ": "Казахстан", + "KE": "Кения", + "KI": "Кирибати", + "KP": "Корея, Народно-Демократическая Республика", + "KR": "Корея, Республика", + "XK": "Косово", + "KW": "Кувейт", + "KG": "Кыргызстан", + "LA": "Лаосская Народно-Демократическая Республика", + "LV": "Латвия", + "LB": "Ливан", + "LS": "Лесото", + "LR": "Либерия", + "LY": "Ливия", + "LI": "Лихтенштейн", + "LT": "Литва", + "LU": "Люксембург", + "MO": "Макао", + "MK": "Македония, бывшая югославская республика", + "MG": "Мадагаскар", + "MW": "Малави", + "MY": "Малайзия", + "MV": "Мальдивы", + "ML": "Мали", + "MT": "Мальта", + "MH": "Маршалловы острова", + "MQ": "Мартиника", + "MR": "Мавритания", + "MU": "Маврикий", + "YT": "Майотта", + "MX": "Мексика", + "FM": "Микронезия, Федеративные Штаты", + "MD": "Молдова, Республика", + "MC": "Монако", + "ME": "Черногория", + "MA": "Марокко", + "MN": "Монголия", + "MS": "Монтсеррат", + "MZ": "Мозамбик", + "MM": "Мьянма", + "NA": "Намибия", + "NR": "Науру", + "NP": "Непал", + "NL": "Нидерланды", + "AN": "Нидерландские Антильские острова", + "NC": "Новая Каледония", + "NZ": "Новая Зеландия", + "NI": "Никарагуа", + "NE": "Нигер", + "NG": "Нигерия", + "NU": "Ниуэ", + "NF": "Остров Норфолк", + "GB-NIR": "Северная Ирландия", + "MP": "Северные Марианские острова", + "NO": "Норвегия", + "OM": "Оман", + "PK": "Пакистан", + "PW": "Палау", + "PS": "Палестина", + "PA": "Панама", + "PG": "Папуа-Новая Гвинея", + "PY": "Парагвай", + "PE": "Перу", + "PH": "Филиппины", + "PN": "Питкэрн", + "PL": "Польша", + "PT": "Португалия", + "PR": "Пуэрто-Рико", + "QA": "Катар", + "RE": "Реюньон", + "RO": "Румыния", + "RU": "Российская Федерация", + "RW": "Руанда", + "BL": "Сен-Бартелеми", + "SH": "Острова Святой Елены, Вознесения и Тристан-да-Кунья", + "KN": "Сент-Китс и Невис", + "LC": "Сент-Люсия", + "MF": "Сен-Мартен", + "PM": "Сен-Пьер и Микелон", + "VC": "Сент-Винсент и Гренадины", + "WS": "Самоа", + "SM": "Сан-Марино", + "ST": "Сан-Томе и Принсипи", + "SA": "Саудовская Аравия", + "GB-SCT": "Шотландия", + "SN": "Сенегал", + "RS": "Сербия", + "SC": "Сейшельские острова", + "SL": "Сьерра-Леоне", + "SG": "Сингапур", + "SX": "Синт-Мартен (голландская часть)", + "SK": "Словакия", + "SI": "Словения", + "SB": "Соломоновы острова", + "SO": "Сомали", + "ZA": "ЮАР", + "GS": "Южная Георгия и Южные Сандвичевы острова", + "SS": "Южный Судан", + "ES": "Испания", + "LK": "Шри-Ланка", + "SD": "Судан", + "SR": "Суринам", + "SJ": "Острова Шпицберген и Ян-Майен", + "SZ": "Свазиленд", + "SE": "Швеция", + "CH": "Швейцария", + "SY": "Сирийская Арабская Республика", + "TW": "Тайвань", + "TJ": "Таджикистан", + "TZ": "Танзания, Объединенная Республика", + "TH": "Таиланд", + "TL": "Тимор-Лешти", + "TG": "Того", + "TK": "Токелау", + "TO": "Тонга", + "TT": "Тринидад и Тобаго", + "TN": "Тунис", + "TR": "Турция", + "TM": "Туркменистан", + "TC": "Острова Теркс и Кайкос", + "TV": "Тувалу", + "UG": "Уганда", + "UA": "Украина", + "AE": "Объединенные Арабские Эмираты", + "GB": "Великобритания", + "US": "Соединенные Штаты", + "UY": "Уругвай", + "UM": "Малые периферийные острова США", + "UZ": "Узбекистан", + "VU": "Вануату", + "VE": "Венесуэла, Боливарианская Республика", + "VN": "Вьетнам", + "VG": "Виргинские острова, Британские", + "VI": "Виргинские острова, США", + "GB-WLS": "Уэльс", + "WF": "Острова Уоллис и Футуна", + "EH": "Западная Сахара", + "YE": "Йемен", + "ZM": "Замбия", + "ZW": "Зимбабве", + "XX": "Неизвестно", + "XM": "Луна" + } +} diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json deleted file mode 100644 index 5f48489..0000000 --- a/res/i18n/strings_ru.i18n.json +++ /dev/null @@ -1,665 +0,0 @@ -{ - "locales(map)": { - "en": "Английский (English)", - "ru": "Русский" - }, - "tetraLeague": "Тетра Лига", - "tlRecords": "Матчи ТЛ", - "history": "История", - "sprint": "40 линий", - "blitz": "Блиц", - "recent": "Недавно", - "recentRuns": "Недавние", - "blitzScore": "$p очков", - "openSPreplay": "Открыть повтор в TETR.IO", - "downloadSPreplay": "Скачать повтор", - "other": "Другое", - "distinguishment": "Заслуга", - "zen": "Дзен", - "bio": "Биография", - "news": "Новости", - "newsParts":{ - "leaderboardStart": "Взял ", - "leaderboardMiddle": "в таблице ", - "personalbest": "Поставил новый ЛР в ", - "personalbestMiddle": "с результатом в ", - "badgeStart": "Заработал значок ", - "badgeEnd": "", - "rankupStart": "Заработал ", - "rankupMiddle": "${r} ранг ", - "rankupEnd": "в Тетра Лиге", - "tetoSupporter": "TETR.IO supporter", - "supporterStart": "Стал обладателем ", - "supporterGiftStart": "Получил подарок в виде ", - "unknownNews": "Неизвестная новость типа ${type}" - }, - "openSearch": "Искать игрока", - "closeSearch": "Закрыть поиск", - "searchHint": "Ник, ID или ID в Discord (с префиксом \"ds:\")", - "refresh": "Обновить", - "fetchAndsaveTLHistory": "Получить историю игрока", - "fetchAndSaveOldTLmatches": "Получить старые матчи Тетра Лиги", - "fetchAndsaveTLHistoryResult": "${number} состояний было найдено", - "fetchAndSaveOldTLmatchesResult": "${number} старых матчей было найдено", - "showStoredData": "Показать сохранённые данные", - "statsCalc": "Калькулятор статистики", - "settings": "Настройки", - "track": "Отслеживать", - "stopTracking": "Перестать\nотслеживать", - "becameTracked": "Добавлен в список отслеживания!", - "stoppedBeingTracked": "Удалён из списка отслеживания!", - "compare": "Сравнить", - "tlLeaderboard": "Рейтинговая таблица", - "noRecords": "Нет записей", - "noOldRecords": { - "zero": "Нет записей", - "one": "Всего один матч", - "two": "Всего $n матча", - "few": "Всего $n матча", - "many": "Всего $n матчей", - "other": "$n матчей" - }, - "noRecord": "Нет рекорда", - "botRecord": "Ботам нельзя ставить рекорды", - "anonRecord": "Гостям нельзя ставить рекорды", - "notEnoughData": "Недостаточно данных", - "noHistorySaved": "Нет сохранённой истории", - "pseudoTooltipHeaderInit": "Наведите курсор на точку", - "pseudoTooltipFooterInit": "чтобы узнать подробности", - "obtainDate": "Получено ${date}", - "fetchDate": "На момент ${date}", - "exactGametime": "Время, проведённое в игре", - "bigRedBanned": "ЗАБАНЕН", - "normalBanned": "Забанен", - "bigRedBadStanding": "ПЛОХАЯ РЕПУТАЦИЯ", - "copiedToClipboard": "Скопировано в буфер обмена!", - "playerRoleAccount": ", аккаунт которого ", - "wasFromBeginning": "существовал с самого начала", - "created": "создан", - "botCreatedBy": "игроком", - "notSupporter": "Нет саппортерки", - "supporter": "Саппортерка ${tier} уровня", - "assignedManualy": "Этот значок был присвоен вручную администрацией TETR.IO", - "comparingWith": "Данные от ${newDate} в сравнении с данными от ${oldDate}", - "top": "Топ", - "topRank": "Топ ранг", - "verdictGeneral": "$verdict среднего $rank ранга на $n", - "verdictBetter": "Лучше", - "verdictWorse": "Хуже", - "smooth": "Гладкий", - "postSeason": "Внесезонье", - "seasonStarts": "Сезон начнётся через:", - "nanow": "Пока недоступно...", - "seasonEnds": "Сезон закончится через ${countdown}", - "seasonEnded": "Сезон закончился", - "gamesUntilRanked": "${left} матчей до получения рейтинга", - "numOfVictories": "~${wins} побед", - "promotionOnNextWin": "Повышение после следующей победы", - "numOfdefeats": "~${losses} поражений", - "demotionOnNextLoss": "Понижение после следующего поражения", - "nerdStats": "Для задротов", - "playersYouTrack": "Отслеживаемые игроки", - "formula": "Формула", - "exactValue": "Точное значение", - "neverPlayedTL": "Этот игрок никогда не играл в Тетра Лигу", - "botTL": "Ботам нельзя играть в Тетра Лигу", - "anonTL": "Гостям нельзя играть в Тетра Лигу", - "quickPlay": "Быстрая Игра", - "expert": "Эксперт", - "withMods": "С модами", - "withModsPlural":{ - "zero": "с $n модами", - "one": "с $n модом", - "two": "с $n модами", - "few": "с $n модами", - "many": "с $n модами", - "other": "с $n модами" - }, - "exportDB": "Экспортировать локальную базу данных", - "exportDBDescription": "Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.", - "desktopExportAlertTitle": "Экспорт на десктопе", - "desktopExportText": "Похоже, вы используете десктопную версию. Проверьте папку \"Документы\", там вы должны найти файл \"TetraStats.db\". Скопируйте его куда-нибудь", - "androidExportAlertTitle": "Экспорт на Android", - "androidExportText": "Экспортировано.\n${exportedDB}", - "importDB": "Импортировать локальную базу данных", - "importDBDescription": "Восстановите свою резеврную копию. Обратите внимание, что текущая база данных будет перезаписана.", - "importWrongFileType": "Неверный тип файла", - "importCancelled": "Операция была отменена", - "importSuccess": "Успешно импортировано", - "yourID": "Ваш аккаунт в TETR.IO", - "yourIDAlertTitle": "Ваш ник в TETR.IO", - "yourIDText": "При запуске приложения оно будет получать статистику этого игрока.", - "language": "Язык (Language)", - "updateInBackground": "Обновлять статистику в фоне", - "updateInBackgroundDescription": "Пока Tetra Stats работает, он может обновлять статистику самостоятельно когда кеш истекает", - "customization": "Кастомизация", - "customizationDescription": "Измените внешний вид пользовательского интерфейса Tetra Stats", - "oskKagari": "\"Оск Кагари\" прикол", - "oskKagariDescription": "Если включено, вместо настоящего ранга оска будет рендерится :kagari:", - "AccentColor": "Цветовой акцент", - "AccentColorDescription": "Почти все интерактивные элементы пользовательского интерфейса окрашены в этот цвет", - "timestamps": "Метки времени", - "timestampsDescription": "Вы можете выбрать, каким образом метки времени показывают время", - "timestampsAbsoluteGMT": "Абсолютные (GMT)", - "timestampsAbsoluteLocalTime": "Абсолютные (Ваш часовой пояс)", - "timestampsRelative": "Относительные", - "rating": "Основное представление рейтинга", - "ratingDescription": "TR нелинеен, тогда как Glicko не имеет границ, а положение в таблице лидеров волатильно", - "ratingLBposition": "Позиция в рейтинге", - "sheetbotGraphs": "Графики-радары как у sheetBot", - "sheetbotGraphsDescription": "Если включено, точки на графике могут появляться на противоположной стороне графика если значение отрицательное", - "lbStats": "Показывать статистику, основанную на рейтинговой таблице", - "lbStatsDescription": "Это повлияет на время загрузки, но позволит видеть положение в рейтинге и сравнение со средними значениями по рангу по каждой стате", - "aboutApp": "О приложении", - "aboutAppText": "${appName} (${packageName}) Версия ${version} Сборка ${buildNumber}\n\nРазработал dan63047\nФормулы предоставил kerrmunism\nИсторию предоставляет p1nkl0bst3r\nВозможность скачивать повторы из TETR.IO предоставляет szy", - "stateViewTitle": "Аккаунт ${nickname} ${date}", - "statesViewTitle": "${number} состояний аккаунта ${nickname}", - "matchesViewTitle": "Матчи аккаунта ${nickname}", - "statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно", - "stateRemoved": "Состояние от ${date} было удалено из локальной базы данных!", - "matchRemoved": "Матч от ${date} был удален из локальной базы данных!", - "viewAllMatches": "Все матчи", - "trackedPlayersViewTitle": "Сохранённые данные", - "trackedPlayersZeroEntrys": "Пустой список. Вернитесь на предыдущий экран и нажмите кнопку \"Отслеживать\", чтобы текущий игрок появился здесь", - "trackedPlayersOneEntry": "В списке только один игрок", - "trackedPlayersManyEntrys": "В списке ${numberOfPlayers} игроков", - "trackedPlayersEntry": "${nickname}: ${numberOfStates} состояний", - "trackedPlayersDescription": "Начиная с ${firstStateDate} и заканчивая ${lastStateDate}", - "trackedPlayersStatesDeleted": "Состояния аккаунта ${nickname} были удалены из локальной базы данных!", - "duplicatedFix": "Удалить дубликаты матчей в Тетра Лиге", - "compressDB": "Сжать базу данных", - "SpaceSaved": "Места освобождено: ${size}", - "averageXrank": "Средний ${rankLetter} ранг", - "vs": "против", - "inTLmatch": "в матче ТЛ", - "downloadReplay": "Скачать .ttrm повтор", - "openReplay": "Открыть повтор в TETR.IO", - "replaySaved": "Повтор сохранён по пути ${path}", - "match": "Матч", - "timeWeightedmatch": "Матч (взвешенная по времени)", - "roundNumber": "Раунд $n", - "statsFor": "Статистика за", - "numberOfRounds": "Количество раундов", - "matchLength": "Продолжительность матча", - "roundLength": "Продолжительность раунда", - "matchStats": "Статистика матча", - "timeWeightedmatchStats": "Взвешенная по времени cтатистика матча", - "replayIssue": "Ошибка обработки повтора", - "matchIsTooOld": "Информация о повторе недоступна", - "winner": "Победитель", - "registred": "Зарегистрирован", - "playedTL": "Играл в Тетра Лигу", - "winChance": "Шансы на победу", - "byGlicko": "По Glicko", - "byEstTR": "По расч. TR", - "compareViewNoValues": "Пожалуйста, введите никнейм, ID, APM-PPS-VS (неважно, какой разделитель, важен порядок) или $avgR (где R это ранг), в оба поля", - "compareViewWrongValue": "Не удалось получить ${value}", - "mostRecentOne": "Самый последний", - "yes": "Да", - "no": "Нет", - "daysLater": "дней позже", - "dayseBefore": "дней раньше", - "fromBeginning": "С начала", - "calc": "Считать", - "calcViewNoValues": "Введите значения, чтобы посчитать статистику", - "rankAveragesViewTitle": "Требования рангов", - "sprintAndBlitsViewTitle": "Средние результаты 40 линий и блица", - "sprintAndBlitsRelevance": "Актуальность: ${date}", - "rank": "Ранг", - "averages": "Средние значения", - "lbViewZeroEntrys": "Рейтинговая таблица пуста", - "lbViewOneEntry": "В рейтинговой таблице всего один игрок", - "lbViewManyEntrys": "В рейтинговой таблице находится ${numberOfPlayers}", - "everyoneAverages": "Значения таблицы", - "sortBy": "Cортировать по", - "reversed": "Наоборот", - "country": "Страна", - "rankAverages": "Значения для $rank ранга", - "players":{ - "zero": "$n игроков", - "one": "$n игрок", - "two": "$n игрока", - "few": "$n игрока", - "many": "$n игроков", - "other": "$n игроков" - }, - "games": { - "zero": "$n игр", - "one": "$n игра", - "two": "$n игры", - "few": "$n игры", - "many": "$n игр", - "other": "$n игр" - }, - "gamesPlayed": "$games сыграно", - "chart": "График", - "entries": "Список", - "minimums": "Минимумы", - "maximums": "Максимумы", - "lowestValues": "Самые низкие показатели", - "averageValues": "Средние значения показателей", - "highestValues": "Самые высокие показатели", - "forPlayer": "для игрока $username", - "currentAxis": "Ось $axis:", - "p1nkl0bst3rAlert": "Эти данные были получены из стороннего API, который поддерживается p1nkl0bst3r", - "notForWeb": "Функция недоступна для веб версии", - "graphs": { - "attack": "Атака", - "speed": "Скорость", - "defense": "Защита", - "cheese": "Сыр" - }, - "statCellNum": { - "xpLevel": "Уровень\nопыта", - "xpProgress": "Прогресс до следующего уровня", - "xpFrom0ToLevel": "Прогресс от 0 XP до $n уровня", - "xpLeft": "XP осталось", - "hoursPlayed": "Часов\nСыграно", - "onlineGames": "Онлайн\nИгр", - "gamesWon": "Онлайн\nПобед", - "totalGames": "Всего матчей", - "totalWon": "Всего побед", - "friends": "Друзей", - "apm": "Атака в\nМинуту", - "vs": "Показатель\nVersus", - "recordLB": "Место в таблице", - "lbp": "Положение\nв рейтинге", - "lbpShort": "№ в рейтинге", - "lbpc": "Положение\nв рейтинге страны", - "lbpcShort": "№ по стране", - "gamesPlayed": "Игр\nСыграно", - "gamesWonTL": "Побед", - "winrate": "Процент\nпобед", - "level": "Уровень", - "score": "Счёт", - "spp": "Очков\nна Фигуру", - "pieces": "Фигур\nУстановлено", - "pps": "Фигур в\nСекунду", - "finesseFaults": "Ошибок\nТехники", - "finessePercentage": "% Качества\nТехники", - "keys": "Нажатий\nКлавиш", - "kpp": "Нажатий\nна Фигуру", - "kps": "Нажатий\nв Секунду", - "tr": "Тетра Рейтинг", - "rd": "Отклонение рейтинга", - "app": "Атака на Фигуру", - "appDescription": "(Сокращенно APP) Главный показатель эффективности. Показывает, сколько атаки приходится на одну фигуру", - "vsapmDescription": "В основном, показывает как много мусора игрок использует в своих атаках и насколько эффективно.", - "dss": "Downstack\nв Секунду", - "dssDescription": "(Сокращенно DS/S) Downstack (спуск вниз) в Секунду показывает как много мусорных линий в среднем игрок убирает за одну секунду.", - "dsp": "Downstack\nна Фигуру", - "dspDescription": "(Сокращенно DS/P) Downstack (спуск вниз) на Фигуру показывает как много мусорных линий в среднем игрок убирает одну фигуру.", - "appdsp": "APP + DS/P", - "appdspDescription": "Просто сумма Атаки на Фигуру и Downstack на Фигуру.", - "cheese": "Индекс сыра", - "cheeseDescription": "(Сокращенно Cheese) Индекс сыра является аппроксимацией того, насколько чистый / дырявый мусор игрок отправляет. Меньше = более чистый. Больше = более дырявый.\nПридумал kerrmunism", - "gbe": "Garbage\nEfficiency", - "gbeDescription": "(Сокращенно Gb Eff.) Garbage Efficiency показывает насколько хорошо игрок использует свой мусор. Больше = лучше (или он использует больше мусора). Меньше = в основном отправляют сыр (или он редко чистит мусор).\nПридумали Zepheniah и Dragonboy.", - "nyaapp": "Взвешенный\nAPP", - "nyaappDescription": "(Сокращенно wAPP) По сути, показывает способность отправлять сыр, сохраняя при этом высокую эффективность.\nПридумал Wertj.", - "area": "Area", - "areaDescription": "Какую площадь занимает диаграмма, если не брать в расчёт индекс сыра и VS/APM", - "estOfTR": "Расчётный TR", - "estOfTRShort": "Расч. TR", - "accOfEst": "Точность расчёта", - "accOfEstShort": "Точность" - }, - "playerRole(map)": { - "user": "Пользователь", - "banned": "Заблокированный пользователь", - "bot": "Бот", - "sysop": "Системный оператор", - "admin": "Администратор", - "mod": "Модератор", - "halfmod": "Модератор сообщества", - "anon": "Аноним" - }, - "numOfGameActions":{ - "pc": "Все чисто", - "hold": "В запас", - "inputs": { - "zero": "$n нажатий клавиш", - "one": "$n нажатие на клавишу", - "two": "$n нажатия на клавишы", - "few": "$n нажатия на клавишы", - "many": "$n нажатий на клавиш", - "other": "$n нажатий на клавиш" - }, - "tspinsTotal": { - "zero": "$n T-спинов всего", - "one": "Всего $n T-спин", - "two": "$n T-спина всего", - "few": "$n T-спина всего", - "many": "$n T-спинов всего", - "other": "$n T-спинов всего" - }, - "lineClears": { - "zero": "$n линий очищено", - "one": "$n линия очищена", - "two": "$n линии очищено", - "few": "$n линии очищено", - "many": "$n линий очищено", - "other": "$n линий очищено" - } - }, - "popupActions":{ - "cancel": "Отменить", - "submit": "Подтвердить", - "ok": "OK" - }, - "errors":{ - "connection": "Проблема с подключением: ${code} ${message}", - "noSuchUser": "Нет такого пользователя", - "noSuchUserSub": "Либо вы ошиблись при вводе, либо аккаунта больше не существует", - "discordNotAssigned": "К данному Discord ID не привязан аккаунт", - "discordNotAssignedSub": "Убедитесь в том, что вы вставили правильный ID", - "history": "История данного игрока отсутствует", - "actionSuggestion": "Возможно, вы хотите", - "p1nkl0bst3rTLmatches": "Старых матчей Тетра Лиги не было найдено", - "clientException": "Нет соединения с интернетом", - "forbidden": "Ваш IP адрес заблокирован", - "forbiddenSub": "Если у вас работает VPN или прокси, выключите его. Если это не помогло, свяжитесь с $nickname", - "tooManyRequests": "Слишком много запросов", - "tooManyRequestsSub": "Подождите немного и попробуйте снова", - "internal": "Что-то случилось на стороне tetr.io", - "internalSub": "Скорее всего, osk уже в курсе об этом", - "internalWebVersion": "Что-то случилось на стороне tetr.io (или на стороне oskware_bridge, я хз если честно)", - "internalWebVersionSub": "Если статус страница osk-а говорит, что всё ок - свяжитесь с dan63047", - "oskwareBridge": "Что-то случилось с oskware_bridge", - "oskwareBridgeSub": "Дайте dan63047 знать", - "p1nkl0bst3rForbidden": "Стороннее API заблокировало ваш IP адрес", - "p1nkl0bst3rTooManyRequests": "Слишком много запросов к стороннему API. Попробуйте позже", - "p1nkl0bst3rinternal": "Что-то случилось на стороне p1nkl0bst3r-а", - "p1nkl0bst3rinternalWebVersion": "Что-то случилось на стороне p1nkl0bst3r-а (или на стороне oskware_bridge, я хз если честно)", - "replayAlreadySaved": "Повтор уже сохранён", - "replayExpired": "Повтор истёк и больше недоступен", - "replayRejected": "Стороннее API заблокировало ваш IP адрес" - }, - "countries(map)": { - "": "Не выбрана", - - "AF": "Афганистан", - "AX": "Аландские острова", - "AL": "Албания", - "DZ": "Алжир", - "AS": "Американское Самоа", - "AD": "Андорра", - "AO": "Ангола", - "AI": "Ангилья", - "AQ": "Антарктида", - "AG": "Антигуа и Барбуда", - "AR": "Аргентина", - "AM": "Армения", - "AW": "Аруба", - "AU": "Австралия", - "AT": "Австрия", - "AZ": "Азербайджан", - - "BS": "Багамские острова", - "BH": "Бахрейн", - "BD": "Бангладеш", - "BB": "Барбадос", - "BY": "Беларусь", - "BE": "Бельгия", - "BZ": "Белиз", - "BJ": "Бенин", - "BM": "Бермуды", - "BT": "Бутан", - "BO": "Боливия, Многонациональное Государство", - "BA": "Босния и Герцеговина", - "BW": "Ботсвана", - "BV": "Остров Буве", - "BR": "Бразилия", - "IO": "Британская территория в Индийском океане", - "BN": "Бруней-Даруссалам", - "BG": "Болгария", - "BF": "Буркина-Фасо", - "BI": "Бурунди", - - "KH": "Камбоджа", - "CM": "Камерун", - "CA": "Канада", - "CV": "Кабо-Верде", - "BQ": "Карибские Нидерланды", - "KY": "Каймановы острова", - "CF": "Центральноафриканская Республика", - "TD": "Чад", - "CL": "Чили", - "CN": "Китай", - "CX": "Остров Рождества", - "CC": "Кокосовые острова", - "CO": "Колумбия", - "KM": "Коморские острова", - "CG": "Конго", - "CD": "Конго, Демократическая Республика", - "CK": "Острова Кука", - "CR": "Коста-Рика", - "CI": "Берег Слоновой Кости", - "HR": "Хорватия", - "CU": "Куба", - "CW": "Кюрасао", - "CY": "Кипр", - "CZ": "Чешская Республика", - - "DK": "Дания", - "DJ": "Джибути", - "DM": "Доминика", - "DO": "Доминиканская Республика", - - "EC": "Эквадор", - "EG": "Египет", - "SV": "Сальвадор", - "GB-ENG": "Англия", - "GQ": "Экваториальная Гвинея", - "ER": "Эритрея", - "EE": "Эстония", - "ET": "Эфиопия", - "EU": "Европа", - - "FK": "Фолклендские (Мальвинские) острова", - "FO": "Фарерские острова", - "FJ": "Фиджи", - "FI": "Финляндия", - "FR": "Франция", - "GF": "Французская Гвиана", - "PF": "Французская Полинезия", - "TF": "Южные территории Франции", - - "GA": "Габон", - "GM": "Гамбия", - "GE": "Грузия", - "DE": "Германия", - "GH": "Гана", - "GI": "Гибралтар", - "GR": "Греция", - "GL": "Гренландия", - "GD": "Гренада", - "GP": "Гваделупа", - "GU": "Гуам", - "GT": "Гватемала", - "GG": "Гернси", - "GN": "Гвинея", - "GW": "Гвинея-Бисау", - "GY": "Гайана", - - "HT": "Гаити", - "HM": "Остров Херд и острова Макдональд", - "VA": "Святой Престол (государство-городок Ватикан)", - "HN": "Гондурас", - "HK": "Гонконг", - "HU": "Венгрия", - - "IS": "Исландия", - "IN": "Индия", - "ID": "Индонезия", - "IR": "Иран, Исламская Республика", - "IQ": "Ирак", - "IE": "Ирландия", - "IM": "Остров Мэн", - "IL": "Израиль", - "IT": "Италия", - - "JM": "Ямайка", - "JP": "Япония", - "JE": "Джерси", - "JO": "Иордания", - - "KZ": "Казахстан", - "KE": "Кения", - "KI": "Кирибати", - "KP": "Корея, Народно-Демократическая Республика", - "KR": "Корея, Республика", - "XK": "Косово", - "KW": "Кувейт", - "KG": "Кыргызстан", - - "LA": "Лаосская Народно-Демократическая Республика", - "LV": "Латвия", - "LB": "Ливан", - "LS": "Лесото", - "LR": "Либерия", - "LY": "Ливия", - "LI": "Лихтенштейн", - "LT": "Литва", - "LU": "Люксембург", - - "MO": "Макао", - "MK": "Македония, бывшая югославская республика", - "MG": "Мадагаскар", - "MW": "Малави", - "MY": "Малайзия", - "MV": "Мальдивы", - "ML": "Мали", - "MT": "Мальта", - "MH": "Маршалловы острова", - "MQ": "Мартиника", - "MR": "Мавритания", - "MU": "Маврикий", - "YT": "Майотта", - "MX": "Мексика", - "FM": "Микронезия, Федеративные Штаты", - "MD": "Молдова, Республика", - "MC": "Монако", - "ME": "Черногория", - "MA": "Марокко", - "MN": "Монголия", - "MS": "Монтсеррат", - "MZ": "Мозамбик", - "MM": "Мьянма", - - "NA": "Намибия", - "NR": "Науру", - "NP": "Непал", - "NL": "Нидерланды", - "AN": "Нидерландские Антильские острова", - "NC": "Новая Каледония", - "NZ": "Новая Зеландия", - "NI": "Никарагуа", - "NE": "Нигер", - "NG": "Нигерия", - "NU": "Ниуэ", - "NF": "Остров Норфолк", - "GB-NIR": "Северная Ирландия", - "MP": "Северные Марианские острова", - "NO": "Норвегия", - - "OM": "Оман", - - "PK": "Пакистан", - "PW": "Палау", - "PS": "Палестина", - "PA": "Панама", - "PG": "Папуа-Новая Гвинея", - "PY": "Парагвай", - "PE": "Перу", - "PH": "Филиппины", - "PN": "Питкэрн", - "PL": "Польша", - "PT": "Португалия", - "PR": "Пуэрто-Рико", - - "QA": "Катар", - - "RE": "Реюньон", - "RO": "Румыния", - "RU": "Российская Федерация", - "RW": "Руанда", - - "BL": "Сен-Бартелеми", - "SH": "Острова Святой Елены, Вознесения и Тристан-да-Кунья", - "KN": "Сент-Китс и Невис", - "LC": "Сент-Люсия", - "MF": "Сен-Мартен", - "PM": "Сен-Пьер и Микелон", - "VC": "Сент-Винсент и Гренадины", - "WS": "Самоа", - "SM": "Сан-Марино", - "ST": "Сан-Томе и Принсипи", - "SA": "Саудовская Аравия", - "GB-SCT": "Шотландия", - "SN": "Сенегал", - "RS": "Сербия", - "SC": "Сейшельские острова", - "SL": "Сьерра-Леоне", - "SG": "Сингапур", - "SX": "Синт-Мартен (голландская часть)", - "SK": "Словакия", - "SI": "Словения", - "SB": "Соломоновы острова", - "SO": "Сомали", - "ZA": "ЮАР", - "GS": "Южная Георгия и Южные Сандвичевы острова", - "SS": "Южный Судан", - "ES": "Испания", - "LK": "Шри-Ланка", - "SD": "Судан", - "SR": "Суринам", - "SJ": "Острова Шпицберген и Ян-Майен", - "SZ": "Свазиленд", - "SE": "Швеция", - "CH": "Швейцария", - "SY": "Сирийская Арабская Республика", - - "TW": "Тайвань", - "TJ": "Таджикистан", - "TZ": "Танзания, Объединенная Республика", - "TH": "Таиланд", - "TL": "Тимор-Лешти", - "TG": "Того", - "TK": "Токелау", - "TO": "Tonga", - "TT": "Тринидад и Тобаго", - "TN": "Тунис", - "TR": "Турция", - "TM": "Туркменистан", - "TC": "Острова Теркс и Кайкос", - "ТВ": "Тувалу", - - "UG": "Уганда", - "UA": "Украина", - "AE": "Объединенные Арабские Эмираты", - "GB": "Великобритания", - "US": "Соединенные Штаты", - "UY": "Уругвай", - "UM": "Малые периферийные острова США", - "UZ": "Узбекистан", - - "VU": "Вануату", - "VE": "Венесуэла, Боливарианская Республика", - "VN": "Вьетнам", - "VG": "Виргинские острова, Британские", - "VI": "Виргинские острова, США", - - "GB-WLS": "Уэльс", - "WF": "Острова Уоллис и Футуна", - "EH": "Западная Сахара", - - "YE": "Йемен", - - "ZM": "Замбия", - "ZW": "Зимбабве", - - "XX": "Неизвестно", - "XM": "Луна" - } - } \ No newline at end of file diff --git a/res/i18n/strings_zh-CN.i18n.json b/res/i18n/strings_zh-CN.i18n.json new file mode 100644 index 0000000..2728f4f --- /dev/null +++ b/res/i18n/strings_zh-CN.i18n.json @@ -0,0 +1,929 @@ +{ + "locales(map)": { + "en": "英语 (English)", + "ru-RU": "俄语 (Русский)", + "zh-CN": "简体中文" + }, + "gamemodes(map)": { + "league": "Tetra 联赛", + "zenith": "快速游戏", + "zenithex": "快速游戏 · 专家模式", + "40l": "40行竞速", + "blitz": "闪电战", + "5mblast": "5,000,000 Blast", + "zen": "禅意模式" + }, + "destinations": { + "home": "主页", + "graphs": "图表", + "leaderboards": "排行榜", + "cutoffs": "段位分界线", + "calc": "计算器", + "info": "信息中心", + "data": "已保存的数据", + "settings": "设置" + }, + "playerRole(map)": { + "user": "用户", + "banned": "已封禁", + "bot": "机器人", + "sysop": "系统操作员", + "admin": "管理员", + "mod": "管理员", + "halfmod": "社区管理员", + "anon": "匿名用户" + }, + "goBackButton": "返回", + "nanow": "目前不可用...", + "seasonEnds": "当前赛季还有${countdown}结束", + "seasonEnded": "赛季已结束", + "overallPB": "生涯最佳:$pb m", + "gamesUntilRanked": "还有${left}局才可获得段位", + "numOfVictories": "~${wins}次胜局", + "promotionOnNextWin": "下一场胜局即可升段", + "numOfdefeats": "~${losses}次负局", + "demotionOnNextLoss": "下一场负局即可掉段", + "records": "记录", + "nerdStats": "详细信息", + "playstyles": "游戏方式", + "horoscopes": "散点图", + "relatedAchievements": "相关成就", + "season": "赛季", + "smooth": "平滑", + "dateAndTime": "日期和时间:", + "TLfullLBnote": "很大,但允许你通过玩家的数据对玩家进行排序,还可以按段位筛选玩家", + "rank": "段位", + "verdictGeneral": "比 $rank 段平均数据$n $verdict", + "verdictBetter": "好", + "verdictWorse": "差", + "localStanding": "本地", + "xp": { + "title": "经验等级", + "progressToNextLevel": "到下一等级的进度:$percentage", + "progressTowardsGoal": "从0级到$goal级的进度:$percentage (还差 $left 点经验值)" + }, + "gametime": { + "title": "精确游戏时长", + "gametimeAday": "平均每天$gametime", + "breakdown": "相当于 $years 年,\n$months 月,\n$days 天,\n$minutes 分钟,\n$seconds 秒" + }, + "track": "跟踪", + "stopTracking": "停止跟踪", + "supporter": "${tier}级会员", + "comparingWith": "${newDate}的数据与${oldDate}相比", + "compare": "比较", + "comparison": "比较", + "enterUsername": "输入用户名或者\\$avgX (X是一个段位)", + "general": "常规", + "badges": "勋章", + "obtainDate": "于${date}获得", + "assignedManualy": "此徽章由TETR.IO管理员手动颁发", + "distinguishment": "区别", + "banned": "已封禁", + "bannedSubtext": "由于 TETR.IO 规则或服务条款被违反 而被封禁", + "badStanding": "信誉不佳", + "badStandingSubtext": "近期有一次或多次违禁行为", + "botAccount": "机器人账号", + "botAccountSubtext": "由$botMaintainers管理", + "copiedToClipboard": "已复制到剪贴板!", + "bio": "个性签名", + "news": "新闻", + "matchResult": { + "victory": "胜利", + "defeat": "失败", + "tie": "平局", + "dqvictory": "对手被取消资格", + "dqdefeat": "被取消资格", + "nocontest": "无竞赛记录", + "nullified": "竞赛记录已取消" + }, + "distinguishments": { + "noHeader": "缺少标题", + "noFooter": "缺少标题", + "twc": "TETR.IO 世界冠军", + "twcYear": "$year TETR.IO 世界杯" + }, + "newsEntries": { + "leaderboard(rich)": "在$gametype中荣获第$rank名", + "personalbest(rich)": "在$gametype中取得新纪录:$pb", + "badge(rich)": "获得勋章 $badge", + "rankup(rich)": "升 $rank", + "supporter(rich)": "成为${s(TETR.IO supporter)}", + "supporter_gift(rich)": "被赠送${s(TETR.IO supporter)}", + "unknown(rich)": "未知新闻类型 $type" + }, + "rankupMiddle": "${r} 段", + "copyUserID": "点击以复制用户 ID", + "searchHint": "用户名或 ID", + "navMenu": "导航菜单", + "navMenuTooltip": "打开导航菜单", + "refresh": "刷新数据", + "searchButton": "搜索", + "trackedPlayers": "跟踪的玩家", + "standing": "名次", + "previousSeasons": "上赛季", + "recent": "最近", + "top": "前", + "noRecord": "暂无记录", + "sprintAndBlitsRelevance": "${date}", + "snackBarMessages": { + "stateRemoved": "成功移除${date}时的状态!", + "matchRemoved": "成功移除${date}时的一局!", + "notForWeb": "此功能在网络版本中不可用", + "importSuccess": "导入成功", + "importCancelled": "导入已取消" + }, + "errors": { + "noRecords": "暂无记录", + "notEnoughData": "数据不足", + "noHistorySaved": "没有保存历史记录", + "connection": "连接错误:${code} ${message}", + "noSuchUser": "用户不存在", + "noSuchUserSub": "您输入的内容有误,或者用户不存在", + "discordNotAssigned": "没有指定Discord ID的用户", + "discordNotAssignedSub": "请确保您提供了有效的 ID", + "history": "缺少该玩家的历史", + "actionSuggestion": "也许,您想要", + "p1nkl0bst3rTLmatches": "没有找到Tetra联赛比赛", + "clientException": "你尚未连接", + "forbidden": "您的 IP 地址已被封禁", + "forbiddenSub": "如果你在使用VPN,请关闭。如果仍然不可以,请联系$nickname", + "tooManyRequests": "您的评分已经被限制", + "tooManyRequestsSub": "请稍后重试", + "internal": "TETR.IO 出现了问题!", + "internalSub": "osk,或许,已经知道了", + "internalWebVersion": "TETR.IO (也许是oskware_bridge,我不知道到底是哪儿) 出现了问题!", + "internalWebVersionSub": "如果 osk status 页面显示一切都很正常,请联系dan63047", + "oskwareBridge": "oskware_bridge 出现了问题!", + "oskwareBridgeSub": "请联系dan63047", + "p1nkl0bst3rForbidden": "第三方API屏蔽了您的 IP 地址", + "p1nkl0bst3rTooManyRequests": "第三方API请求过多,请稍后再试", + "p1nkl0bst3rinternal": "p1nkl0bst3r 那边出现了问题!", + "p1nkl0bst3rinternalWebVersion": "p1nkl0bst3r 那边(也许是oskware_bridge,我不知道到底是哪儿)出现了问题!", + "replayAlreadySaved": "回放已保存", + "replayExpired": "回放已过期或不再可用", + "replayRejected": "第三方API屏蔽了您的 IP 地址" + }, + "actions": { + "cancel": "取消", + "submit": "确定", + "ok": "确定", + "apply": "应用", + "refresh": "刷新" + }, + "graphsDestination": { + "fetchAndsaveTLHistory": "获取玩家历史", + "fetchAndSaveOldTLmatches": "获取 Tetra 联赛历史记录", + "fetchAndsaveTLHistoryResult": "找到 ${number} 个状态", + "fetchAndSaveOldTLmatchesResult": "找到 ${number} 场比赛", + "gamesPlayed": "游玩次数:$games", + "dateAndTime": "日期和时间", + "filterModaleTitle": "在图表上筛选等级" + }, + "filterModale": { + "all": "全部" + }, + "cutoffsDestination": { + "title": "Tetra 联赛 状态", + "relevance": "$timestamp", + "actual": "实际", + "target": "目标", + "cutoffTR": "分段 TR", + "targetTR": "目标 TR", + "state": "状态", + "advanced": "高级选项", + "players": "玩家($n)", + "moreInfo": "更多信息", + "NumberOne": "№ 1 is $tr TR", + "inflated": "高于目标 $tr", + "notInflated": "不偏高", + "deflated": "低于目标 $tr", + "notDeflated": "不偏低", + "wellDotDotDot": "嗯…", + "fromPlace": "自 № $n", + "viewButton": "查看" + }, + "rankView": { + "rankTitle": "$rank 段数据", + "everyoneTitle": "全部排行榜", + "trRange": "TR 范围", + "supposedToBe": "应为", + "gap": "相差 $value", + "trGap": "相差 $value TR", + "deflationGap": "偏低量", + "inflationGap": "偏高量", + "LBposRange": "排行榜位置范围", + "overpopulated": "比期望的多 $players", + "underpopulated": "比期望的少 $players", + "PlayersEqualSupposedToBe": "符合", + "avgStats": "平均数据", + "avgForRank": "$rank 段平均数据", + "avgNerdStats": "平均详细信息", + "minimums": "最小值", + "maximums": "最大值" + }, + "stateView": { + "title": "$date的状态" + }, + "tlMatchView": { + "match": "匹配", + "vs": "vs", + "winner": "获胜者", + "roundNumber": "第$n回合", + "statsFor": "状态", + "numberOfRounds": "回合数", + "matchLength": "比赛时长", + "roundLength": "回合时长", + "matchStats": "比赛数据", + "downloadReplay": "下载 .ttrm 回放", + "openReplay": "在 TETR.IO 中打开回放" + }, + "calcDestination": { + "placeholders": "输入你的$stat", + "tip": "输入值并按 \"计算\" 来查看TA的详细信息", + "statsCalcButton": "计算", + "damageCalcTip": "点击左侧的操作在此添加", + "actions": "操作", + "results": "结果", + "rules": "规则", + "noSpinClears": "非 Spin 清除", + "spins": "Spin", + "miniSpins": "Mini spin", + "noLineclear": "无清除(连消结束)", + "custom": "自定义", + "multiplier": "倍增", + "pcDamage": "全消伤害", + "comboTable": "连击", + "b2bChaining": "B2B增伤", + "surgeStartAtB2B": "开始于B2B", + "surgeStartAmount": "初始值", + "totalDamage": "累计伤害", + "lineclears": "清除行数", + "combo": "连击", + "surge": "B2B充能", + "pcs": "全消" + }, + "infoDestination": { + "title": "信息中心", + "sprintAndBlitzAverages": "40 行 & 闪电战平均数据", + "sprintAndBlitzAveragesDescription": "计算40 行 & 闪电战平均数据是个很繁琐的过程,所以很久才会更新一次。 点击标题查看完整的 40 行 & 闪电战平均数据表", + "tetraStatsWiki": "Tetra Stats Wiki", + "tetraStatsWikiDescription": "查看更多关于Tetra Stats提供的功能和数据", + "about": "关于 Tetra Stats", + "aboutDescription": "由 dan63 开发" + }, + "leaderboardsDestination": { + "title": "排行榜", + "tl": "Tetra 联赛(当前赛季)", + "fullTL": "Tetra 联赛(当前赛季,完整)", + "ar": "成就点" + }, + "savedDataDestination": { + "title": "已保存的数据", + "tip": "选择左边的昵称以查看与之相关的数据", + "seasonTLstates": "第$s赛季状态", + "TLrecords": "联赛记录" + }, + "settingsDestination": { + "title": "设置", + "general": "常规", + "customization": "自定义设置", + "database": "本地数据库", + "checking": "正在检查...", + "enterToSubmit": "按回车键提交", + "account": "您的 TETR.IO 账号", + "accountDescription": "该玩家的状态将在启动此应用后立即加载。 默认情况下,它会加载我的数据。如要更改,请在此输入您的昵称。", + "done": "完成!", + "noSuchAccount": "账号不存在", + "language": "语言", + "languageDescription": "Tetra Stats 有$languages。默认情况下,应用程序将选择您的系统语言,如果您的系统区域设置不可用,则选择英语。", + "languages(plural)": { + "zero": "0种语言", + "one": "$n种语言", + "two": "$n种语言", + "few": "$n种语言", + "many": "$n种语言", + "other": "$n种语言" + }, + "updateInTheBackground": "后台更新数据", + "updateInTheBackgroundDescription": "如果开启,Tetra Stats将尝试在缓存过期后查询新信息。通常一次/5分钟。", + "compareStats": "将TL数据与段位平均水平作比较", + "compareStatsDescription": "如果开启,Tetra Stats将提供额外的量度,使您能够将自己与普通玩家的等级相比较。 你看到它的方式——统计信息将以相应的颜色高亮,用光标悬停在它们上面以获取更多信息。", + "showPosition": "显示排行榜中的位置", + "showPositionDescription": "这可能需要一些时间(和流量),但您可以看到您在排行榜上的位置,按数据排序", + "accentColor": "主题色", + "accentColorDescription": "这种颜色会在这个应用上可见,而且通常会高亮显示交互界面元素。", + "accentColorModale": "选取主题色", + "timestamps": "时间戳格式", + "timestampsDescriptionPart1": "您可以选择时间戳显示时间的方式。默认情况下,它们以 GMT 时区显示时间,并根据所选区域设置进行格式设置,例如:$d。", + "timestampsDescriptionPart2": "这里还有:\n• 以您的时区设置的区域设置:$y\n• 相对时间戳:$r", + "timestampsAbsoluteGMT": "GMT", + "timestampsAbsoluteLocalTime": "您的时区", + "timestampsRelative": "相对", + "sheetbotLikeGraphs": "Sheetbot 型雷达图", + "sheetbotLikeGraphsDescription": "尽管我认为,图表在 SheetBot 中的工作方式不是很正确,有些人感到困惑,那 -0.5 Stride 看起来不像它在 SheetBot 图表上那样。因此,我们这里有:如果开启,则如果数值为负,则图形上的点可以出现在图形的另一半。", + "oskKagariGimmick": "Osk-Kagari", + "oskKagariGimmickDescription": "如果开启,osk的段位会显示为:kagari:", + "bytesOfDataStored": "存储数据", + "TLrecordsSaved": "已保存 Tetra 联赛记录", + "TLplayerstatesSaved": "已保存 Tetra 联赛玩家状态", + "fixButton": "修复", + "compressButton": "压缩", + "exportDB": "导出本地数据库", + "desktopExportAlertTitle": "桌面导出", + "desktopExportText": "看起来您在桌面上使用了这个应用程序。请检查您的文档文件夹,您应该找到\"TetraStats.db\"。请将其复制到某处", + "androidExportAlertTitle": "Android 导出", + "androidExportText": "已导出。\n${exportedDB}", + "importDB": "导入本地数据库", + "importDBDescription": "还原您的备份。请注意已存储的数据库将被覆盖。", + "importWrongFileType": "文件类型错误!" + }, + "homeNavigation": { + "overview": "概览", + "standing": "名次", + "seasons": "赛季", + "mathces": "比赛场次", + "pb": "个人最佳", + "normal": "普通模式", + "expert": "专家模式", + "expertRecords": "专家模式记录" + }, + "graphsNavigation": { + "history": "玩家历史记录", + "league": "联赛状态", + "cutoffs": "分段线历史" + }, + "calcNavigation": { + "stats": "数据计算器", + "damage": "伤害计算器" + }, + "firstTimeView": { + "welcome": "欢迎使用 Tetra Stats", + "description": "服务,允许您跟踪TETR.IO的各种数据", + "nicknameQuestion": "您的昵称是?", + "inpuntHint": "在此处输入... (3-16个符号)", + "emptyInputError": "不能提交空字符串", + "niceToSeeYou": "很高兴见到你,$n", + "letsTakeALook": "让我们看看您的统计资料...", + "skip": "跳过" + }, + "aboutView": { + "title": "关于 Tetra Stats", + "about": "Tetra Stats是一种服务,与TETR.IO Tetra Channel API共用,提供数据并根据这种数据计算一些附加度量。 服务允许用户用\"Track\"功能跟踪他们在Tetra League中的进度,这个功能记录每个Tetra Leage更改到本地数据库(非自动) 您必须不时地访问服务。这样,这些更改可以通过图表来查看。\n\nBeanserver blaster 是Tetra Stats的一部分,它被拆解成服务器侧脚本。 它提供完整的Tetra League排行榜,允许Tetra Stats通过任何公式对排行榜进行排序并生成散点图,这允许用户分析Tetra联赛趋势。 它还提供了Tetra League 的评分历史,用户也可以通过图表看到。\n\n我们有一个添加回放分析和锦标赛历史记录的计划,所以随时关注!\n\n服务没有与TETR.IO与osk以任何身份关联。", + "appVersion": "版本", + "build": "$build", + "GHrepo": "GitHub Repository", + "submitAnIssue": "提交问题", + "credits": "鸣谢", + "authorAndDeveloper": "作者 & 开发者", + "providedFormulas": "提供的公式", + "providedS1history": "提供的 S1 历史", + "inoue": "Inoue (回放抓取器)", + "zhCNlocale": "简中翻译员", + "supportHim": "为他提供支持!" + }, + "stats": { + "registrationDate": "注册时间", + "gametime": "游玩时长", + "ogp": "在线游戏次数", + "ogw": "在线游戏胜利次数", + "followers": "粉丝", + "xp": { + "short": "经验值", + "full": "经验点" + }, + "tr": { + "short": "TR", + "full": "Tetra 评分" + }, + "glicko": { + "short": "Glicko", + "full": "Glicko" + }, + "rd": { + "short": "RD", + "full": "评分偏差" + }, + "glixare": { + "short": "GXE", + "full": "GLIXARE" + }, + "s1tr": { + "short": "S1 TR", + "full": "第 1 赛季式 TR" + }, + "gp": { + "short": "GP", + "full": "总场数" + }, + "gw": { + "short": "GW", + "full": "胜场数" + }, + "winrate": { + "short": "WR%", + "full": "胜率" + }, + "apm": { + "short": "APM", + "full": "每分钟攻击数" + }, + "pps": { + "short": "PPS", + "full": "每秒块数" + }, + "vs": { + "short": "VS", + "full": "VS 分数" + }, + "app": { + "short": "APP", + "full": "每块攻击数" + }, + "vsapm": { + "short": "VS/APM", + "full": "VS / APM" + }, + "dss": { + "short": "DS/S", + "full": "每秒挖掘数" + }, + "dsp": { + "short": "DS/P", + "full": "每块挖掘数" + }, + "appdsp": { + "short": "APP+DSP", + "full": "APP + DSP" + }, + "cheese": { + "short": "CI", + "full": "垃圾行混乱指数" + }, + "gbe": { + "short": "GbE", + "full": "垃圾行效率" + }, + "nyaapp": { + "short": "wAPP", + "full": "加权APP" + }, + "area": { + "short": "面积", + "full": "面积" + }, + "etr": { + "short": "eTR", + "full": "预测 TR" + }, + "etracc": { + "short": "±eTR", + "full": "预测实际差量" + }, + "opener": { + "short": "定式", + "full": "定式" + }, + "plonk": { + "short": "太极", + "full": "太极" + }, + "stride": { + "short": "速度", + "full": "速度" + }, + "infds": { + "short": "挖掘", + "full": "挖掘" + }, + "altitude": { + "short": "m", + "full": "高度" + }, + "climbSpeed": { + "short": "CSP", + "full": "爬行速度", + "gaugetTitle": "爬行速度" + }, + "peakClimbSpeed": { + "short": "最高CSP", + "full": "最高爬行速度", + "gaugetTitle": "最高" + }, + "kos": { + "short": "KO's", + "full": "击杀" + }, + "b2b": { + "short": "B2B", + "full": "背靠背/满贯" + }, + "finesse": { + "short": "极", + "full": "极简率", + "widgetTitle": "简率" + }, + "finesseFaults": { + "short": "非极简", + "full": "非极简操作数" + }, + "totalTime": { + "short": "时长", + "full": "总时长", + "widgetTitle": "总时长" + }, + "level": { + "short": "Lvl", + "full": "等级" + }, + "pieces": { + "short": "P", + "full": "块" + }, + "spp": { + "short": "SPP", + "full": "每块得分" + }, + "kp": { + "short": "KP", + "full": "按键" + }, + "kpp": { + "short": "KPP", + "full": "每块按键数" + }, + "kps": { + "short": "KPS", + "full": "每秒按键数" + }, + "blitzScore": "$p 分", + "levelUpRequirement": "还需 $p 升到下一级", + "piecesTotal": "放块总数", + "piecesWithPerfectFinesse": "极简块数", + "score": "分数", + "lines": "行数", + "linesShort": "行", + "pcs": "全消数", + "holds": "暂存数", + "spike": "最高暴击", + "top": "前 $percentage", + "topRank": "最高段位:$rank", + "floor": "层", + "split": "拆分", + "total": "总计", + "sent": "已发送", + "received": "已接收", + "placement": "排名", + "peak": "最高", + "qpWithMods(plural)": { + "one": "使用 1 个模组", + "two": "使用 $n 个模组", + "few": "使用 $n 个模组", + "many": "使用 $n 个模组", + "other": "使用 $n 个模组" + }, + "inputs(plural)": { + "zero": "$n 按键", + "one": "$n 按键", + "two": "$n 按键", + "few": "$n 按键", + "many": "$n 按键", + "other": "$n 按键" + }, + "tspinsTotal(plural)": { + "zero": "总共 $n 次T旋", + "one": "总共 $n 次T旋", + "two": "总共 $n 次T旋", + "few": "总共 $n 次T旋", + "many": "总共 $n 次T旋", + "other": "总共 $n 次T旋" + }, + "linesCleared(plural)": { + "zero": "总共消除 $n 行", + "one": "总共消除 $n 行", + "two": "总共消除 $n 行", + "few": "总共消除 $n 行", + "many": "总共消除 $n 行", + "other": "总共消除 $n 行" + }, + "graphs": { + "attack": "攻击", + "speed": "速度", + "defense": "防御", + "cheese": "奶酪层" + }, + "players(plural)": { + "zero": "$n 名玩家", + "one": "$n 名玩家", + "two": "$n 名玩家", + "few": "$n 名玩家", + "many": "$n 名玩家", + "other": "$n 名玩家" + }, + "games(plural)": { + "zero": "$n 次游戏", + "one": "$n 次游戏", + "two": "$n 次游戏", + "few": "$n 次游戏", + "many": "$n 次游戏", + "other": "$n 次游戏" + }, + "lineClear": { + "single": "Single", + "double": "Double", + "triple": "Triple", + "quad": "Quad", + "penta": "Penta", + "hexa": "Hexa", + "hepta": "Hepta", + "octa": "Octa", + "ennea": "Ennea", + "deca": "Deca", + "hendeca": "Hendeca", + "dodeca": "Dodeca", + "triadeca": "Triadeca", + "tessaradeca": "Tessaradeca", + "pentedeca": "Pentedeca", + "hexadeca": "Hexadeca", + "heptadeca": "Heptadeca", + "octadeca": "Octadeca", + "enneadeca": "Enneadeca", + "eicosa": "Eicosa", + "kagaris": "Kagaris" + }, + "lineClears": { + "zero": "Zeros", + "single": "Singles", + "double": "Doubles", + "triple": "Triples", + "quad": "Quads", + "penta": "Pentas" + }, + "mini": "Mini", + "tSpin": "T-spin", + "tSpins": "T-spins", + "spin": "Spin", + "spins": "Spins" + }, + "countries(map)": { + "": "全球", + "AF": "阿富汗", + "AX": "奥兰群岛", + "AL": "阿尔巴尼亚", + "DZ": "阿尔及利亚", + "AS": "美属萨摩亚", + "AD": "安道尔", + "AO": "安哥拉", + "AI": "安圭拉", + "AQ": "南极洲", + "AG": "安提瓜和巴布达", + "AR": "阿根廷", + "AM": "亚美尼亚", + "AW": "阿鲁巴", + "AU": "澳大利亚", + "AT": "奥地利", + "AZ": "阿塞拜疆", + "BS": "巴哈马", + "BH": "巴林", + "BD": "孟加拉国", + "BB": "巴巴多斯", + "BY": "白俄罗斯", + "BE": "比利时", + "BZ": "伯利兹", + "BJ": "贝宁", + "BM": "百慕大", + "BT": "不丹", + "BO": "玻利维亚", + "BA": "波黑", + "BW": "博茨瓦纳", + "BV": "布韦岛", + "BR": "巴西", + "IO": "英属印度洋领地", + "BN": "文莱", + "BG": "保加利亚", + "BF": "布基纳法索", + "BI": "布隆迪", + "KH": "柬埔寨", + "CM": "喀麦隆", + "CA": "加拿大", + "CV": "佛得角", + "BQ": "荷兰加勒比区", + "KY": "开曼群岛", + "CF": "中非", + "TD": "乍得", + "CL": "智利", + "CN": "中国", + "CX": "圣诞岛", + "CC": "科科斯群岛", + "CO": "哥伦比亚", + "KM": "科摩罗", + "CG": "刚果(布)", + "CD": "刚果(金)", + "CK": "库克群岛", + "CR": "哥斯达黎加", + "CI": "科特迪瓦", + "HR": "克罗地亚", + "CU": "古巴", + "CW": "库拉索", + "CY": "塞浦路斯", + "CZ": "捷克", + "DK": "丹麦", + "DJ": "吉布提", + "DM": "多米尼克", + "DO": "多米尼加共和国", + "EC": "厄瓜多尔", + "EG": "埃及", + "SV": "萨尔瓦多", + "GB-ENG": "英格兰", + "GQ": "赤道几内亚", + "ER": "厄立特里亚", + "EE": "爱沙尼亚", + "ET": "埃塞俄比亚", + "EU": "欧洲", + "FK": "福克兰群岛 (马尔维纳斯)", + "FO": "法罗群岛", + "FJ": "斐济", + "FI": "芬兰", + "FR": "法国", + "GF": "法属圭亚那", + "PF": "法属波利尼西亚", + "TF": "法属南部领地", + "GA": "加蓬", + "GM": "冈比亚", + "GE": "格鲁吉亚", + "DE": "德国", + "GH": "加纳", + "GI": "直布罗陀", + "GR": "希腊", + "GL": "格陵兰", + "GD": "格林纳达", + "GP": "瓜德罗普", + "GU": "关岛", + "GT": "危地马拉", + "GG": "根西", + "GN": "几内亚", + "GW": "几内亚比绍", + "GY": "圭亚那", + "HT": "海地", + "HM": "赫德岛和麦克唐纳群岛", + "VA": "梵蒂冈", + "HN": "洪都拉斯", + "HK": "中国香港", + "HU": "匈牙利", + "IS": "冰岛", + "IN": "印度", + "ID": "印尼", + "IR": "伊朗", + "IQ": "伊拉克", + "IE": "爱尔兰", + "IM": "马恩岛", + "IL": "以色列", + "IT": "意大利", + "JM": "牙买加", + "JP": "日本", + "JE": "泽西", + "JO": "约旦", + "KZ": "哈萨克斯坦", + "KE": "肯尼亚", + "KI": "基里巴斯", + "KP": "朝鲜", + "KR": "韩国", + "XK": "科索沃", + "KW": "科威特", + "KG": "吉尔吉斯斯坦", + "LA": "老挝", + "LV": "拉托维亚", + "LB": "黎巴嫩", + "LS": "莱索托", + "LR": "利比里亚", + "LY": "利比亚", + "LI": "列支敦士登", + "LT": "立陶宛", + "LU": "卢森堡", + "MO": "中国澳门", + "MK": "马其顿", + "MG": "马达加斯加", + "MW": "马拉维", + "MY": "马来西亚", + "MV": "马尔代夫", + "ML": "马里", + "MT": "马耳他", + "MH": "马绍尔群岛", + "MQ": "马提尼克", + "MR": "毛里塔尼亚", + "MU": "毛里求斯", + "YT": "马约特岛", + "MX": "墨西哥", + "FM": "密克罗尼西亚", + "MD": "摩尔多瓦", + "MC": "摩纳哥", + "ME": "黑山", + "MA": "摩洛哥", + "MN": "蒙古", + "MS": "蒙特塞拉特", + "MZ": "莫桑比克", + "MM": "缅甸", + "NA": "纳米比亚", + "NR": "瑙鲁", + "NP": "尼泊尔", + "NL": "荷兰", + "AN": "荷属安地列斯", + "NC": "新喀里多尼亚", + "NZ": "新西兰", + "NI": "尼加拉瓜", + "NE": "尼日尔", + "NG": "尼日利亚", + "NU": "纽埃", + "NF": "诺福克岛", + "GB-NIR": "北爱尔兰", + "MP": "北马里亚纳群岛", + "NO": "挪威", + "OM": "阿曼", + "PK": "巴基斯坦", + "PW": "帕劳", + "PS": "巴勒斯坦", + "PA": "巴拿马", + "PG": "巴布亚新几内亚", + "PY": "巴拉圭", + "PE": "秘鲁", + "PH": "菲律宾", + "PN": "皮特凯恩", + "PL": "波兰", + "PT": "葡萄牙", + "PR": "波多黎哥", + "QA": "卡塔尔", + "RE": "留尼汪", + "RO": "罗马尼亚", + "RU": "俄罗斯", + "RW": "卢旺达", + "BL": "圣巴泰勒米", + "SH": "圣赫勒拿-阿森松-特里斯坦达库尼亚", + "KN": "圣基茨和尼维斯", + "LC": "圣卢西亚", + "MF": "圣马丁", + "PM": "圣皮埃尔和密克隆", + "VC": "圣文森特和格林纳丁斯", + "WS": "萨摩亚", + "SM": "圣马利诺", + "ST": "圣多美和普林西比", + "SA": "沙特阿拉伯", + "GB-SCT": "苏格兰", + "SN": "塞内加尔", + "RS": "塞尔维亚", + "SC": "塞舌尔", + "SL": "塞拉利昂", + "SG": "新加坡", + "SX": "荷属圣马丁", + "SK": "斯洛伐克", + "SI": "斯洛文尼亚", + "SB": "所罗门", + "SO": "索马里", + "ZA": "南非", + "GS": "南乔治亚和南桑威奇", + "SS": "南苏丹", + "ES": "西班牙", + "LK": "斯里兰卡", + "SD": "苏丹", + "SR": "苏里南", + "SJ": "斯瓦尔巴和扬马延", + "SZ": "斯威士兰", + "SE": "瑞典", + "CH": "瑞士", + "SY": "叙利亚", + "TW": "中国台湾", + "TJ": "塔吉克斯坦", + "TZ": "坦桑尼亚", + "TH": "泰国", + "TL": "东帝汶", + "TG": "多哥", + "TK": "托克劳", + "TO": "汤加", + "TT": "特立尼达和多巴哥", + "TN": "突尼斯", + "TR": "土耳其", + "TM": "土库曼斯坦", + "TC": "特克斯河凯科斯群岛", + "TV": "图瓦卢", + "UG": "乌干达", + "UA": "乌克兰", + "AE": "阿联酋", + "GB": "英国", + "US": "美国", + "UY": "乌拉圭", + "UM": "美国本土外小岛屿", + "UZ": "乌兹别克斯坦", + "VU": "瓦努阿图", + "VE": "委内瑞拉", + "VN": "越南", + "VG": "英属维京群岛", + "VI": "美属维京群岛", + "GB-WLS": "威尔士", + "WF": "瓦利斯群岛和富图纳群岛", + "EH": "西撒哈拉", + "YE": "也门", + "ZM": "赞比亚", + "ZW": "津巴布韦", + "XX": "未知", + "XM": "月球" + } +} diff --git a/res/icons/achievements.png b/res/icons/achievements.png new file mode 100644 index 0000000..0339c34 Binary files /dev/null and b/res/icons/achievements.png differ diff --git a/res/images/info card 1.png b/res/images/info card 1.png new file mode 100644 index 0000000..658da52 Binary files /dev/null and b/res/images/info card 1.png differ diff --git a/res/images/info card 2.png b/res/images/info card 2.png new file mode 100644 index 0000000..a355f1a Binary files /dev/null and b/res/images/info card 2.png differ diff --git a/res/images/info card 3.png b/res/images/info card 3.png new file mode 100644 index 0000000..36477dc Binary files /dev/null and b/res/images/info card 3.png differ diff --git a/res/tetrio_badges/100player.png b/res/tetrio_badges/100player.png deleted file mode 100644 index f219375..0000000 Binary files a/res/tetrio_badges/100player.png and /dev/null differ diff --git a/res/tetrio_badges/20tsd.png b/res/tetrio_badges/20tsd.png deleted file mode 100644 index dcbdf82..0000000 Binary files a/res/tetrio_badges/20tsd.png and /dev/null differ diff --git a/res/tetrio_badges/5mblast_1.png b/res/tetrio_badges/5mblast_1.png deleted file mode 100644 index 6abd717..0000000 Binary files a/res/tetrio_badges/5mblast_1.png and /dev/null differ diff --git a/res/tetrio_badges/5mblast_10.png b/res/tetrio_badges/5mblast_10.png deleted file mode 100644 index 6e454d9..0000000 Binary files a/res/tetrio_badges/5mblast_10.png and /dev/null differ diff --git a/res/tetrio_badges/5mblast_100.png b/res/tetrio_badges/5mblast_100.png deleted file mode 100644 index 8911ee1..0000000 Binary files a/res/tetrio_badges/5mblast_100.png and /dev/null differ diff --git a/res/tetrio_badges/5mblast_1000.png b/res/tetrio_badges/5mblast_1000.png deleted file mode 100644 index 557e296..0000000 Binary files a/res/tetrio_badges/5mblast_1000.png and /dev/null differ diff --git a/res/tetrio_badges/allclear.png b/res/tetrio_badges/allclear.png deleted file mode 100644 index 36965fd..0000000 Binary files a/res/tetrio_badges/allclear.png and /dev/null differ diff --git a/res/tetrio_badges/bugbounty.png b/res/tetrio_badges/bugbounty.png deleted file mode 100644 index 5412404..0000000 Binary files a/res/tetrio_badges/bugbounty.png and /dev/null differ diff --git a/res/tetrio_badges/cometopen_1.png b/res/tetrio_badges/cometopen_1.png deleted file mode 100644 index c127ec0..0000000 Binary files a/res/tetrio_badges/cometopen_1.png and /dev/null differ diff --git a/res/tetrio_badges/cometopen_2.png b/res/tetrio_badges/cometopen_2.png deleted file mode 100644 index ea8d3a1..0000000 Binary files a/res/tetrio_badges/cometopen_2.png and /dev/null differ diff --git a/res/tetrio_badges/cometopen_3.png b/res/tetrio_badges/cometopen_3.png deleted file mode 100644 index 523db1b..0000000 Binary files a/res/tetrio_badges/cometopen_3.png and /dev/null differ diff --git a/res/tetrio_badges/early-supporter.png b/res/tetrio_badges/early-supporter.png deleted file mode 100644 index 0eee60c..0000000 Binary files a/res/tetrio_badges/early-supporter.png and /dev/null differ diff --git a/res/tetrio_badges/founder.png b/res/tetrio_badges/founder.png deleted file mode 100644 index 33fce01..0000000 Binary files a/res/tetrio_badges/founder.png and /dev/null differ diff --git a/res/tetrio_badges/galactic2x2_1.png b/res/tetrio_badges/galactic2x2_1.png deleted file mode 100644 index c0d27d6..0000000 Binary files a/res/tetrio_badges/galactic2x2_1.png and /dev/null differ diff --git a/res/tetrio_badges/galactic2x2_2.png b/res/tetrio_badges/galactic2x2_2.png deleted file mode 100644 index 167cace..0000000 Binary files a/res/tetrio_badges/galactic2x2_2.png and /dev/null differ diff --git a/res/tetrio_badges/galactic2x2_3.png b/res/tetrio_badges/galactic2x2_3.png deleted file mode 100644 index 77df947..0000000 Binary files a/res/tetrio_badges/galactic2x2_3.png and /dev/null differ diff --git a/res/tetrio_badges/ggc_1.png b/res/tetrio_badges/ggc_1.png deleted file mode 100644 index 26f25a5..0000000 Binary files a/res/tetrio_badges/ggc_1.png and /dev/null differ diff --git a/res/tetrio_badges/ggc_2.png b/res/tetrio_badges/ggc_2.png deleted file mode 100644 index 2759375..0000000 Binary files a/res/tetrio_badges/ggc_2.png and /dev/null differ diff --git a/res/tetrio_badges/ggc_3.png b/res/tetrio_badges/ggc_3.png deleted file mode 100644 index 3b29b97..0000000 Binary files a/res/tetrio_badges/ggc_3.png and /dev/null differ diff --git a/res/tetrio_badges/hdoxii_1.png b/res/tetrio_badges/hdoxii_1.png deleted file mode 100644 index 0379f17..0000000 Binary files a/res/tetrio_badges/hdoxii_1.png and /dev/null differ diff --git a/res/tetrio_badges/hdoxii_2.png b/res/tetrio_badges/hdoxii_2.png deleted file mode 100644 index 7511e5a..0000000 Binary files a/res/tetrio_badges/hdoxii_2.png and /dev/null differ diff --git a/res/tetrio_badges/hdoxii_3.png b/res/tetrio_badges/hdoxii_3.png deleted file mode 100644 index 5b692ab..0000000 Binary files a/res/tetrio_badges/hdoxii_3.png and /dev/null differ diff --git a/res/tetrio_badges/heart.png b/res/tetrio_badges/heart.png deleted file mode 100644 index acc47ea..0000000 Binary files a/res/tetrio_badges/heart.png and /dev/null differ diff --git a/res/tetrio_badges/hnprism_1.png b/res/tetrio_badges/hnprism_1.png deleted file mode 100644 index 4e03c19..0000000 Binary files a/res/tetrio_badges/hnprism_1.png and /dev/null differ diff --git a/res/tetrio_badges/hnprism_2.png b/res/tetrio_badges/hnprism_2.png deleted file mode 100644 index b3fda4e..0000000 Binary files a/res/tetrio_badges/hnprism_2.png and /dev/null differ diff --git a/res/tetrio_badges/hnprism_3.png b/res/tetrio_badges/hnprism_3.png deleted file mode 100644 index 5caa0e2..0000000 Binary files a/res/tetrio_badges/hnprism_3.png and /dev/null differ diff --git a/res/tetrio_badges/hnstratosphere50_1.png b/res/tetrio_badges/hnstratosphere50_1.png deleted file mode 100644 index 21b0ea7..0000000 Binary files a/res/tetrio_badges/hnstratosphere50_1.png and /dev/null differ diff --git a/res/tetrio_badges/hnstratosphere50_2.png b/res/tetrio_badges/hnstratosphere50_2.png deleted file mode 100644 index 8928550..0000000 Binary files a/res/tetrio_badges/hnstratosphere50_2.png and /dev/null differ diff --git a/res/tetrio_badges/hnstratosphere50_3.png b/res/tetrio_badges/hnstratosphere50_3.png deleted file mode 100644 index d251ebf..0000000 Binary files a/res/tetrio_badges/hnstratosphere50_3.png and /dev/null differ diff --git a/res/tetrio_badges/ift_1.png b/res/tetrio_badges/ift_1.png deleted file mode 100644 index 6a27df8..0000000 Binary files a/res/tetrio_badges/ift_1.png and /dev/null differ diff --git a/res/tetrio_badges/ift_2.png b/res/tetrio_badges/ift_2.png deleted file mode 100644 index 97d85ab..0000000 Binary files a/res/tetrio_badges/ift_2.png and /dev/null differ diff --git a/res/tetrio_badges/ift_3.png b/res/tetrio_badges/ift_3.png deleted file mode 100644 index 51213b9..0000000 Binary files a/res/tetrio_badges/ift_3.png and /dev/null differ diff --git a/res/tetrio_badges/infdev.png b/res/tetrio_badges/infdev.png deleted file mode 100644 index c94e78d..0000000 Binary files a/res/tetrio_badges/infdev.png and /dev/null differ diff --git a/res/tetrio_badges/kod_by_founder.png b/res/tetrio_badges/kod_by_founder.png deleted file mode 100644 index a4aad3b..0000000 Binary files a/res/tetrio_badges/kod_by_founder.png and /dev/null differ diff --git a/res/tetrio_badges/kod_founder.png b/res/tetrio_badges/kod_founder.png deleted file mode 100644 index 06aa918..0000000 Binary files a/res/tetrio_badges/kod_founder.png and /dev/null differ diff --git a/res/tetrio_badges/leaderboard1.png b/res/tetrio_badges/leaderboard1.png deleted file mode 100644 index 6264876..0000000 Binary files a/res/tetrio_badges/leaderboard1.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_1.png b/res/tetrio_badges/mmc_tabi_1.png deleted file mode 100644 index 45e343f..0000000 Binary files a/res/tetrio_badges/mmc_tabi_1.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_2.png b/res/tetrio_badges/mmc_tabi_2.png deleted file mode 100644 index d0e8e50..0000000 Binary files a/res/tetrio_badges/mmc_tabi_2.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_3.png b/res/tetrio_badges/mmc_tabi_3.png deleted file mode 100644 index 5f64b43..0000000 Binary files a/res/tetrio_badges/mmc_tabi_3.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_superlobby.png b/res/tetrio_badges/mmc_tabi_superlobby.png deleted file mode 100644 index 1b4fef2..0000000 Binary files a/res/tetrio_badges/mmc_tabi_superlobby.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_superlobby2.png b/res/tetrio_badges/mmc_tabi_superlobby2.png deleted file mode 100644 index 72a5810..0000000 Binary files a/res/tetrio_badges/mmc_tabi_superlobby2.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_superlobby3.png b/res/tetrio_badges/mmc_tabi_superlobby3.png deleted file mode 100644 index 3cd35a0..0000000 Binary files a/res/tetrio_badges/mmc_tabi_superlobby3.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_superlobby4.png b/res/tetrio_badges/mmc_tabi_superlobby4.png deleted file mode 100644 index 08d6401..0000000 Binary files a/res/tetrio_badges/mmc_tabi_superlobby4.png and /dev/null differ diff --git a/res/tetrio_badges/mmc_tabi_superlobby5.png b/res/tetrio_badges/mmc_tabi_superlobby5.png deleted file mode 100644 index edf07dd..0000000 Binary files a/res/tetrio_badges/mmc_tabi_superlobby5.png and /dev/null differ diff --git a/res/tetrio_badges/mts_1.png b/res/tetrio_badges/mts_1.png deleted file mode 100644 index 0cf0b28..0000000 Binary files a/res/tetrio_badges/mts_1.png and /dev/null differ diff --git a/res/tetrio_badges/mts_2.png b/res/tetrio_badges/mts_2.png deleted file mode 100644 index eb704f8..0000000 Binary files a/res/tetrio_badges/mts_2.png and /dev/null differ diff --git a/res/tetrio_badges/mts_3.png b/res/tetrio_badges/mts_3.png deleted file mode 100644 index 8f706f3..0000000 Binary files a/res/tetrio_badges/mts_3.png and /dev/null differ diff --git a/res/tetrio_badges/mts_participation.png b/res/tetrio_badges/mts_participation.png deleted file mode 100644 index b48f401..0000000 Binary files a/res/tetrio_badges/mts_participation.png and /dev/null differ diff --git a/res/tetrio_badges/pkrescueroyale_1.png b/res/tetrio_badges/pkrescueroyale_1.png deleted file mode 100644 index 0e06c80..0000000 Binary files a/res/tetrio_badges/pkrescueroyale_1.png and /dev/null differ diff --git a/res/tetrio_badges/pkrescueroyale_2.png b/res/tetrio_badges/pkrescueroyale_2.png deleted file mode 100644 index 432fe8c..0000000 Binary files a/res/tetrio_badges/pkrescueroyale_2.png and /dev/null differ diff --git a/res/tetrio_badges/pkrescueroyale_3.png b/res/tetrio_badges/pkrescueroyale_3.png deleted file mode 100644 index 8290a96..0000000 Binary files a/res/tetrio_badges/pkrescueroyale_3.png and /dev/null differ diff --git a/res/tetrio_badges/pkstarcup_1.png b/res/tetrio_badges/pkstarcup_1.png deleted file mode 100644 index f91cb97..0000000 Binary files a/res/tetrio_badges/pkstarcup_1.png and /dev/null differ diff --git a/res/tetrio_badges/pkstarcup_2.png b/res/tetrio_badges/pkstarcup_2.png deleted file mode 100644 index 0e932d7..0000000 Binary files a/res/tetrio_badges/pkstarcup_2.png and /dev/null differ diff --git a/res/tetrio_badges/pkstarcup_3.png b/res/tetrio_badges/pkstarcup_3.png deleted file mode 100644 index 4004fea..0000000 Binary files a/res/tetrio_badges/pkstarcup_3.png and /dev/null differ diff --git a/res/tetrio_badges/redgevo_1.png b/res/tetrio_badges/redgevo_1.png deleted file mode 100644 index fd03b88..0000000 Binary files a/res/tetrio_badges/redgevo_1.png and /dev/null differ diff --git a/res/tetrio_badges/redgevo_2.png b/res/tetrio_badges/redgevo_2.png deleted file mode 100644 index 28dcf72..0000000 Binary files a/res/tetrio_badges/redgevo_2.png and /dev/null differ diff --git a/res/tetrio_badges/redgevo_3.png b/res/tetrio_badges/redgevo_3.png deleted file mode 100644 index 16ddf2e..0000000 Binary files a/res/tetrio_badges/redgevo_3.png and /dev/null differ diff --git a/res/tetrio_badges/rengervl_1.png b/res/tetrio_badges/rengervl_1.png deleted file mode 100644 index f294f62..0000000 Binary files a/res/tetrio_badges/rengervl_1.png and /dev/null differ diff --git a/res/tetrio_badges/rengervl_2.png b/res/tetrio_badges/rengervl_2.png deleted file mode 100644 index dd50c6b..0000000 Binary files a/res/tetrio_badges/rengervl_2.png and /dev/null differ diff --git a/res/tetrio_badges/rengervl_3.png b/res/tetrio_badges/rengervl_3.png deleted file mode 100644 index 3e6b560..0000000 Binary files a/res/tetrio_badges/rengervl_3.png and /dev/null differ diff --git a/res/tetrio_badges/sakurablend_1.png b/res/tetrio_badges/sakurablend_1.png deleted file mode 100644 index 2501257..0000000 Binary files a/res/tetrio_badges/sakurablend_1.png and /dev/null differ diff --git a/res/tetrio_badges/sakurablend_2.png b/res/tetrio_badges/sakurablend_2.png deleted file mode 100644 index b5ce4ca..0000000 Binary files a/res/tetrio_badges/sakurablend_2.png and /dev/null differ diff --git a/res/tetrio_badges/sakurablend_3.png b/res/tetrio_badges/sakurablend_3.png deleted file mode 100644 index b15fcc0..0000000 Binary files a/res/tetrio_badges/sakurablend_3.png and /dev/null differ diff --git a/res/tetrio_badges/scuncapped_1.png b/res/tetrio_badges/scuncapped_1.png deleted file mode 100644 index 28b81b6..0000000 Binary files a/res/tetrio_badges/scuncapped_1.png and /dev/null differ diff --git a/res/tetrio_badges/scuncapped_2.png b/res/tetrio_badges/scuncapped_2.png deleted file mode 100644 index 3ac3d21..0000000 Binary files a/res/tetrio_badges/scuncapped_2.png and /dev/null differ diff --git a/res/tetrio_badges/scuncapped_3.png b/res/tetrio_badges/scuncapped_3.png deleted file mode 100644 index 4925bbd..0000000 Binary files a/res/tetrio_badges/scuncapped_3.png and /dev/null differ diff --git a/res/tetrio_badges/secretgrade.png b/res/tetrio_badges/secretgrade.png deleted file mode 100644 index 86894e4..0000000 Binary files a/res/tetrio_badges/secretgrade.png and /dev/null differ diff --git a/res/tetrio_badges/sfu_raccoon_1.png b/res/tetrio_badges/sfu_raccoon_1.png deleted file mode 100644 index 356d48b..0000000 Binary files a/res/tetrio_badges/sfu_raccoon_1.png and /dev/null differ diff --git a/res/tetrio_badges/sfu_raccoon_2.png b/res/tetrio_badges/sfu_raccoon_2.png deleted file mode 100644 index 9f98af0..0000000 Binary files a/res/tetrio_badges/sfu_raccoon_2.png and /dev/null differ diff --git a/res/tetrio_badges/sfu_raccoon_3.png b/res/tetrio_badges/sfu_raccoon_3.png deleted file mode 100644 index e4ebc90..0000000 Binary files a/res/tetrio_badges/sfu_raccoon_3.png and /dev/null differ diff --git a/res/tetrio_badges/sii_1.png b/res/tetrio_badges/sii_1.png deleted file mode 100644 index 420d137..0000000 Binary files a/res/tetrio_badges/sii_1.png and /dev/null differ diff --git a/res/tetrio_badges/sii_2.png b/res/tetrio_badges/sii_2.png deleted file mode 100644 index fb9d66a..0000000 Binary files a/res/tetrio_badges/sii_2.png and /dev/null differ diff --git a/res/tetrio_badges/sii_3.png b/res/tetrio_badges/sii_3.png deleted file mode 100644 index 491a0c8..0000000 Binary files a/res/tetrio_badges/sii_3.png and /dev/null differ diff --git a/res/tetrio_badges/sii_participation.png b/res/tetrio_badges/sii_participation.png deleted file mode 100644 index d7059e9..0000000 Binary files a/res/tetrio_badges/sii_participation.png and /dev/null differ diff --git a/res/tetrio_badges/streamersuperlobby.png b/res/tetrio_badges/streamersuperlobby.png deleted file mode 100644 index 21af4e3..0000000 Binary files a/res/tetrio_badges/streamersuperlobby.png and /dev/null differ diff --git a/res/tetrio_badges/stride_1.png b/res/tetrio_badges/stride_1.png deleted file mode 100644 index 027ed12..0000000 Binary files a/res/tetrio_badges/stride_1.png and /dev/null differ diff --git a/res/tetrio_badges/stride_2.png b/res/tetrio_badges/stride_2.png deleted file mode 100644 index 04e692a..0000000 Binary files a/res/tetrio_badges/stride_2.png and /dev/null differ diff --git a/res/tetrio_badges/stride_3.png b/res/tetrio_badges/stride_3.png deleted file mode 100644 index a4a85f8..0000000 Binary files a/res/tetrio_badges/stride_3.png and /dev/null differ diff --git a/res/tetrio_badges/stride_participation.png b/res/tetrio_badges/stride_participation.png deleted file mode 100644 index 19c1160..0000000 Binary files a/res/tetrio_badges/stride_participation.png and /dev/null differ diff --git a/res/tetrio_badges/stride_superlobby.png b/res/tetrio_badges/stride_superlobby.png deleted file mode 100644 index cd43105..0000000 Binary files a/res/tetrio_badges/stride_superlobby.png and /dev/null differ diff --git a/res/tetrio_badges/superlobby.png b/res/tetrio_badges/superlobby.png deleted file mode 100644 index a4e887c..0000000 Binary files a/res/tetrio_badges/superlobby.png and /dev/null differ diff --git a/res/tetrio_badges/superlobby2.png b/res/tetrio_badges/superlobby2.png deleted file mode 100644 index 0a69b93..0000000 Binary files a/res/tetrio_badges/superlobby2.png and /dev/null differ diff --git a/res/tetrio_badges/taws2_1.png b/res/tetrio_badges/taws2_1.png deleted file mode 100644 index 61ac03c..0000000 Binary files a/res/tetrio_badges/taws2_1.png and /dev/null differ diff --git a/res/tetrio_badges/taws2_2.png b/res/tetrio_badges/taws2_2.png deleted file mode 100644 index 33939d0..0000000 Binary files a/res/tetrio_badges/taws2_2.png and /dev/null differ diff --git a/res/tetrio_badges/taws2_3.png b/res/tetrio_badges/taws2_3.png deleted file mode 100644 index f80aa0f..0000000 Binary files a/res/tetrio_badges/taws2_3.png and /dev/null differ diff --git a/res/tetrio_badges/taws_u50_1.png b/res/tetrio_badges/taws_u50_1.png deleted file mode 100644 index ae1137f..0000000 Binary files a/res/tetrio_badges/taws_u50_1.png and /dev/null differ diff --git a/res/tetrio_badges/taws_u50_2.png b/res/tetrio_badges/taws_u50_2.png deleted file mode 100644 index 0731799..0000000 Binary files a/res/tetrio_badges/taws_u50_2.png and /dev/null differ diff --git a/res/tetrio_badges/taws_u50_3.png b/res/tetrio_badges/taws_u50_3.png deleted file mode 100644 index 0158753..0000000 Binary files a/res/tetrio_badges/taws_u50_3.png and /dev/null differ diff --git a/res/tetrio_badges/tawshdsl_capped.png b/res/tetrio_badges/tawshdsl_capped.png deleted file mode 100644 index 3a252dc..0000000 Binary files a/res/tetrio_badges/tawshdsl_capped.png and /dev/null differ diff --git a/res/tetrio_badges/tawshdsl_uncapped.png b/res/tetrio_badges/tawshdsl_uncapped.png deleted file mode 100644 index 4235c64..0000000 Binary files a/res/tetrio_badges/tawshdsl_uncapped.png and /dev/null differ diff --git a/res/tetrio_badges/tawshdslsanta.png b/res/tetrio_badges/tawshdslsanta.png deleted file mode 100644 index c30a8da..0000000 Binary files a/res/tetrio_badges/tawshdslsanta.png and /dev/null differ diff --git a/res/tetrio_badges/tawsignite_expert.png b/res/tetrio_badges/tawsignite_expert.png deleted file mode 100644 index 8394154..0000000 Binary files a/res/tetrio_badges/tawsignite_expert.png and /dev/null differ diff --git a/res/tetrio_badges/tawslg.png b/res/tetrio_badges/tawslg.png deleted file mode 100644 index b28d64c..0000000 Binary files a/res/tetrio_badges/tawslg.png and /dev/null differ diff --git a/res/tetrio_badges/tawslo.png b/res/tetrio_badges/tawslo.png deleted file mode 100644 index 3064fb3..0000000 Binary files a/res/tetrio_badges/tawslo.png and /dev/null differ diff --git a/res/tetrio_badges/tetralympic_bronze.png b/res/tetrio_badges/tetralympic_bronze.png deleted file mode 100644 index 2282959..0000000 Binary files a/res/tetrio_badges/tetralympic_bronze.png and /dev/null differ diff --git a/res/tetrio_badges/tetralympic_gold.png b/res/tetrio_badges/tetralympic_gold.png deleted file mode 100644 index 94b2267..0000000 Binary files a/res/tetrio_badges/tetralympic_gold.png and /dev/null differ diff --git a/res/tetrio_badges/tetralympic_masters.png b/res/tetrio_badges/tetralympic_masters.png deleted file mode 100644 index 089a6b3..0000000 Binary files a/res/tetrio_badges/tetralympic_masters.png and /dev/null differ diff --git a/res/tetrio_badges/tetralympic_silver.png b/res/tetrio_badges/tetralympic_silver.png deleted file mode 100644 index 5a4b0c4..0000000 Binary files a/res/tetrio_badges/tetralympic_silver.png and /dev/null differ diff --git a/res/tetrio_badges/thaitour_1.png b/res/tetrio_badges/thaitour_1.png deleted file mode 100644 index 903568d..0000000 Binary files a/res/tetrio_badges/thaitour_1.png and /dev/null differ diff --git a/res/tetrio_badges/thaitour_2.png b/res/tetrio_badges/thaitour_2.png deleted file mode 100644 index f3ffccd..0000000 Binary files a/res/tetrio_badges/thaitour_2.png and /dev/null differ diff --git a/res/tetrio_badges/thaitour_3.png b/res/tetrio_badges/thaitour_3.png deleted file mode 100644 index cec402b..0000000 Binary files a/res/tetrio_badges/thaitour_3.png and /dev/null differ diff --git a/res/tetrio_badges/tiolatam_1.png b/res/tetrio_badges/tiolatam_1.png deleted file mode 100644 index d27611f..0000000 Binary files a/res/tetrio_badges/tiolatam_1.png and /dev/null differ diff --git a/res/tetrio_badges/tiolatam_2.png b/res/tetrio_badges/tiolatam_2.png deleted file mode 100644 index 958b1df..0000000 Binary files a/res/tetrio_badges/tiolatam_2.png and /dev/null differ diff --git a/res/tetrio_badges/tiolatam_3.png b/res/tetrio_badges/tiolatam_3.png deleted file mode 100644 index 6ef1eb6..0000000 Binary files a/res/tetrio_badges/tiolatam_3.png and /dev/null differ diff --git a/res/tetrio_badges/ttsd_ou_1.png b/res/tetrio_badges/ttsd_ou_1.png deleted file mode 100644 index c937225..0000000 Binary files a/res/tetrio_badges/ttsd_ou_1.png and /dev/null differ diff --git a/res/tetrio_badges/ttsd_ou_2.png b/res/tetrio_badges/ttsd_ou_2.png deleted file mode 100644 index 1891b91..0000000 Binary files a/res/tetrio_badges/ttsd_ou_2.png and /dev/null differ diff --git a/res/tetrio_badges/ttsd_ou_3.png b/res/tetrio_badges/ttsd_ou_3.png deleted file mode 100644 index 28ea18a..0000000 Binary files a/res/tetrio_badges/ttsd_ou_3.png and /dev/null differ diff --git a/res/tetrio_badges/ttsdpf_1.png b/res/tetrio_badges/ttsdpf_1.png deleted file mode 100644 index 4749f2e..0000000 Binary files a/res/tetrio_badges/ttsdpf_1.png and /dev/null differ diff --git a/res/tetrio_badges/ttsdpf_2.png b/res/tetrio_badges/ttsdpf_2.png deleted file mode 100644 index 136d014..0000000 Binary files a/res/tetrio_badges/ttsdpf_2.png and /dev/null differ diff --git a/res/tetrio_badges/ttsdpf_3.png b/res/tetrio_badges/ttsdpf_3.png deleted file mode 100644 index ba2c977..0000000 Binary files a/res/tetrio_badges/ttsdpf_3.png and /dev/null differ diff --git a/res/tetrio_badges/ttsdtc_1.png b/res/tetrio_badges/ttsdtc_1.png deleted file mode 100644 index 3565860..0000000 Binary files a/res/tetrio_badges/ttsdtc_1.png and /dev/null differ diff --git a/res/tetrio_badges/ttsdtc_2.png b/res/tetrio_badges/ttsdtc_2.png deleted file mode 100644 index 2986832..0000000 Binary files a/res/tetrio_badges/ttsdtc_2.png and /dev/null differ diff --git a/res/tetrio_badges/ttsdtc_3.png b/res/tetrio_badges/ttsdtc_3.png deleted file mode 100644 index ce507ce..0000000 Binary files a/res/tetrio_badges/ttsdtc_3.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_1.png b/res/tetrio_badges/twc23_1.png deleted file mode 100644 index 3411c01..0000000 Binary files a/res/tetrio_badges/twc23_1.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_2.png b/res/tetrio_badges/twc23_2.png deleted file mode 100644 index ecab68c..0000000 Binary files a/res/tetrio_badges/twc23_2.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_3.png b/res/tetrio_badges/twc23_3.png deleted file mode 100644 index 4824d57..0000000 Binary files a/res/tetrio_badges/twc23_3.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_4.png b/res/tetrio_badges/twc23_4.png deleted file mode 100644 index d033d1e..0000000 Binary files a/res/tetrio_badges/twc23_4.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_honorary.png b/res/tetrio_badges/twc23_honorary.png deleted file mode 100644 index 469ce26..0000000 Binary files a/res/tetrio_badges/twc23_honorary.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_t16.png b/res/tetrio_badges/twc23_t16.png deleted file mode 100644 index 54f9bdd..0000000 Binary files a/res/tetrio_badges/twc23_t16.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_t64.png b/res/tetrio_badges/twc23_t64.png deleted file mode 100644 index 35e8198..0000000 Binary files a/res/tetrio_badges/twc23_t64.png and /dev/null differ diff --git a/res/tetrio_badges/twc23_t8.png b/res/tetrio_badges/twc23_t8.png deleted file mode 100644 index d9055ce..0000000 Binary files a/res/tetrio_badges/twc23_t8.png and /dev/null differ diff --git a/res/tetrio_badges/ubcea_1.png b/res/tetrio_badges/ubcea_1.png deleted file mode 100644 index 8076904..0000000 Binary files a/res/tetrio_badges/ubcea_1.png and /dev/null differ diff --git a/res/tetrio_badges/ubcea_2.png b/res/tetrio_badges/ubcea_2.png deleted file mode 100644 index 3f4d46e..0000000 Binary files a/res/tetrio_badges/ubcea_2.png and /dev/null differ diff --git a/res/tetrio_badges/ubcea_3.png b/res/tetrio_badges/ubcea_3.png deleted file mode 100644 index f9ff961..0000000 Binary files a/res/tetrio_badges/ubcea_3.png and /dev/null differ diff --git a/res/tetrio_badges/underdog_1.png b/res/tetrio_badges/underdog_1.png deleted file mode 100644 index 9307ecc..0000000 Binary files a/res/tetrio_badges/underdog_1.png and /dev/null differ diff --git a/res/tetrio_badges/underdog_2.png b/res/tetrio_badges/underdog_2.png deleted file mode 100644 index a2b0a74..0000000 Binary files a/res/tetrio_badges/underdog_2.png and /dev/null differ diff --git a/res/tetrio_badges/underdog_3.png b/res/tetrio_badges/underdog_3.png deleted file mode 100644 index 272fcc2..0000000 Binary files a/res/tetrio_badges/underdog_3.png and /dev/null differ diff --git a/res/tetrio_badges/underdog_predict.png b/res/tetrio_badges/underdog_predict.png deleted file mode 100644 index e66aca7..0000000 Binary files a/res/tetrio_badges/underdog_predict.png and /dev/null differ diff --git a/res/tetrio_badges/uoftflag_1.png b/res/tetrio_badges/uoftflag_1.png deleted file mode 100644 index b6a51db..0000000 Binary files a/res/tetrio_badges/uoftflag_1.png and /dev/null differ diff --git a/res/tetrio_badges/uoftflag_2.png b/res/tetrio_badges/uoftflag_2.png deleted file mode 100644 index 4113f0b..0000000 Binary files a/res/tetrio_badges/uoftflag_2.png and /dev/null differ diff --git a/res/tetrio_badges/uoftflag_3.png b/res/tetrio_badges/uoftflag_3.png deleted file mode 100644 index d26430c..0000000 Binary files a/res/tetrio_badges/uoftflag_3.png and /dev/null differ diff --git a/res/tetrio_badges/wpl_1.png b/res/tetrio_badges/wpl_1.png deleted file mode 100644 index 6be04d9..0000000 Binary files a/res/tetrio_badges/wpl_1.png and /dev/null differ diff --git a/res/tetrio_badges/wpl_2.png b/res/tetrio_badges/wpl_2.png deleted file mode 100644 index 6dc8536..0000000 Binary files a/res/tetrio_badges/wpl_2.png and /dev/null differ diff --git a/res/tetrio_badges/wpl_3.png b/res/tetrio_badges/wpl_3.png deleted file mode 100644 index 1922237..0000000 Binary files a/res/tetrio_badges/wpl_3.png and /dev/null differ diff --git a/res/tetrio_badges/wplc_1.png b/res/tetrio_badges/wplc_1.png deleted file mode 100644 index f2f3542..0000000 Binary files a/res/tetrio_badges/wplc_1.png and /dev/null differ diff --git a/res/tetrio_badges/wplc_2.png b/res/tetrio_badges/wplc_2.png deleted file mode 100644 index cdd6461..0000000 Binary files a/res/tetrio_badges/wplc_2.png and /dev/null differ diff --git a/res/tetrio_badges/wplc_3.png b/res/tetrio_badges/wplc_3.png deleted file mode 100644 index 2025666..0000000 Binary files a/res/tetrio_badges/wplc_3.png and /dev/null differ diff --git a/res/tetrio_badges/wplc_participation.png b/res/tetrio_badges/wplc_participation.png deleted file mode 100644 index 2c51e80..0000000 Binary files a/res/tetrio_badges/wplc_participation.png and /dev/null differ diff --git a/web/index.html b/web/index.html index 66c6956..cfca8f3 100644 --- a/web/index.html +++ b/web/index.html @@ -131,7 +131,7 @@ } - +
@@ -150,19 +150,20 @@ let tip = document.querySelector("#tip"); const tips = [ // Promoting Tetra Stats "native" - "Want a better perfomance?
Try out Tetra Stats \"Native\"", + "Did you know, that mobile browsers suck?
Fortunately, we have a solution", "Imagine a world, where Tetra Stats was written in JS", "Did you know, that Flutter for web sucks?", "Welcome to fullscreen canvas", // An actual tips "You can interact with most objects that have an accent color", - "Like Sheetbot graphs? Go to three dots menu → Settings → Customization", + "Like Sheetbot graphs? Go to Settings → Customization", "Click and hold on line chart graph, then start dragging to zoom in", "Discord userID ≠ Discord username. It should look like a bunch of digits", // :droidsmile: "Is she real?", + "Stats doesn't matter that much, you know...", "Check out wiki for more information" ]; tip.innerHTML = tips[Math.floor(Math.random() * tips.length)];