From 8770d5dca8d6f066dcad8c0f516d81e35aea6d80 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Fri, 23 Jun 2023 21:38:15 +0300 Subject: [PATCH] TL records are now stored in the database for tracked players only. Also fixed variety of shit --- README.md | 6 +- android/build.gradle | 62 ++++++------- lib/data_objects/tetrio.dart | 43 +++++---- lib/services/sqlite_db_controller.dart | 1 + lib/services/tetrio_crud.dart | 40 ++++++++- lib/views/compare_view.dart | 3 +- lib/views/main_view.dart | 115 ++++++++++++++++++------- lib/views/states_view.dart | 2 +- lib/views/tl_match_view.dart | 25 +++--- lib/widgets/tl_thingy.dart | 2 +- lib/widgets/user_thingy.dart | 4 +- pubspec.yaml | 5 ++ res/tetrio_badges/ggc_1.png | Bin 0 -> 8170 bytes res/tetrio_badges/ggc_3.png | Bin 0 -> 6322 bytes res/tetrio_badges/hdoxii_1.png | Bin 0 -> 6352 bytes res/tetrio_badges/hdoxii_2.png | Bin 0 -> 5028 bytes res/tetrio_badges/hdoxii_3.png | Bin 0 -> 5044 bytes 17 files changed, 200 insertions(+), 108 deletions(-) create mode 100644 res/tetrio_badges/ggc_1.png create mode 100644 res/tetrio_badges/ggc_3.png create mode 100644 res/tetrio_badges/hdoxii_1.png create mode 100644 res/tetrio_badges/hdoxii_2.png create mode 100644 res/tetrio_badges/hdoxii_3.png diff --git a/README.md b/README.md index 5a85a5a..4186b90 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ - ~~Ability to compare 2 players~~ *v0.1.0, we are here* - ~~Stats Calculator~~ - ~~Ability to compare player with himself in past~~ -- ~~Tetra League matches history~~ *dev build are here* -- ~~Tetra League historic charts for tracked players~~ (bit mess idk) +- ~~Tetra League matches history~~ +- ~~Tetra League historic charts for tracked players~~ *dev build are here* - Better UI with delta and hints for stats *that will be v0.2.0* - Ability to compare player with APM-PPS-VS stats - Ability to fetch Tetra League leaderboard @@ -22,7 +22,7 @@ - UI Animations - i18n, EN and RU locales - Talk with osk about CORS and EndContext in TL matches -- RELEASE ??? +- RELEASE ??? *that will be v1.0.0* --- diff --git a/android/build.gradle b/android/build.gradle index 6b815dd..713d7f6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,31 +1,31 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index e9eacf9..ba3d586 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -430,10 +430,10 @@ class Handling { Map toJson() { final Map data = {}; - data['arr'] = arr; - data['das'] = das; - data['dcd'] = dcd; - data['sdf'] = sdf; + data['arr'] = arr.toDouble(); + data['das'] = das.toDouble(); + data['dcd'] = dcd.toDouble(); + data['sdf'] = sdf.toDouble(); data['safelock'] = safeLock; data['cancel'] = cancel; return data; @@ -526,24 +526,24 @@ class Playstyle { class TetraLeagueAlphaStream{ late String userId; - List? records; + late List records; - TetraLeagueAlphaStream({required this.userId, this.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));} + for (var value in json) {records.add(TetraLeagueAlphaRecord.fromJson(value));} } } class TetraLeagueAlphaRecord{ late String replayId; late String ownId; - DateTime? timestamp; + late DateTime timestamp; late List endContext; - TetraLeagueAlphaRecord({required this.replayId, required this.ownId, this.timestamp, required this.endContext}); + TetraLeagueAlphaRecord({required this.replayId, required this.ownId, required this.timestamp, required this.endContext}); TetraLeagueAlphaRecord.fromJson(Map json) { ownId = json['_id']; @@ -561,6 +561,10 @@ class TetraLeagueAlphaRecord{ data['ts'] = timestamp; return data; } + + @override + bool operator ==(covariant TetraLeagueAlphaRecord other) => ownId == other.ownId; + @override String toString() { return "TetraLeagueAlphaRecord: ${endContext.first.userId} vs ${endContext.last.userId}"; @@ -577,11 +581,11 @@ class EndContextMulti { late int points; late int wins; late double secondary; - late List secondaryTracking; + late List secondaryTracking; late double tertiary; - late List tertiaryTracking; + late List tertiaryTracking; late double extra; - late List extraTracking; + late List extraTracking; late bool success; late NerdStats nerdStats; late EstTr estTr; @@ -616,10 +620,10 @@ class EndContextMulti { points = json['points']['primary']; secondary = json['points']['secondary'].toDouble(); tertiary = json['points']['tertiary'].toDouble(); - secondaryTracking = json['points']['secondaryAvgTracking'].cast(); - tertiaryTracking = json['points']['tertiaryAvgTracking'].cast(); + secondaryTracking = json['points']['secondaryAvgTracking'].map((e) => e.toDouble()).toList(); + tertiaryTracking = json['points']['tertiaryAvgTracking'].map((e) => e.toDouble()).toList(); extra = json['points']['extra']['vs'].toDouble(); - extraTracking = json['points']['extraAvgTracking']['aggregatestats___vsscore'].cast(); + extraTracking = json['points']['extraAvgTracking']['aggregatestats___vsscore'].map((e) => e.toDouble()).toList(); nerdStats = NerdStats(secondary, tertiary, extra); estTr = EstTr(secondary, tertiary, extra, noTrRd, nerdStats.app, nerdStats.dss, nerdStats.dsp, nerdStats.gbe); playstyle = Playstyle(secondary, tertiary, nerdStats.app, nerdStats.vsapm, nerdStats.dsp, nerdStats.gbe, estTr.srarea, estTr.statrank); @@ -627,19 +631,14 @@ class EndContextMulti { Map toJson() { final Map data = {}; - data['user']['_id'] = userId; - data['user']['username'] = username; + 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; - data['points']['secondary'] = secondary; - data['points']['tertiary'] = tertiary; - data['points']['extra']['vs'] = extra; - data['points']['extraAvgTracking']['aggregatestats___vsscore'] = extraTracking; + 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/services/sqlite_db_controller.dart b/lib/services/sqlite_db_controller.dart index 0c878b0..2daa851 100644 --- a/lib/services/sqlite_db_controller.dart +++ b/lib/services/sqlite_db_controller.dart @@ -20,6 +20,7 @@ class DB { _db = db; await db.execute(createTetrioUsersTable); await db.execute(createTetrioUsersToTrack); + await db.execute(createTetrioTLRecordsTable); } on MissingPlatformDirectoryException { throw UnableToGetDocuments(); } diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 69c9e3c..577bfbf 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -9,10 +9,16 @@ import 'package:tetra_stats/data_objects/tetrio.dart'; const String dbName = "TetraStats.db"; const String tetrioUsersTable = "tetrioUsers"; const String tetrioUsersToTrackTable = "tetrioUsersToTrack"; -const String tetraLeagueMatchesTable = "tetraLeagueMatches"; +const String tetraLeagueMatchesTable = "tetrioAlphaLeagueMathces"; const String idCol = "id"; +const String replayID = "replayId"; const String nickCol = "nickname"; +const String timestamp = "timestamp"; +const String endContext1 = "endContext1"; +const String endContext2 = "endContext2"; const String statesCol = "jsonStates"; +const String player1id = "player1id"; +const String player2id = "player2id"; const String createTetrioUsersTable = ''' CREATE TABLE IF NOT EXISTS "tetrioUsers" ( "id" TEXT UNIQUE, @@ -26,6 +32,17 @@ const String createTetrioUsersToTrack = ''' PRIMARY KEY("ID") ) '''; +const String createTetrioTLRecordsTable = ''' + CREATE TABLE IF NOT EXISTS "tetrioAlphaLeagueMathces" ( + "id" TEXT, + "replayId" TEXT, + "player1id" TEXT, + "player2id" TEXT, + "timestamp" TEXT, + "endContext1" TEXT, + "endContext2" TEXT + ) +'''; class TetrioService extends DB { Map> _players = {}; @@ -108,6 +125,27 @@ class TetrioService extends DB { } } + Future saveTLMatchesFromStream(TetraLeagueAlphaStream stream) async { + ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + for (TetraLeagueAlphaRecord match in stream.records) { + final results = await db.query(tetraLeagueMatchesTable, where: '$idCol = ?', whereArgs: [match.ownId]); + if (results.isNotEmpty) continue; + db.insert(tetraLeagueMatchesTable, {idCol: match.ownId, replayID: match.replayId, timestamp: match.timestamp.toString(), player1id: match.endContext.first.userId, player2id: match.endContext.last.userId, endContext1: jsonEncode(match.endContext.first.toJson()), endContext2: jsonEncode(match.endContext.last.toJson())}); + } + } + + Future> getTLMatchesbyPlayerID(String playerID) async { + ensureDbIsOpen(); + final db = getDatabaseOrThrow(); + List matches = []; + final results = await db.query(tetraLeagueMatchesTable, where: '($player1id = ?) OR ($player2id = ?)', whereArgs: [playerID, playerID]); + for (var match in results){ + matches.add(TetraLeagueAlphaRecord(ownId: match[idCol].toString(), replayId: match[replayID].toString(), timestamp: DateTime.parse(match[timestamp].toString()), endContext:[EndContextMulti.fromJson(jsonDecode(match[endContext1].toString())), EndContextMulti.fromJson(jsonDecode(match[endContext2].toString()))])); + } + return matches; + } + Future> fetchRecords(String userID) async { try{ var cached = _recordsCache.entries.firstWhere((element) => element.value['user'] == userID); diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 40448fc..9fc6cb9 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -1194,8 +1194,9 @@ class CompareRegTimeThingy extends StatelessWidget { String verdict(DateTime? greenSide, DateTime? redSide) { var f = NumberFormat("#,### days later;#,### days before"); String result = "---"; - if (greenSide != null && redSide != null) + if (greenSide != null && redSide != null) { result = f.format(greenSide.difference(redSide).inDays); + } return result; } diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 4bdb67e..ccaa900 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'dart:math'; import 'package:fl_chart/fl_chart.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; @@ -21,6 +22,7 @@ const allowedHeightForPlayerBioInPixels = 30.0; const givenTextHeightByScreenPercentage = 0.3; final NumberFormat timeInSec = NumberFormat("#,###.###s."); final NumberFormat f2 = NumberFormat.decimalPatternDigits(decimalDigits: 2); +final NumberFormat f4 = NumberFormat.decimalPatternDigits(decimalDigits: 4); final DateFormat dateFormat = DateFormat.yMMMd().add_Hms(); class MainView extends StatefulWidget { @@ -112,12 +114,31 @@ class _MainState extends State with SingleTickerProviderStateMixin { List states = []; if (isTracking){ teto.storeState(me); + teto.saveTLMatchesFromStream(await teto.getTLStream(me.userId)); states.addAll(await teto.getPlayer(me.userId)); } Map records = await teto.fetchRecords(me.userId); return [me, records, states, isTracking]; } + Future> getTLMatches(String userID) async { + var fetched = await teto.getTLStream(userID); + bool isTracked = await teto.isPlayerTracking(userID); + if (!isTracked) return fetched.records; + teto.saveTLMatchesFromStream(fetched); + var fromdb = await teto.getTLMatchesbyPlayerID(userID); + for (var match in fetched.records) { + if (!fromdb.contains(match)) fromdb.add(match); + } + fromdb.sort((a, b) { + if(a.timestamp.isBefore(b.timestamp)) return 1; + if(a.timestamp.isAtSameMomentAs(b.timestamp)) return 0; + if(a.timestamp.isAfter(b.timestamp)) return -1; + return 0; + }); + return fromdb; + } + void _justUpdate() { setState(() {}); } @@ -238,7 +259,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { TLThingy( tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId), - _TLRecords(userID: snapshot.data![0].userId), + _TLRecords(userID: snapshot.data![0].userId, get: getTLMatches,), _TLHistory(states: snapshot.data![2]), _RecordThingy( record: (snapshot.data![1]['sprint'].isNotEmpty) @@ -380,13 +401,14 @@ class _NavDrawerState extends State { class _TLRecords extends StatelessWidget { final String userID; + final Future> Function(String user) get; - const _TLRecords({required this.userID}); + const _TLRecords({required this.userID, required this.get}); @override Widget build(BuildContext context) { return FutureBuilder( - future: teto.getTLStream(userID), + future: get(userID), builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: @@ -400,19 +422,19 @@ class _TLRecords extends StatelessWidget { } else { return ListView( physics: const ClampingScrollPhysics(), - children: (snapshot.data!.records!.isNotEmpty) - ? [for (var value in snapshot.data!.records!) ListTile( + children: (snapshot.data!.isNotEmpty) + ? [for (var value in snapshot.data!) ListTile( leading: Text("${value.endContext.firstWhere((element) => element.userId == userID).points} : ${value.endContext.firstWhere((element) => element.userId != userID).points}", style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 28,)), title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username}"), - subtitle: Text(dateFormat.format(value.timestamp!)), - trailing: Column(mainAxisAlignment: MainAxisAlignment.end, + subtitle: Text(dateFormat.format(value.timestamp)), + trailing: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).secondary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).secondary)} APM", style: TextStyle(height: 1.1)), - Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary)} PPS", style: TextStyle(height: 1.1)), - Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra)} VS", style: TextStyle(height: 1.1)), + Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).secondary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).secondary)} APM", style: const TextStyle(height: 1.1)), + Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary)} PPS", style: const TextStyle(height: 1.1)), + Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra)} VS", style: const TextStyle(height: 1.1)), ]), onTap: (){Navigator.push( context, @@ -431,23 +453,47 @@ class _TLRecords extends StatelessWidget { class _TLHistory extends StatelessWidget{ final List states; - const _TLHistory({super.key, required this.states}); + const _TLHistory({required this.states}); @override Widget build(BuildContext context) { bool bigScreen = MediaQuery.of(context).size.width > 768; - List trData = [for (var state in states) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.rating)]; - List apmData = [for (var state in states) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.apm!)]; - List ppsData = [for (var state in states) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.pps!)]; - List vsData = [for (var state in states) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.vs!)]; + List trData = [for (var state in states) if (state.tlSeason1.gamesPlayed > 9) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.rating)]; + List apmData = [for (var state in states) if (state.tlSeason1.apm != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.apm!)]; + List ppsData = [for (var state in states) if (state.tlSeason1.pps != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.pps!)]; + List vsData = [for (var state in states) if (state.tlSeason1.vs != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.vs!)]; + List appData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.app)]; + List dssData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.dss)]; + List dspData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.dsp)]; + List appdspData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.appdsp)]; + List vsapmData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.vsapm)]; + List cheeseData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.cheese)]; + List gbeData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.gbe)]; + List nyaappData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.nyaapp)]; + List areaData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.area)]; + List estTrData = [for (var state in states) if (state.tlSeason1.estTr != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.estTr!.esttr)]; + List estaccData = [for (var state in states) if (state.tlSeason1.esttracc != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.esttracc!)]; return ListView(physics: const ClampingScrollPhysics(), children: states.isNotEmpty ? [ Column( children: [ - _HistoryChartThigy(data: trData, title: "Tetra Rating", yAxisTitle: "TR", bigScreen: bigScreen), - _HistoryChartThigy(data: apmData, title: "Attack Per Minute", yAxisTitle: "APM", bigScreen: bigScreen), - _HistoryChartThigy(data: ppsData, title: "Pieces Per Second", yAxisTitle: "PPS", bigScreen: bigScreen), - _HistoryChartThigy(data: vsData, title: "Versus Score", yAxisTitle: "VS", bigScreen: bigScreen), + if(trData.length > 1) _HistoryChartThigy(data: trData, title: "Tetra Rating", yAxisTitle: "TR", bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),), + if(apmData.length > 1) _HistoryChartThigy(data: apmData, title: "Attack Per Minute", yAxisTitle: "APM", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), + if(ppsData.length > 1) _HistoryChartThigy(data: ppsData, title: "Pieces Per Second", yAxisTitle: "PPS", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), + if(vsData.length > 1) _HistoryChartThigy(data: vsData, title: "Versus Score", yAxisTitle: "VS", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), + if(appData.length > 1) _HistoryChartThigy(data: appData, title: "Attack Per Piece", yAxisTitle: "APP", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), + if(dssData.length > 1) _HistoryChartThigy(data: dssData, title: bigScreen ? "Downstack Per Second" : "Downstack\nPer Second", yAxisTitle: "DS/S", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), + if(dspData.length > 1) _HistoryChartThigy(data: dspData, title: bigScreen ? "Downstack Per Piece" : "Downstack\nPer Piece", yAxisTitle: "DS/P", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), + if(appdspData.length > 1) _HistoryChartThigy(data: appdspData, title: "APP + DS/P", yAxisTitle: "APP + DS/P", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), + if(vsapmData.length > 1) _HistoryChartThigy(data: vsapmData, title: "VS/APM", yAxisTitle: "VS/APM", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), + if(cheeseData.length > 1) _HistoryChartThigy(data: cheeseData, title: "Cheese Index", yAxisTitle: "Cheese", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), + if(gbeData.length > 1) _HistoryChartThigy(data: gbeData, title: "Garbage Efficiency", yAxisTitle: "GbE", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), + if(nyaappData.length > 1) _HistoryChartThigy(data: nyaappData, title: "Weighted APP", yAxisTitle: "wAPP", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), + if(areaData.length > 1) _HistoryChartThigy(data: areaData, title: "Area", yAxisTitle: "Area", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), + if(estTrData.length > 1) _HistoryChartThigy(data: estTrData, title: "Est. of TR", yAxisTitle: "eTR", bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),), + if(estaccData.length > 1) _HistoryChartThigy(data: estaccData, title: "Accuracy of Est.", yAxisTitle: "±eTR", bigScreen: bigScreen, leftSpace: 60, yFormat: NumberFormat.compact(explicitSign: true),), + if(trData.length <= 1 || apmData.length <= 1 || ppsData.length <= 1 || vsData.length <= 1 || appData.length <= 1 || dssData.length <= 1 || dspData.length <= 1 || appdspData.length <= 1 || vsapmData.length <= 1 || cheeseData.length <= 1 || gbeData.length <= 1 || nyaappData.length <= 1 || areaData.length <= 1 || estTrData.length <= 1 || estaccData.length <= 1) const Center(child: Text("Some charts aren't shown due to lack of data...", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) + // Why it's look like a garbage solution??? ], ), ] : [const Center(child: Text("No history saved", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]); @@ -459,37 +505,40 @@ class _HistoryChartThigy extends StatelessWidget{ final String title; final String yAxisTitle; final bool bigScreen; - const _HistoryChartThigy({super.key, required this.data, required this.title, required this.yAxisTitle, required this.bigScreen}); + final double leftSpace; + final NumberFormat yFormat; + const _HistoryChartThigy({required this.data, required this.title, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context) { + double xInterval = bigScreen ? max(1, (data.last.x - data.first.x) / 6) : max(1, (data.last.x - data.first.x) / 3); return AspectRatio( aspectRatio: bigScreen ? 1.9 : 1.1, child: Stack( children: [ - Row(mainAxisAlignment: MainAxisAlignment.center, children: [Text(title, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))]), - Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 80, 40, 48) : const EdgeInsets.fromLTRB(0, 80, 0, 48) , + Row(mainAxisAlignment: MainAxisAlignment.center, children: [Text(title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))]), + Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 75, 40, 48) : const EdgeInsets.fromLTRB(0, 80, 0, 48) , child: LineChart( LineChartData( lineBarsData: [LineChartBarData(spots: data)], borderData: FlBorderData(show: false), + gridData: FlGridData(verticalInterval: xInterval), titlesData: FlTitlesData(topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){ - return SideTitleWidget( + bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){ + return value != meta.min && value != meta.max ? SideTitleWidget( axisSide: meta.axisSide, - angle: 0.3, child: Text(DateFormat(DateFormat.YEAR_ABBR_MONTH_DAY).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))), - ); + ) : Container(); })), - leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 80, getTitlesWidget: (double value, TitleMeta meta){ - return SideTitleWidget( + leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: leftSpace, getTitlesWidget: (double value, TitleMeta meta){ + return value != meta.min && value != meta.max ? SideTitleWidget( axisSide: meta.axisSide, - child: Text(f2.format(value)), - ); + child: Text(yFormat.format(value)), + ) : Container(); }))), - lineTouchData: LineTouchData(touchTooltipData: LineTouchTooltipData(getTooltipItems: (touchedSpots) { - return [for (var v in touchedSpots) LineTooltipItem("${f2.format(v.y)} $yAxisTitle \n", TextStyle(), children: [TextSpan(text: "${dateFormat.format(DateTime.fromMillisecondsSinceEpoch(v.x.floor()))}")])]; + lineTouchData: LineTouchData(touchTooltipData: LineTouchTooltipData( fitInsideHorizontally: true, fitInsideVertically: true, getTooltipItems: (touchedSpots) { + return [for (var v in touchedSpots) LineTooltipItem("${f4.format(v.y)} $yAxisTitle \n", const TextStyle(), children: [TextSpan(text: dateFormat.format(DateTime.fromMillisecondsSinceEpoch(v.x.floor())))])]; },)) ) ), diff --git a/lib/views/states_view.dart b/lib/views/states_view.dart index 73abd02..c769895 100644 --- a/lib/views/states_view.dart +++ b/lib/views/states_view.dart @@ -28,7 +28,7 @@ class StatesState extends State { itemBuilder: (context, index) { return ListTile( title: Text("On ${dateFormat.format(widget.states[index].state)}"), - subtitle: Text("Level ${widget.states[index].level.toStringAsFixed(2)} level, ${widget.states[index].gameTime} of gametime"), + subtitle: Text("Level ${widget.states[index].level.toStringAsFixed(2)}, ${widget.states[index].gameTime} of gametime, ${widget.states[index].friendCount} friends, ${NumberFormat.compact().format(widget.states[index].tlSeason1.rd)} RD"), trailing: IconButton( icon: const Icon(Icons.delete_forever), onPressed: () { diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 46ffd64..5d69c87 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -31,7 +31,7 @@ class TlMatchResultState extends State { return Scaffold( appBar: AppBar( title: Text( - "${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} vs. ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} in TL match ${dateFormat.format(widget.record.timestamp!)}"), + "${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} vs. ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} in TL match ${dateFormat.format(widget.record.timestamp)}"), ), backgroundColor: Colors.black, body: SafeArea( @@ -48,19 +48,19 @@ class TlMatchResultState extends State { children: [ Expanded( child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( - colors: [Colors.green, Colors.transparent], + colors: const [Colors.green, Colors.transparent], begin: Alignment.bottomCenter, end: Alignment.topCenter, - stops: [0.0, 0.4], + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).success ? 0.4 : 0.0], )), child: Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column(children: [ - Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: const TextStyle( + Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username, style: bigScreen ? const TextStyle( fontFamily: "Eurostile Round Extended", - fontSize: 28)), + fontSize: 28) : const TextStyle()), Text(widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).points.toString(), style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 42)) @@ -74,19 +74,19 @@ class TlMatchResultState extends State { ), Expanded( child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( - colors: [Colors.red, Colors.transparent], + colors: const [Colors.red, Colors.transparent], begin: Alignment.bottomCenter, end: Alignment.topCenter, - stops: [0.0, 0.4], + stops: [0.0, widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).success ? 0.4 : 0.0], )), child: Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column(children: [ - Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: const TextStyle( + Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username, style: bigScreen ? const TextStyle( fontFamily: "Eurostile Round Extended", - fontSize: 28)), + fontSize: 28) : const TextStyle()), Text(widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).points.toString(), style: const TextStyle( fontFamily: "Eurostile Round Extended", fontSize: 42)) @@ -779,8 +779,9 @@ class CompareRegTimeThingy extends StatelessWidget { String verdict(DateTime? greenSide, DateTime? redSide) { var f = NumberFormat("#,### days later;#,### days before"); String result = "---"; - if (greenSide != null && redSide != null) + if (greenSide != null && redSide != null) { result = f.format(greenSide.difference(redSide).inDays); + } return result; } diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 907151a..00e0b26 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -206,7 +206,7 @@ class TLThingy extends StatelessWidget { RadarEntry(value: tl.nerdStats!.dss * dssWeight), RadarEntry(value: tl.nerdStats!.dsp * dspWeight), RadarEntry(value: tl.nerdStats!.appdsp * appdspWeight), - RadarEntry(value: tl.nerdStats!.vsapm * vsWeight), + RadarEntry(value: tl.nerdStats!.vsapm * vsapmWeight), RadarEntry(value: tl.nerdStats!.cheese * cheeseWeight), RadarEntry(value: tl.nerdStats!.gbe * gbeWeight), ], diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 0c7de58..0f2f808 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -48,9 +48,7 @@ class UserThingy extends StatelessWidget { height: bannerHeight, errorBuilder: (context, error, stackTrace) { developer.log("Error with building banner image", name: "main_view", error: error, stackTrace: stackTrace); - return const Placeholder( - color: Colors.black, - ); + return Container(); }, ), Container( diff --git a/pubspec.yaml b/pubspec.yaml index 2514616..dd896dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,7 +85,12 @@ flutter: - res/tetrio_badges/early-supporter.png - res/tetrio_badges/founder.png - res/tetrio_badges/galactic2x2_1.png + - res/tetrio_badges/ggc_1.png - res/tetrio_badges/ggc_2.png + - res/tetrio_badges/ggc_3.png + - res/tetrio_badges/hdoxii_1.png + - res/tetrio_badges/hdoxii_2.png + - res/tetrio_badges/hdoxii_3.png - res/tetrio_badges/heart.png - res/tetrio_badges/hnprism_1.png - res/tetrio_badges/hnprism_2.png diff --git a/res/tetrio_badges/ggc_1.png b/res/tetrio_badges/ggc_1.png new file mode 100644 index 0000000000000000000000000000000000000000..26f25a50385cf4af0fc53ed00337a47be5e8f900 GIT binary patch literal 8170 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 O0F663svmAY3i%&lMdD5X literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/hdoxii_2.png b/res/tetrio_badges/hdoxii_2.png new file mode 100644 index 0000000000000000000000000000000000000000..7511e5aae81f77d2d870dc41e236736737562fd5 GIT binary patch literal 5028 zcmW+)c{tSF7yr&?8O!LkW*aIo_MNO_kPsnWvP+T5zB4fxOBAwX3sbW1`x+C8j4fqJ zcCRh6O_s6#^!wx9^W5j$=RD{0Ip?|e^PJ~KhB^#zPB;Jn47#_pOfD?$@1mi)*t^sm zNEZg=ZK8t#Dh9dNE)Gy<4Fe4Ts7|0eaex5;v!Sk*hH1d$?`&Eh7PFR~jKD=MRp*y;NpP-`2>!@}{}AefT}`hgkRhbc-doZo>9 zjIY;RfcB342r4ZJ?(lEVYB4BOrnQ-FEKy$j<$V?{uomn-dTxGj{A=t(Bd6@6;W@#A zE0)W;k|4>4VEaBOF@+X~Vg~MB2lfnPoCi;eU3{RBIS&i2hfYpsjV^xBXyBgTezWiT zn8fX_t1P&IEQRy5cej#BeSK%+vZ|P5bX>QWLjEp1y6bDNtQuwu92-|Tqf=Z%S`rTJ zlU2vKLqvpyL+JRNZzY}HV8+p?dPH~a2<6AZ(DCx17$f6fkF|@f%Y;URm~kbY`m4s- zpnQn%6C*tV-6SPWfC@!?LYj}H4?LvAwxcK%(gw* zviGOxR}X$^P50?kJ_7>E2K4*;(cjDN(oZ#+_0naRcu(E=7*r!}H4z4#T%P>X^IDbv z_a_%B4Gjm-pH+3ew4k=gZ83`$s+o+A+v7pQPmn-n!*2-Uv{|3{Oa19TLhyMCnmT(S zL^MBTa)WV|TkcBH{*3OOx`gmDk~s%-iS?nRqGxQyo}Ak#UBlKH<5~5?!iqh@-$10iq0R#rL4s##P79Jf=r@FztE{D&$EF%?viMqce!H4El_JaEsj}Ij}VlL=;G$s{wnWHRcQ<}qV ziG2rNKofhcU)82>URnD&?olCIH)A&77aSQBS451xqfm2lb;66eX9yP?>m`J$mmYqh z#QV2gg*m^e4J;4X-ZdG2!!JQQSKaOk=;}ElG`0UCHPi}0Ow%u88b#3kbSpDbj=2peeCe>>_ zxWBm+kn98AY1SF39*>kJ+VzUt8?ijBVFPIRkqZYE7G8wCxCnfs7)SZfraU+_tOBM5rPB@G(8Kmc;tiBi``}tXP z7wuQg>#HLb_W0?u3iop}Sa@5O!u(QG_2}7I%#IAiK34w=u61)q z^v1BJ$kU;*Do@6Pj;xaH$Him(pJWmzy$>})Q3{NPzmJr#k zJ?uzaO>E%#un!_=4)OmD5l|D-d#Er(-Clx161%xlMv&VFey8L_x8IlY8C99*HYjms zd{xR%H#7`O2P5Wc2b4=s`q#XO!&2Y62-BKa(CYVKAsIah-*p8zJhn^)nQq#38X|pm zGP#HJbx#oTea=h!<3_XDi`T5|C&$=-Q_u5E!y1?0S!7eEx@pVj?IE|v==cfbhrVe`q zHt}OF^Xg! zjOQA1HA=>y@~>7AYHHvYkC7e0v63HyZ`6OQwyPD@QK=Ese3z=cxv zZN$y>)q?>lw528I)5$vBT09u1UxLbCsU}MJVM$5C#+Hpc`eVZ|(ipASft^mgHyPpl z;K39tJphBj>fxb>4Y#k|WJ~#6Zvk)R{Zjx$C<3qev1oHFqjVvrI@+7s4^h(m*YkvfsvKkiF zqVj9H>B31%OG}7D2=O_ypnXp~Po@AjuCJR}?+S^!PlRu?-bK_lI?FcqPmc4{7VGNO zM|XhO*w_*?PfsbK*bRAJ4Iv2$XZ1zE{+_(myk<(Da~lS8)4^f>L;d#-FiFw(7t6+E zy?(6^2rZCoW4lsYf!PYv9sV#|RAubb8E`1I#imXnM`T10LBxspo92SLUd;o7S`Jm) zxq)DuFbYLs&EMs(^bFg2EFELg2Pk?k({KE!)v5J?5D0|1w3HD#>|?g;R`c&7iq}p) zA!)B|J^>Ned|C#2YymPblp74e^?mu$9BuNjRS=55l~rOD`x&Gdyf3;j-D-Xj<$}SS zPFkgOME}v;)m7Di+r9N0$uCddvrqiqkB-O={|q9E^DMR%3d?Nhkp*<`F9)9+`T_(- zZpCdGjCE-VX3o;m;1pS{T^DCm7sI1; z>D@vPSH;KYcc(pbnReTYP^&qB{ZkzWccM(l2JG&H;5CC75Tb2 zB@Kw%w$bRsbAJ`p1dPZa?0>MI{#%ag124wr^Ddt}D5T;#D1`v-Ym$hI0y;kD1ryIJ zuT$u7lgxhvwfst$dTTZ|)@g}Lq_iK82>+Tb3n@Ve%aDOUHA(-S$MV)sQ`Bq^B8Qvo zn1O&L$9K_yeS%RI#rh0PP=o!ZC!`cHO$dCMdCs&e_1bgqmziS7Nt4rMDx8vRG%J@MLKNjSOP_FU`vlRVz8PvSd-hR zZicb=^KFNU&RqQ0@Wg^a}dRGJ~5Y;h0FJL&J|Ib&fn90w7 z3-S#C>!SkemTYeD183*jYM>kr12(a9rS6PXuP`4{j=FSh7TP6%6o!3!j^PqCfSzZP z;`gG8=2K9hmX?u`BY6Pdu>{xcWRV{Itu zmde11@lyg|x?l7_AlC}V58I-u6M6v5K{(E^$U@%ph53d~e1sa_L_XIj>HU}zgy@Bv znz9}+M+yf3t9^ak#7@k|uX)9ZiHX3@&Q8el75*qNF2R*?ZF&>T?JA`gaN{Ge+C1i4(yts1Y82}=oC8XK8E?(A|LF%MVTHuW-_owD^R=^a4QQ=nd0iqB{CzT5nyqit4 zV&d19z2^^cI%>nxCIFg&Tal^ZDFwW+ugOaD4^= zSh=|9P$+#u==s?Z1m`^o%<*MGYo6Fyq}yd6>caAR?s(IvRjva}7_}XCz|_Y__I8H! z<@@*V|AFC{q@&Acht4i-hW`0c1p*?EkJ;E-$zfl8+o*@3IOki#48XnDt&USY)Y45G zN^kmCesQS}W+UbX{rTJJs;KCQ@3iA&K?EzB?H9+)g`s@k>t-T8(}=l$^*Tt}j=Ru% z*8yA4T$3Pz*X~M4lRn{IrNJ8*-Y7tq4|-raKrKI}eIdYdqZlsm9~`!;c7)uvo^n64 z4sEcZ4e^lB;9g|UK(G`aBLHHN(gXxg4b+JGFM4*^Bm_86O^+9;THPV!2sI=k{V#VM z`1eA*KYDZalq?>SG}1>Rg=(U@6=aAf0=e*V z!NBg>K-GJeyP!)K{Djkjp3p;pt#w-dm6A&`=cLMmE*nH0i&I}OtxPkjyXms5JSOSA ze2R(SCdUT$tL|NZyhBevN=a# zg>j^8wl3K`sI6e)LA~X9v`L>(M&n^}B7@MpWsf~bsNr?glKtgh<}Z~w>40|E2cU|L7*=EK|^xVY=4=Umjp8d(z3f=hQ-doc%?Q{Isx8cr@&9HK`yT?s@wj;v}Af zyRtKMwN}*;vxRg6)GxqyWpP>$wwlff}(zLj$dAxUz&T9Y^JSx{7w>|GXSI63q<%AR#+ARpuj^`Wo!9|YJ+7FVeFkKb=sfZpt< zR*Vd#H9>N^s0~rm@Ml5mrtea)@`ynTIL1qI*_;~0|4IDNf=t6-$i?Am-30e$$Mm|f z0vMD5{kEY1F9<${b*J%IF}~=I(fJ7jvg*K!lrj^IR@Tr`Ge|DKu9y~QxJ;_)-~T}( zk-pJ~*)H3)`VDv|t@LHuwz-1&da)r3^=NxM1<$0~pI{9nM9jK-+jBFzuyzrO={J({5Tc@uZgq zlrrK!n#{e;I2V+8hlVzl@+uj+Tkh5g0#}NJ-lG{vDEo=V^llhw&iO7yPoDI0(Y#BE zqW|l3;YdejXnw>eHsAceGmi4=ShDvPP3Oc(ZwU8onv5QP;&>gWeTZIqhPxpRKfZsQ z4qTbuagBaJ0c6D7CGd;2@AkL(K7={h08nw_D2xdwFlM-e`8dJ!S9zBgeJHt6lG&^%{vg8NLF#Bvq@1&8$<9YyZVP2Bc|gyBqD2szwoE@l7HJh-=M pl@R|$R<8454)kJfb*c26TE2<3WSJ-V`o*LT(A757s=(NX{|{xcZx{do literal 0 HcmV?d00001 diff --git a/res/tetrio_badges/hdoxii_3.png b/res/tetrio_badges/hdoxii_3.png new file mode 100644 index 0000000000000000000000000000000000000000..5b692abbd7caa7b46e6a82c24bb8ee1291124af6 GIT binary patch literal 5044 zcmYLNc{mi_*S}*OWT}+OUfDtuk$sG{Bs&q+jOZ zvp|+SN`_=8XH#XB(M+%Z*PHHFaQ4lw=N|v{)eiJLqG$5 zd%@nbqAFb&csk6}@-ulmPdn3>k5mN`e@N<(}&s1ybe^=01G>+fJWQFje_%gPm#ynh)qatkC12E7=5-}yl(WTHdX z`42qL-q$uCwIrn0l@N7RcAr1fRYScW(0}j4oiQ&zA02fl0xm7BDuP{b6h~oIZht=P`pLMi8 zGzH#9ouZmgEhqkZIEjBYw^QB}sM$jGT=Wm#(j~ebO$5Mg&U}C;jR^q-&kXDGta1)CMp{Q%0{ zZfkvq@3kA0t7X?b*<6MH&#XUe`*p4i?MrS_;RSO z&gxcb@m(I>u2-K|CikWH_oL!-;~SndPy#~L(E=WIe~%CDrT(Xy+iac$Ymh4`8+MNb zfVxdGDcVNoD|LTGW~|)kD7IpZ#KCuu=kAThi1J3R-Gdgl$C*27(8Jt_xXgn>^D8wo zj96!TMwN-6=m*A*eJaV^eGgV(=gF?q`Rt|cE+sgn1CA+@U{ zID}cr^Y{GWogjUdPXtAI$w?!+wFmP@M7+}OyN^9)a(4U^jX`Ov?2|cGSF9^$23s_$ zbxkNk%0%wqiH) zmtb%0q&h^Bun||;w#b$sQ$D$wUZO6hq_nTK89J{=%2b6Fxh<#m&s)wqNE; z+MinyQ3h^R9$SZDQuj+Y{ohZeRHqyVhz29+EQMcf@#~4|^_)uOuk!C)GYk^|qg|f3 zj)jw{PktGus>v&a4BvgtJ!ox~EQk!IYPdN??FvPfDutdX=<_TO4XZdl*T-^8LxGy` zz3CA`a6M98q~6lhbo0aW<5)%>zd%+m61vgZfLTOV_MZ|+LNI&%Zq1ais;k$nQ7D-=5=&p8NW0sf9ADun8n5#6%?JUcP*} z?*S26UiF&(6p}!fi|A){kHgHh+%f#%SNFzz^b`5Y-RkwnZhw+br>jTg*VC+#LfULV ze(~C3|Kd)s)#1bdPgXAf4_4$bY-v_TeC-`y`!h7& zx)O~utAi|Vc9kpmZmk@AWYGdie)eidytUI7a%F?|28+v6JKm_tHAPKwgBs}MH}j@K z5HaX*)RthyRoK$qQTTL|UxV`+3qoJNiw0?Du2P6gDMt;xaTr?Y5Z(>gr~@M8h09)_ znud+byYgR2Ph&is&kM8Nq#3(`n~$Vc0>pTfkDf|^g$G;*>2Zfa{_$Fx@|T}3tGd?HF7d&V9&C*qSZOjT7sMR>t66a{-rlbuq!-5mpQ&Vu z|CS`qMu5vB&U`F`F$oPnkw~k*^YIvj{+v#f=09xO-=aF}Z`{z0uOIJr6e67mxHfd- zz(7L{pCEIcoWz>l5iNGN)*A5YpoF0~WsoM*)PlS11}=AAw_gf-nC%3mgJ*huiSgs< ziiMF*eYeiWe)GkUJ75W^_FN@%Z_^Q`wr1C7JiV6Q%#6^$Lve++<-giWcPnx7 zmnB|ztX@!4EbDRR0qc}@Xfk(vkO#}25G4a2M5dBl>vrl*%!qmuc7sfSxir}(Dd<@u zV0u3qL#{%fL(D0skjByXccF(lA#~PtOD*X$^UG9BgV{TD0AM0_Ve7g@N{T?!V)Y zlLmO#iyV@R{bI;Y5Yxa7h2?u1@6Dnx47dYTTOo+)4~Vi`HL$3>h@z~0$#zoy4-D%i zMq$}Iag6${()Y>?Pp3M@29`Wa3>0EfSkq5K0^UR>PccYRjvs&O$$Z6L zAxW?bi4SZwF-#2IadmLZHxOo_Naf7T`zTytz{ce~eJ~ou?K(0YbrAsR()gUQ%X1k& zM|@0kSQSdIXm$sJ)gftz_>As_@c`#V7|q7I5q`H>|L(Nm#Kc6~PkwB~fGXY)%D$`P2Z4!ZQ*1RtltZ?$6^`DCJt0rBk&5O{O54u7DR0I*SN=0;y zXP}toBACD#hddwfmbmh~B1`d@W?+laCIbK;JK?RY9-js|!LWnhWD@}vD0=|v?Yf}t z5U4doq+fvV`N_jyTyWs}Sxh5Zs(Px*cdE598#dZNIMdjEF=xiBQkWei{r+pg5%pX{ zKpmJ)7Z+Jup4CXd+JO;m{AsBZM#Qwk0KDE#A8KQD8Wd`kheox$Zj#;vCB8JDt%W3E zPDSJrWB2F|E12s=F=B(`-BIN;TDGx5z6>KXjBfs#j-7}BaLFj(W(G9qZ^y9nyLsY>Gg z>F&10*7s9we^Z<)Was&U`&lWBxW-vx!bw56DljA(gh%r0Q zv`I4H462LSEsH9*KqK3fN16k+NPW zL;xEZ#)2?Q^FNPQS{p=WYqgaRr4b}dVZ5_FCdKiy9tszjg&d@p46Hp2b{ZWZxLh4E-#YxG-2b3YYBI^xOk*iHvN)pXMj~0`$+Y#@ zt{<%SwU5jIt9VAR)Rlujc?u zf3S*nkLx_O|1_|n4JDvi47xcnhpAP!+QuVDhE$uwQ|&KJQL0 z!NiPg?lY{23byt`BQ;&Uq5H-+(lLwV5!ALVl+ z#ve6)8pSZYObFE45!6NsK-)nx``}fbMi=4)LtAW0h%{ za;H;MQXW;t>t*9f?eU~MOLuUykF|Mw#-=&=QRzI-DocA=rHOxe)Q%$F8WVrh7e4rA ztY?l@M0zD*nJ;vi@3{_+0}xfKJG9GeQpfBPHRv@JOFGRWNc^bw_S8*g#Kf{S2b5_T zf~yZb>JT$qm2dI?_l^Ww8dHVYDy`un{MgujB_W12@ScikiY6C3c2e25!&ujXbu4Fz zPo%?nu;c@mtCpbo>Z}d)JJVcMqu{}5Soyhk@LB7{qmx0|NL68+Qp&K$6viz;Z?d88 z09X9g=$_o87|m!E1QBob8x5;9bYGYYdan9h_iXrBahDI3WUMrK<= z=a;Zyjp@0T*W8+z$%E|#`)tv{Zi;*0(a76XneQj<=Tf)tH$P+$`G)bGuSK9DNNDsc zuH~r9;MB`XW`^*i?Ol0I1;}O>##>}?fa0wGQaA{>%C((`5*0O($H>y?YnnA+vy)OS zf-ARU$)Z(3mZP}9Q5NzX15M(|clox4UQCcjmez6<3y|*%hyvJeilIjBeU*nGFstpr zpp7%`-kwR!a@|~IfkhPc4@(Ym7viIuOYOuQ{<&9Bc(yp4)XRVhag6}r0Lxt&%X#ve z|1s5_vkqbMn&=dS?mn9MD|)6Xm#U!}tE?11whQ9)B3+OGsL6}9;)>chJ|ZyQV6xY_3t&s;_l z2k%@XeU*{)=FcYovzv~sY*;m>n&ufKbm3P^`}Grk<4E46MGKEF{S@!gMzt&OT!W)Hsw5$vBO2| nyWI+kU!CqV(OX%fWi(bvQXC|`v!I9Gc>@M|#<~^SsAvBJG!m;j literal 0 HcmV?d00001