From ee3bbe63697181dd0c94a06fdf9ff0eae6ed9cd5 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 18 Jan 2025 01:00:46 +0300 Subject: [PATCH] A lot of things, 2.0.3 is ready to release - Can fetch S2 history - New averages - Damage calculator fix - Icon for linux runner - We can build .deb now --- .github/workflows/main.yml | 4 +- .metadata | 34 ++--- .../com/dan63/tetra_stats/MainActivity.kt | 5 + debian/debian.yaml | 3 +- debian/gui/tetra-stats.desktop | 2 +- ios/RunnerTests/RunnerTests.swift | 12 ++ lib/data_objects/tetrio_constants.dart | 76 ++++++------ lib/gen/strings.g.dart | 10 +- lib/services/sqlite_db_controller.dart | 2 +- lib/services/tetrio_crud.dart | 117 ++++++++++++------ lib/views/destination_calculator.dart | 12 +- lib/views/destination_graphs.dart | 31 +++-- lib/views/destination_settings.dart | 4 +- lib/views/main_view.dart | 3 +- lib/views/sprint_and_blitz_averages.dart | 62 +++++----- lib/widgets/beta_league_entry_thingy.dart | 12 +- lib/widgets/tl_rating_thingy.dart | 2 +- lib/widgets/tl_records_thingy.dart | 2 +- lib/widgets/tl_thingy.dart | 4 +- linux/my_application.cc | 2 + macos/RunnerTests/RunnerTests.swift | 12 ++ pubspec.lock | 16 +++ pubspec.yaml | 3 +- res/i18n/strings_ru-RU.i18n.json | 2 +- web/index.html | 2 +- 25 files changed, 273 insertions(+), 161 deletions(-) create mode 100644 android/app/src/main/kotlin/com/dan63/tetra_stats/MainActivity.kt create mode 100644 ios/RunnerTests/RunnerTests.swift create mode 100644 macos/RunnerTests/RunnerTests.swift diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 737977e..0811e00 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,6 +62,8 @@ jobs: type: 'zip' filename: TetraStats-${{github.ref_name}}-linux.zip directory: build/linux/x64/release/bundle + - name: Build .deb package + run: dart run flutter_to_debian - name: Push to Releases uses: ncipollo/release-action@v1 with: @@ -69,7 +71,7 @@ jobs: allowUpdates: true replacesArtifacts: false discussionCategory: autobuilded-releases - artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip" + artifacts: "build/linux/x64/release/bundle/TetraStats-${{github.ref_name}}-linux.zip,build/linux/x64/release/debian/*" tag: Auto-${{ github.run_number }} body: Build with GitHub Action workflow token: ${{ secrets.TOKEN }} diff --git a/.metadata b/.metadata index f25b3d4..9d32c61 100644 --- a/.metadata +++ b/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - channel: stable + revision: "b0850beeb25f6d5b10426284f506557f66181b36" + channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 - platform: android - create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 - platform: ios - create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 - platform: linux - create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 - platform: macos - create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 - platform: web - create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 - platform: windows - create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf - base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 # User provided section diff --git a/android/app/src/main/kotlin/com/dan63/tetra_stats/MainActivity.kt b/android/app/src/main/kotlin/com/dan63/tetra_stats/MainActivity.kt new file mode 100644 index 0000000..c81e690 --- /dev/null +++ b/android/app/src/main/kotlin/com/dan63/tetra_stats/MainActivity.kt @@ -0,0 +1,5 @@ +package com.dan63.tetra_stats + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/debian/debian.yaml b/debian/debian.yaml index dc01608..3d8d822 100644 --- a/debian/debian.yaml +++ b/debian/debian.yaml @@ -2,10 +2,11 @@ flutter_app: command: tetra_stats arch: x64 parent: /usr/local/lib + nonInteractive: true control: Package: tetra-stats - Version: 0.2.0 + Version: 2.0.3 Architecture: amd64 Essential: no Priority: optional diff --git a/debian/gui/tetra-stats.desktop b/debian/gui/tetra-stats.desktop index edf46b1..4d0e09a 100644 --- a/debian/gui/tetra-stats.desktop +++ b/debian/gui/tetra-stats.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=0.2.0 +Version=2.0.3 Name=Tetra Stats GenericName=Tetra Stats Comment=Track your and other player stats in TETR.IO diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index 7a031f5..e72f47d 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; const int currentSeason = 2; -final DateTime sprintAndBlitzRelevance = DateTime(2024, 8, 25); +final DateTime sprintAndBlitzRelevance = DateTime(2025, 1, 16); const double noTrRd = 60.9; const double apmWeight = 1; const double ppsWeight = 45; @@ -210,46 +210,46 @@ const List achievementColors = [ ]; 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), + // based on https://discord.com/channels/673303546107658242/1260605501754839060/1329448681539244094 + 'x+': Duration(seconds: 19, milliseconds: 223), + 'x': Duration(seconds: 24, milliseconds: 832), + 'u': Duration(seconds: 32, milliseconds: 586), + 'ss': Duration(seconds: 40, milliseconds: 011), + 's+': Duration(seconds: 47, milliseconds: 963), + 's': Duration(seconds: 54, milliseconds: 413), + 's-': Duration(seconds: 61, milliseconds: 740), + 'a+': Duration(seconds: 70, milliseconds: 101), + 'a': Duration(seconds: 73, milliseconds: 294), + 'a-': Duration(seconds: 81, milliseconds: 773), + 'b+': Duration(seconds: 88, milliseconds: 647), + 'b': Duration(seconds: 97, milliseconds: 699), + 'b-': Duration(seconds: 105, milliseconds: 721), + 'c+': Duration(seconds: 113, milliseconds: 229), + 'c': Duration(seconds: 124, milliseconds: 740), + 'c-': Duration(seconds: 129, milliseconds: 382), + 'd+': Duration(seconds: 138, milliseconds: 947), + 'd': Duration(seconds: 155, milliseconds: 190), }; 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, + 'x+': 886046, + 'x': 631014, + 'u': 428799, + 'ss': 296430, + 's+': 212237, + 's': 157234, + 's-': 122791, + 'a+': 103031, + 'a': 90174, + 'a-': 73474, + 'b+': 60655, + 'b': 52463, + 'b-': 43877, + 'c+': 36594, + 'c': 34014, + 'c-': 29613, + 'd+': 31521, + 'd': 23437, }; List seasonStarts = [ diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 1565380..95aff97 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 3 /// Strings: 2295 (765 per locale) /// -/// Built on 2024-12-31 at 17:29 UTC +/// Built on 2025-01-14 at 21:20 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -707,7 +707,7 @@ class _StringsGraphsDestinationEn { final Translations _root; // ignore: unused_field // Translations - String get fetchAndsaveTLHistory => 'Get player history'; + String get fetchAndsaveTLHistory => 'Fetch 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'; @@ -2247,7 +2247,7 @@ class _StringsGraphsDestinationRuRu implements _StringsGraphsDestinationEn { @override final _StringsRuRu _root; // ignore: unused_field // Translations - @override String get fetchAndsaveTLHistory => 'Получить историю игрока'; + @override String get fetchAndsaveTLHistory => 'Получить историю'; @override String get fetchAndSaveOldTLmatches => 'Получить историю матчей Тетра Лиги'; @override String fetchAndsaveTLHistoryResult({required Object number}) => '${number} состояний было найдено'; @override String fetchAndSaveOldTLmatchesResult({required Object number}) => '${number} матчей было найдено'; @@ -4925,7 +4925,7 @@ extension on Translations { case 'actions.ok': return 'OK'; case 'actions.apply': return 'Apply'; case 'actions.refresh': return 'Refresh'; - case 'graphsDestination.fetchAndsaveTLHistory': return 'Get player history'; + case 'graphsDestination.fetchAndsaveTLHistory': return 'Fetch 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'; @@ -5739,7 +5739,7 @@ extension on _StringsRuRu { case 'actions.ok': return 'ОК'; case 'actions.apply': return 'Применить'; case 'actions.refresh': return 'Обновить'; - case 'graphsDestination.fetchAndsaveTLHistory': 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} матчей было найдено'; diff --git a/lib/services/sqlite_db_controller.dart b/lib/services/sqlite_db_controller.dart index 2d2a87a..7720a1a 100644 --- a/lib/services/sqlite_db_controller.dart +++ b/lib/services/sqlite_db_controller.dart @@ -88,7 +88,7 @@ class DB { } Future checkImportingDB(File db) async { - final newDB = await openDatabase(db.path); + final newDB = await openDatabase(db.path); // TODO: Maybe i should use arguments, that this method provides? 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; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 1d2f170..d059164 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -4,9 +4,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; +import 'dart:math'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:tetra_stats/data_objects/beta_record.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'; @@ -24,8 +26,6 @@ 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'; @@ -372,7 +372,7 @@ class TetrioService extends DB { dbPath = join(docsPath.path, dbName); } var dbFile = File(dbPath); - var dbSize = (await dbFile.stat()).size; + var dbSize = kIsWeb ? -1 : (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); @@ -673,8 +673,7 @@ 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, int season) async { - // TODO: find le way to get season 2 history + Future> fetchAndsaveS1TLHistory(String id) async { Uri url; if (kIsWeb) { url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "TLHistory", "user": id}); @@ -745,6 +744,70 @@ class TetrioService extends DB { } } + Future> fetchAndsaveS2TLHistory(String id) async { + final db = getDatabaseOrThrow(); + List records = []; + int entries = 100; + String? prisecter; + while (entries > 0){ + TetraLeagueBetaStream stream = await fetchTLStream(id, prisecter: prisecter); + if (stream.records.isEmpty) break; + records.addAll(stream.records); + prisecter = stream.records.last.prisecter.toString(); + entries = stream.records.length; + } + //TetraLeague currentState = await fetchTLSummary(id); + List states = []; + //states.add(currentState); + int gp = 0; + int gw = 0; + List last10apm = []; + List last10pps = []; + List last10vs = []; + int bestRankIndex = -1; // -1 - Z; 0 - D, 1 - D+ ... 18 - X+ + Batch batch = db.batch(); + for (BetaRecord match in records.reversed){ + gp++; + if (match.extras.result.contains("victory")) gw++; + last10apm.add(match.results.leaderboard.firstWhere((e) => e.id == id).stats.apm); + if (last10apm.length > 10) last10apm.removeAt(0); + last10pps.add(match.results.leaderboard.firstWhere((e) => e.id == id).stats.pps); + if (last10pps.length > 10) last10pps.removeAt(0); + last10vs.add(match.results.leaderboard.firstWhere((e) => e.id == id).stats.vs); + if (last10vs.length > 10) last10vs.removeAt(0); + double apm = last10apm.reduce((v, e) => v + e) / last10apm.length; + double pps = last10pps.reduce((v, e) => v + e) / last10pps.length; + double vs = last10vs.reduce((v, e) => v + e) / last10vs.length; + TetraLeague state = TetraLeague( + id: id, + timestamp: match.ts, + gamesPlayed: gp, + gamesWon: gw, + bestRank: bestRankIndex != -1 ? ranks[bestRankIndex] : "z", + decaying: false, + tr: match.extras.league[id]?[1]?.tr??-1.0, + glicko: match.extras.league[id]?[1]?.glicko, + rd: match.extras.league[id]?[1]?.rd, + gxe: match.extras.league[id]?[1]?.glicko != null ? 10000 / (1 + pow(10, (((1500 - match.extras.league[id]![1]!.glicko) * pi / sqrt(3 * pow(ln10, 2) * pow(match.extras.league[id]![1]!.rd, 2) + 2500 * (64 * pow(pi, 2) + 147 * pow(ln10, 2))))))) / 100 : -1, + rank: match.extras.league[id]?[1]?.rank??"z", + percentileRank: match.extras.league[id]?[1]?.rank??"z", + percentile: match.extras.league[id]?[1]?.rank != null ? rankCutoffs[match.extras.league[id]![1]!.rank]! : -1, + standing: match.extras.league[id]?[1]?.placement??-1, + standingLocal: -1, + nextAt: -1, + prevAt: -1, + apm: apm, + pps: pps, + vs: vs, + season: currentSeason + ); + states.add(state); + batch.insert(tetrioLeagueTable, state.toJson(), conflictAlgorithm: ConflictAlgorithm.replace); + } + batch.commit(); + return states; + } + /// Docs later Future fetchAndSaveOldTLmatches(String userID) async { Uri url; @@ -1129,38 +1192,29 @@ class TetrioService extends DB { await db.delete(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [rID]); } - /// Retrieves Blitz, 40 Lines and Zen records for a given [userID] from Tetra Channel api. Returns `UserRecords`. - /// Throws an exception if fails to retrieve. - Future fetchRecords(String userID) async { - UserRecords? cached = _cache.get(userID, UserRecords); + Future fetchTLSummary(String id) async { + TetraLeague? cached = _cache.get(id, TetraLeague); if (cached != null) return cached; - + Uri url; if (kIsWeb) { - url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "tetrioUserRecords", "user": userID.toLowerCase().trim()}); + url = Uri.https(webVersionDomain, 'oskware_bridge.php', {"endpoint": "Summaries", "id": id}); } else { - url = Uri.https('ch.tetr.io', 'api/users/${userID.toLowerCase().trim()}/records'); + url = Uri.https('ch.tetr.io', 'api/users/$id/summaries/league'); } + try{ final response = await client.get(url); switch (response.statusCode) { case 200: if (jsonDecode(response.body)['success']) { - Map jsonRecords = jsonDecode(response.body); - var sprint = jsonRecords['data']['records']['40l']['record'] != null - ? RecordSingle.fromJson(jsonRecords['data']['records']['40l']['record'], jsonRecords['data']['records']['40l']['rank'], jsonRecords['data']['records']['40l']['rank_local']) - : null; - var blitz = jsonRecords['data']['records']['blitz']['record'] != null - ? RecordSingle.fromJson(jsonRecords['data']['records']['blitz']['record'], jsonRecords['data']['records']['blitz']['rank'], jsonRecords['data']['records']['blitz']['rank_local']) - : null; - var zen = TetrioZen.fromJson(jsonRecords['data']['zen']); - UserRecords result = UserRecords(userID, sprint, blitz, zen); - _cache.store(result, jsonDecode(response.body)['cache']['cached_until']); - developer.log("fetchRecords: $userID records retrieved and cached", name: "services/tetrio_crud"); - return result; + developer.log("fetchTLSummary: $id TL state retrieved and cached", name: "services/tetrio_crud"); + TetraLeague league = TetraLeague.fromJson(jsonDecode(response.body)['data'], DateTime.now(), currentSeason, id); + _cache.store(league, jsonDecode(response.body)['cache']['cached_until']); + return league; } else { - developer.log("fetchRecords User dosen't exist", name: "services/tetrio_crud", error: response.body); + developer.log("fetchTLSummary: User dosen't exist", name: "services/tetrio_crud", error: response.body); throw TetrioPlayerNotExist(); } case 403: @@ -1175,7 +1229,7 @@ class TetrioService extends DB { case 504: throw TetrioInternalProblem(); default: - developer.log("fetchRecords Failed to fetch records", name: "services/tetrio_crud", error: response.statusCode); + developer.log("fetchTLSummary Failed to fetch TL state", name: "services/tetrio_crud", error: response.statusCode); throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } } on http.ClientException catch (e, s) { @@ -1427,15 +1481,4 @@ class TetrioService extends DB { } return data; } - - // Future fetchTracked() async { - // for (String userID in (await getAllPlayerToTrack())) { - // TetrioPlayer player = await fetchPlayer(userID); - // storeState(player); - // sleep(Durations.extralong4); - // TetraLeagueBetaStream matches = await fetchTLStream(userID); - // saveTLMatchesFromStream(matches); - // sleep(Durations.extralong4); - // } - // } } diff --git a/lib/views/destination_calculator.dart b/lib/views/destination_calculator.dart index 37b1a6f..1ea69d5 100644 --- a/lib/views/destination_calculator.dart +++ b/lib/views/destination_calculator.dart @@ -76,7 +76,7 @@ class ClearData{ 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))]; + if (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) { @@ -166,7 +166,7 @@ class _DestinationCalculatorState extends State { List clears = []; Map customClearsChoice = { t.calcDestination.noSpinClears: 5, - t.calcDestination.spins: 5 + t.stats.spins: 5 }; int idCounter = 0; Rules rules = Rules(); @@ -400,7 +400,13 @@ class _DestinationCalculatorState extends State { ), 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)); + clears.add(ClearData( + "${key == t.stats.spins ? "${t.stats.spin} " : ""}${clearNames[min(customClearsChoice[key]!, clearNames.length-1)]} (${customClearsChoice[key]!} ${t.stats.lines})", + key == t.stats.spins ? Lineclears.TSPIN_PENTA : Lineclears.PENTA, + customClearsChoice[key]!, + false, + key == t.stats.spins).cloneWith(idCounter) + ); }); idCounter++; }, diff --git a/lib/views/destination_graphs.dart b/lib/views/destination_graphs.dart index 1a932e2..7fc7dd3 100644 --- a/lib/views/destination_graphs.dart +++ b/lib/views/destination_graphs.dart @@ -51,6 +51,7 @@ class _DestinationGraphsState extends State { ValueNotifier historyPlayerUsername = ValueNotifier(""); ValueNotifier historyPlayerAvatarRevizion = ValueNotifier(""); List excludeRanks = []; + late Future>>> playerHistory = getHistoryData(fetchData); late Future> futureLeague = getTetraLeagueData(_Xchart, Ychart); String searchLeague = ""; int? TLstatePlayers; @@ -143,9 +144,11 @@ class _DestinationGraphsState extends State { } Future>>> getHistoryData(bool fetchHistory) async { + var playerID = (await teto.fetchPlayer(widget.searchFor)).userId; if(fetchHistory){ try{ - var history = await teto.fetchAndsaveTLHistory(widget.searchFor, 1); + //var history = await Future.wait([teto.fetchAndsaveS1TLHistory(widget.searchFor), teto.fetchAndsaveS2TLHistory(widget.searchFor)]); // S1 history unavaliable because of certificate issue on p1nkl0bst3r side + var history = await teto.fetchAndsaveS2TLHistory(playerID); 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))); @@ -159,19 +162,19 @@ class _DestinationGraphsState extends State { } List> states = await Future.wait>([ - teto.getStates(widget.searchFor, season: 1), teto.getStates(widget.searchFor, season: 2), + teto.getStates(playerID, season: 1), teto.getStates(playerID, 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())]; + for (var stat in Stats.values) statsMap[stat] = [for (var tl in states[season]) if (tl.getStatByEnum(stat) != null && tl.getStatByEnum(stat) != -1.00) _HistoryChartSpot(tl.timestamp, tl.gamesPlayed, tl.rank, tl.getStatByEnum(stat)!.toDouble())]; historyData[season] = statsMap; } } fetchData = false; - historyPlayerUsername.value = await teto.getNicknameByID(widget.searchFor); + historyPlayerUsername.value = await teto.getNicknameByID(playerID); return historyData; } @@ -234,7 +237,7 @@ class _DestinationGraphsState extends State { trendlines:[ Trendline( isVisible: _smooth, - period: (selectedGraph.length/175).floor(), + period: (selectedGraph.length/100).floor(), type: TrendlineType.movingAverage, color: Theme.of(context).colorScheme.primary) ], @@ -250,7 +253,7 @@ class _DestinationGraphsState extends State { trendlines:[ Trendline( isVisible: _smooth, - period: (selectedGraph.length/175).floor(), + period: (selectedGraph.length/100).floor(), type: TrendlineType.movingAverage, color: Theme.of(context).colorScheme.primary) ], @@ -532,7 +535,16 @@ class _DestinationGraphsState extends State { ); }); }, icon: Icon(Icons.filter_alt)), - IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,) + IconButton(onPressed: () => _zoomPanBehavior.reset(), icon: const Icon(Icons.refresh), alignment: Alignment.center,), + if (graph == Graph.history) ElevatedButton.icon( + onPressed: (){ + setState(() { + fetchData = true; + }); + }, + label: Text(t.graphsDestination.fetchAndsaveTLHistory), + icon: Icon(Icons.download), + ) ], ), ), @@ -581,6 +593,11 @@ class _DestinationGraphsState extends State { ), ); } + + void markNeedsBuild() { + + } + } class _HistoryChartSpot{ diff --git a/lib/views/destination_settings.dart b/lib/views/destination_settings.dart index be1b18c..440e390 100644 --- a/lib/views/destination_settings.dart +++ b/lib/views/destination_settings.dart @@ -447,7 +447,7 @@ class _DestinationSettings extends State with SingleTickerP 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: "${snapshot.data!.$1 == -1 ? "???" : 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"), @@ -457,7 +457,7 @@ class _DestinationSettings extends State with SingleTickerP ) ); } - if (snapshot.hasError){ return FutureError(snapshot); } + if (snapshot.hasError){ return SizedBox(height: 500.0, child: FutureError(snapshot)); } } return Text("huh?"); } diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 1763532..2ecad57 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -41,7 +41,6 @@ Future getData(String searchFor, {bool withHistory = false}) async }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()); } @@ -62,7 +61,7 @@ Future getData(String searchFor, {bool withHistory = false}) async cutoffs = requests.elementAtOrNull(2); averages = requests.elementAtOrNull(3); - if(withHistory) await teto.fetchAndsaveTLHistory(player.userId, 1); // Retrieve if needed + if(withHistory) await teto.fetchAndsaveS1TLHistory(player.userId); // Retrieve if needed } on Exception catch (e) { return FetchResults(false, null, [], null, null, null, null, null, false, e); } diff --git a/lib/views/sprint_and_blitz_averages.dart b/lib/views/sprint_and_blitz_averages.dart index df115fa..4cd76a5 100644 --- a/lib/views/sprint_and_blitz_averages.dart +++ b/lib/views/sprint_and_blitz_averages.dart @@ -57,44 +57,38 @@ class SprintAndBlitzState extends State { constraints: const BoxConstraints(maxWidth: 600), child: SingleChildScrollView( padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + border: TableBorder.all(color: Colors.grey.shade900), + columnWidths: const {0: FixedColumnWidth(48)}, children: [ - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - border: TableBorder.all(color: Colors.grey.shade900), - columnWidths: const {0: FixedColumnWidth(48)}, + TableRow( 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: const EdgeInsets.only(right: 8.0), - 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.gamemodes["blitz"]!, textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white)), - ), - ] + Text(t.rank, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + 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)), ), - for (MapEntry sprintEntry in sprintAverages.entries) TableRow( - decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[sprintEntry.key]!.withAlpha(100), rankColors[sprintEntry.key]!.withAlpha(200)])), - 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/${sprintEntry.key}.png", height: 48)), - Padding( - padding: const EdgeInsets.only(right: 8.0), - 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), - child: Text(NumberFormat.decimalPattern().format(blitzAverages[sprintEntry.key]), 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), + 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)), + ), + ] ), - Text(t.sprintAndBlitsRelevance(date: dateFormat.format(DateTime(2024, 8, 25)))) + for (MapEntry sprintEntry in sprintAverages.entries) TableRow( + decoration: BoxDecoration(gradient: LinearGradient(colors: [rankColors[sprintEntry.key]!.withAlpha(100), rankColors[sprintEntry.key]!.withAlpha(200)])), + 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/${sprintEntry.key}.png", height: 48)), + Padding( + padding: const EdgeInsets.only(right: 8.0), + 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), + child: Text(NumberFormat.decimalPattern().format(blitzAverages[sprintEntry.key]), textAlign: TextAlign.right, style: TextStyle(fontFamily: bigScreen ? "Eurostile Round" : "Eurostile Round Condensed", fontSize: 28, fontWeight: FontWeight.w500, color: Colors.white, shadows: textShadow)), + ), + ] + ) ], ), ), diff --git a/lib/widgets/beta_league_entry_thingy.dart b/lib/widgets/beta_league_entry_thingy.dart index 8b989eb..f25f35f 100644 --- a/lib/widgets/beta_league_entry_thingy.dart +++ b/lib/widgets/beta_league_entry_thingy.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.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'; @@ -9,8 +10,8 @@ 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); + final bool wide; + const BetaLeagueEntryThingy(this.record, this.userID, this.wide); TextSpan matchResult(String result){ return switch(result){ @@ -57,6 +58,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{ @override Widget build(BuildContext context) { + NumberFormat diff = wide ? fDiff : comparef2; 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; @@ -88,7 +90,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{ text: ", ${timestamp(record.ts)}\n" ), TextSpan( - text: deltaTR != null ? "${fDiff.format(deltaTR)} TR" : "??? TR", + text: deltaTR != null ? "${diff.format(deltaTR)} TR" : "??? TR", style: TextStyle( color: deltaColor(deltaTR) ) @@ -97,7 +99,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{ text: ", " ), TextSpan( - text: deltaGlicko != null ? "${fDiff.format(deltaGlicko)} Glicko" : "??? Glicko", + text: deltaGlicko != null ? "${diff.format(deltaGlicko)} Glicko" : "??? Glicko", style: TextStyle( color: deltaColor(deltaGlicko) ) @@ -106,7 +108,7 @@ class BetaLeagueEntryThingy extends StatelessWidget{ text: ", " ), TextSpan( - text: deltaRD != null ? "${fDiff.format(deltaRD)} RD" : "??? RD", + text: deltaRD != null ? "${diff.format(deltaRD)} RD" : "??? RD", style: TextStyle( color: Colors.grey ) diff --git a/lib/widgets/tl_rating_thingy.dart b/lib/widgets/tl_rating_thingy.dart index 4ab80a9..25bde0c 100644 --- a/lib/widgets/tl_rating_thingy.dart +++ b/lib/widgets/tl_rating_thingy.dart @@ -65,7 +65,7 @@ class TLRatingThingy extends StatelessWidget{ } : [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) RichText( + if (oldTl != null && oldTl!.tr != -1.0) RichText( textAlign: TextAlign.center, softWrap: true, text: TextSpan( diff --git a/lib/widgets/tl_records_thingy.dart b/lib/widgets/tl_records_thingy.dart index 26e33ab..d1aefb2 100644 --- a/lib/widgets/tl_records_thingy.dart +++ b/lib/widgets/tl_records_thingy.dart @@ -103,7 +103,7 @@ class _TLRecordsState extends State { ), ), itemBuilder: (BuildContext context, int index){ - return BetaLeagueEntryThingy(records[index], widget.userID); + return BetaLeagueEntryThingy(records[index], widget.userID, MediaQuery.of(context).size.width >= 768.0); } ), ); diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index b676a55..a36fe08 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -47,7 +47,7 @@ class TetraLeagueThingy extends StatelessWidget{ 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 (toCompare != null) Text(toCompare!.gxe != -1 ? " (${comparef.format(league.gxe-toCompare!.gxe)})" : "(---)", textAlign: TextAlign.right, style: TextStyle(fontSize: width > 768.0 ? 21 : 18, color: toCompare!.gxe != -1 ? getDifferenceColor(league.gxe-toCompare!.gxe) : Colors.grey)), 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)) ]), ]; @@ -60,7 +60,7 @@ class TetraLeagueThingy extends StatelessWidget{ child: Column( children: [ TLRatingThingy(userID: league.id, tlData: league, oldTl: toCompare, showPositions: true), - if (league.gamesPlayed > 9) TLProgress( + if (league.gamesPlayed > 9 && league.percentileRank != "z") 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, diff --git a/linux/my_application.cc b/linux/my_application.cc index b9c26db..9dadbc7 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -19,6 +19,8 @@ static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + gtk_window_set_icon_from_file(GTK_WINDOW(window),"res/icons/app.png",NULL); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock index 82d38d7..7f01a3e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -328,6 +328,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_to_debian: + dependency: "direct main" + description: + name: flutter_to_debian + sha256: d23534407334b331ce20fbaa8395b9ecc255d0c047136b8998715f36933ee696 + url: "https://pub.dev" + source: hosted + version: "2.0.2" flutter_web_plugins: dependency: transitive description: flutter @@ -509,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + mime_type: + dependency: transitive + description: + name: mime_type + sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb + url: "https://pub.dev" + source: hosted + version: "1.0.1" node_preamble: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 867915a..4bfdc9a 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: 2.0.2+43 +version: 2.0.3+44 environment: sdk: '>=3.0.0' @@ -46,6 +46,7 @@ dependencies: flutter_layout_grid: ^2.0.0 go_router: ^13.0.0 syncfusion_flutter_charts: ^24.2.9 + flutter_to_debian: ^2.0.2 dev_dependencies: flutter_test: diff --git a/res/i18n/strings_ru-RU.i18n.json b/res/i18n/strings_ru-RU.i18n.json index 1acb9aa..c24f219 100644 --- a/res/i18n/strings_ru-RU.i18n.json +++ b/res/i18n/strings_ru-RU.i18n.json @@ -172,7 +172,7 @@ "refresh": "Обновить" }, "graphsDestination": { - "fetchAndsaveTLHistory": "Получить историю игрока", + "fetchAndsaveTLHistory": "Получить историю", "fetchAndSaveOldTLmatches": "Получить историю матчей Тетра Лиги", "fetchAndsaveTLHistoryResult": "${number} состояний было найдено", "fetchAndSaveOldTLmatchesResult": "${number} матчей было найдено", diff --git a/web/index.html b/web/index.html index c9472ad..2d8b016 100644 --- a/web/index.html +++ b/web/index.html @@ -131,7 +131,7 @@ } - +