diff --git a/lib/data_objects/achievement.dart b/lib/data_objects/achievement.dart index d3794a3..4558d6b 100644 --- a/lib/data_objects/achievement.dart +++ b/lib/data_objects/achievement.dart @@ -46,6 +46,11 @@ class Achievement { this.total, this.rank}); + @override + String toString(){ + return "${name}: ${v}"; + } + Achievement.fromJson(Map json) { k = json['k']; o = json['o']; diff --git a/lib/data_objects/tetrio_constants.dart b/lib/data_objects/tetrio_constants.dart index 0588125..e3c3bb3 100644 --- a/lib/data_objects/tetrio_constants.dart +++ b/lib/data_objects/tetrio_constants.dart @@ -262,6 +262,13 @@ enum ComboTables{ 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; diff --git a/lib/main.dart b/lib/main.dart index d05dfac..c21e6f5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ 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/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'; @@ -18,6 +19,8 @@ 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( @@ -59,20 +62,6 @@ ThemeData theme = ThemeData( scaffoldBackgroundColor: Colors.black ); -final router = GoRouter( - initialLocation: "/", - 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']) - ) - ], -); - void main() async { // Initializing sqflite if (kIsWeb) { @@ -96,6 +85,25 @@ void main() async { prefs = await SharedPreferences.getInstance(); teto = TetrioService(); + router = GoRouter( + //initialLocation: prefs.getBool("notFirstTime") == true ? "/" : "/hihello", + initialLocation: "/", + 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/utils/colors_functions.dart b/lib/utils/colors_functions.dart index 1e04665..3340437 100644 --- a/lib/utils/colors_functions.dart +++ b/lib/utils/colors_functions.dart @@ -1,6 +1,7 @@ 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]!; diff --git a/lib/views/about_view.dart b/lib/views/about_view.dart new file mode 100644 index 0000000..99cd626 --- /dev/null +++ b/lib/views/about_view.dart @@ -0,0 +1,125 @@ +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.titleSmall), + Divider(), + Text(value, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white)), + if (undervalue != null) Text(undervalue!), + Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: RichText(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("Tetra Stats: ${t.settings}"); + } + 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 >= 368; + 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: 'Fuck go back', + child: const Icon(Icons.arrow_back), + ), + ), + backgroundColor: Colors.black, + body: SafeArea( + 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("About Tetra Stats", style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center), + ))), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Card(child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + textAlign: TextAlign.center, + "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." + ), + ) + ], + )), + ), + Expanded( + child: Column( + children: [ + AboutCard("App Version", packageInfo.version, "Build ${packageInfo.buildNumber}", [TextSpan(text: "${packageInfo.appName} (${packageInfo.packageName}) • "), TextSpan(text: "GitHub Repo", 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")); + })]), + AboutCard("Developed By", "dan63", 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("dan63.by", "donate"));})]), + ], + ), + ), + ], + ) + ], + )), + ); + } +} diff --git a/lib/views/compare_view_tiles.dart b/lib/views/compare_view_tiles.dart index 5c23860..9796286 100644 --- a/lib/views/compare_view_tiles.dart +++ b/lib/views/compare_view_tiles.dart @@ -665,7 +665,7 @@ class CompareState extends State { void getSummariesForInit() async { summaries.add(await teto.fetchSummaries(widget.initPlayer.userId)); - if (summaries[0].league.nerdStats != null) nicknames.add(players[0].username); + nicknames.add(players[0].username); addvaluesEntrys(players.first, summaries.first); best = recalculateBestEntries(); setState(() { @@ -678,7 +678,7 @@ class CompareState extends State { summaries.add(await teto.fetchSummaries(players.last.userId)); addvaluesEntrys(players.last, summaries.last); best = recalculateBestEntries(); - if (summaries.last.league.nerdStats != null) nicknames.add(players.last.username); + nicknames.add(players.last.username); setState(() { }); @@ -688,6 +688,7 @@ class CompareState extends State { int id = players.indexWhere((e) => e.username == nickname); players.removeAt(id); summaries.removeAt(id); + nicknames.remove(nickname); for (int i = 0; i < 7; i++){ rawValues[i].removeAt(id); formattedValues[i].removeAt(id); @@ -786,7 +787,9 @@ class CompareState extends State { ), ] ), - //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: nicknames) + 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]]), ], ), ), diff --git a/lib/views/destination_calculator.dart b/lib/views/destination_calculator.dart index fce3747..02de265 100644 --- a/lib/views/destination_calculator.dart +++ b/lib/views/destination_calculator.dart @@ -388,14 +388,27 @@ class _DestinationCalculatorState extends State { child: Column( children: [ Card( - child: ListTile( - title: Text("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);}), - )), + child: Column( + children: [ + ListTile( + title: Text("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("Perfect Clear Damage"), + 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( @@ -408,7 +421,7 @@ class _DestinationCalculatorState extends State { if (rules.combo) ListTile( title: Text("Combo Table"), trailing: DropdownButton( - items: [for (var v in ComboTables.values) DropdownMenuItem(value: v.index, child: Text(v.name))], + 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!];}), ), @@ -509,7 +522,7 @@ class _DestinationCalculatorState extends State { Text("Combo: ${intf.format(comboDamage)}"), Text("B2B: ${intf.format(b2bDamage)}"), Text("Surge: ${intf.format(surgeDamage)}"), - Text("PC's: ${intf.format(pcDamage)}") + Text("PCs: ${intf.format(pcDamage)}") ], ), if (totalDamage > 0) SfLinearGauge( diff --git a/lib/views/destination_home.dart b/lib/views/destination_home.dart index 6339792..5eff9da 100644 --- a/lib/views/destination_home.dart +++ b/lib/views/destination_home.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.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'; @@ -42,11 +43,10 @@ import 'package:tetra_stats/widgets/zenith_thingy.dart'; class DestinationHome extends StatefulWidget{ final String searchFor; final Future dataFuture; - final Future? newsFuture; final BoxConstraints constraints; final bool noSidebar; - const DestinationHome({super.key, required this.searchFor, required this.dataFuture, this.newsFuture, required this.constraints, this.noSidebar = false}); + const DestinationHome({super.key, required this.searchFor, required this.dataFuture, required this.constraints, this.noSidebar = false}); @override State createState() => _DestinationHomeState(); @@ -57,13 +57,14 @@ class FetchResults{ 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.cutoffs, this.averages, this.playerPos, this.isTracked, this.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{ @@ -139,6 +140,93 @@ class RecordSummary extends StatelessWidget{ ], ); } +} + +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, 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.285 * (((achievement?.k??1) - 1) % 8), 0.285 * (((achievement?.k??0) - 1) / 8).floor())), + //alignment: Alignment.topLeft.add(Alignment(0.285 * 1, 0)), + heightFactor: 0.125, + widthFactor: 0.125, + child: Image.asset("res/icons/achievements.png", width: 2048, height: 2048, scale: 1), + ), + ), + ), + ), + //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: 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: [ + TextSpan(text: "${achievement!.object}\n"), + if (achievement!.vt == 4) TextSpan(text: "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: "Top ${achievement?.pos != null ? percentage.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)) + ], + ), + ), + ); + } } @@ -730,7 +818,7 @@ class _DestinationHomeState extends State with SingleTickerProv ); } - Widget getRecordCard(RecordSingle? record, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ + Widget getRecordCard(RecordSingle? record, List achievements, bool? betterThanRankAverage, MapEntry? closestAverage, bool? betterThanClosestAverage, String? rank){ if (record == null) { return const Card( child: Center(child: Text("No record", style: TextStyle(fontSize: 42))), @@ -895,7 +983,13 @@ class _DestinationHomeState extends State with SingleTickerProv ], ), ), - ) + ), + Wrap( + direction: Axis.horizontal, + children: [ + for (Achievement achievement in achievements) FractionallySizedBox(widthFactor: 0.5, child: AchievementSummary(achievement: achievement)), + ], + ), ] ); } @@ -965,12 +1059,6 @@ class _DestinationHomeState extends State with SingleTickerProv _transition = AnimationController(vsync: this, duration: Durations.long4); - // _transition.addListener((){ - // setState(() { - - // }); - // }); - _offsetAnimation = Tween( begin: Offset.zero, end: const Offset(1.5, 0.0), @@ -1012,6 +1100,21 @@ class _DestinationHomeState extends State with SingleTickerProv closestAverageBlitz = blitzAverages.entries.last; blitzBetterThanClosestAverage = false; } + List sprintAchievements = [ + 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.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), @@ -1051,24 +1154,8 @@ class _DestinationHomeState extends State with SingleTickerProv ], ), ), - //if (testNews != null && testNews!.news.isNotEmpty) Expanded( - child: FutureBuilder( - future: widget.newsFuture, - 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 FutureError(snapshot); } - } - return const Text("what?"); - } - ), + child: NewsThingy(snapshot.data!.news!) ) ], ), @@ -1097,12 +1184,12 @@ class _DestinationHomeState extends State with SingleTickerProv CardMod.exRecords => getListOfRecords("zenithex/recent", "zenithex/top", widget.constraints), }, Cards.sprint => switch (cardMod){ - CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintBetterThanRankAverage, closestAverageSprint, sprintBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), + CardMod.info => getRecordCard(snapshot.data?.summaries!.sprint, sprintAchievements, 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.info => getRecordCard(snapshot.data?.summaries!.blitz, blitzAchievements, blitzBetterThanRankAverage, closestAverageBlitz, blitzBetterThanClosestAverage, snapshot.data!.summaries!.league.rank), CardMod.records => getListOfRecords("blitz/recent", "blitz/top", widget.constraints), _ => const Center(child: Text("huh?")) }, diff --git a/lib/views/destination_info.dart b/lib/views/destination_info.dart index 75d89df..895fc96 100644 --- a/lib/views/destination_info.dart +++ b/lib/views/destination_info.dart @@ -2,6 +2,8 @@ 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{ @@ -74,18 +76,23 @@ class _DestinationInfo extends State { InfoCard( height: widget.constraints.maxHeight - 77, assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", - title: "Shizuru!", - description: "Shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru shizuru\nNakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru Nakatsu Shizuru ", - onPressed: (){} + title: "Tetra Stats Wiki", + description: "Find more information about Tetra Stats functions and statictic, that it provides", + onPressed: (){ + launchInBrowser(Uri.https("github.com", "dan63047/TetraStats/wiki")); + } ), InfoCard( height: widget.constraints.maxHeight - 77, assetLink: "res/images/Снимок экрана_2023-11-06_01-00-50.png", title: "About Tetra Stats", description: "Developed by dan63\n", - onPressed: (){}, + onPressed: (){ + Navigator.push(context, MaterialPageRoute( + builder: (context) => AboutView(), + )); + }, ), - Card() ], ), ) diff --git a/lib/views/first_time_view.dart b/lib/views/first_time_view.dart new file mode 100644 index 0000000..e5ee2e2 --- /dev/null +++ b/lib/views/first_time_view.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.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 _transition; + late final Animation _offsetAnimation; + + @override + void 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(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: 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: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Welcome to Tetra Stats", style: Theme.of(context).textTheme.titleLarge), + Text("Service, that allows you to keep track of various statistics for TETR.IO"), + 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("What's your nickname?", style: Theme.of(context).textTheme.titleSmall), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: SizedBox(width: 400.0, child: TextField( + maxLength: 16, + decoration: InputDecoration( + hintText: "Type it here... (3-16 symbols)", + counter: const Offstage() + ), + )), + ), + ElevatedButton.icon(onPressed: (){}, icon: Icon(Icons.subdirectory_arrow_left), label: Text("Submit")) + ], + ), + ), + ), + ) + ], + ) + ) + ), + ), + ); + } + +} \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 06a4807..26eb932 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -23,7 +23,6 @@ import 'package:tetra_stats/views/destination_settings.dart'; import 'package:tetra_stats/main.dart'; late Future _data; -late Future _newsData; TetrioPlayersLeaderboard? _everyone; Future getData(String searchFor) async { @@ -36,23 +35,26 @@ Future getData(String searchFor) async { } }on TetrioPlayerNotExist{ - return FetchResults(false, null, [], null, null, null, null, false, 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]; - cutoffs = requests.elementAtOrNull(1); - averages = requests.elementAtOrNull(2); + news = requests[1]; + cutoffs = requests.elementAtOrNull(2); + averages = requests.elementAtOrNull(3); } on Exception catch (e) { - return FetchResults(false, null, [], null, null, null, null, false, e); + return FetchResults(false, null, [], null, null, null, null, null, false, e); } PlayerLeaderboardPosition? _meAmongEveryone; if (prefs.getBool("showPositions") == true){ @@ -71,7 +73,7 @@ Future getData(String searchFor) async { await teto.storeState(summaries.league); } - return FetchResults(true, player, states, summaries, cutoffs, averages, _meAmongEveryone, isTracking, null); + return FetchResults(true, player, states, summaries, news, cutoffs, averages, _meAmongEveryone, isTracking, null); } class MainView extends StatefulWidget { @@ -116,7 +118,6 @@ class _MainState extends State with TickerProviderStateMixin { setState(() { _searchFor = player; _data = getData(_searchFor); - _newsData = teto.fetchNews(_searchFor); }); } @@ -160,7 +161,7 @@ class _MainState extends State with TickerProviderStateMixin { onPressed: () { // Add your onPressed code here! }, - icon: const Icon(Icons.more_horiz_rounded), + icon: const Icon(Icons.refresh), ), destinations: [ getDestinationButton(Icons.home, "Home"), @@ -191,7 +192,7 @@ class _MainState extends State with TickerProviderStateMixin { ), Expanded( child: switch (destination){ - 0 => DestinationHome(searchFor: _searchFor, constraints: constraints, dataFuture: _data, newsFuture: _newsData), + 0 => DestinationHome(searchFor: _searchFor, constraints: constraints, dataFuture: _data), 1 => DestinationGraphs(searchFor: _searchFor, constraints: constraints), 2 => DestinationLeaderboards(constraints: constraints), 3 => DestinationCutoffs(constraints: constraints), diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart index c2395ad..23e0027 100644 --- a/lib/views/user_view.dart +++ b/lib/views/user_view.dart @@ -56,7 +56,7 @@ class UserState extends State { body: SafeArea( child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return DestinationHome(searchFor: widget.searchFor, dataFuture: getData(widget.searchFor), newsFuture: teto.fetchNews(widget.searchFor), constraints: constraints, noSidebar: true); + return DestinationHome(searchFor: widget.searchFor, dataFuture: getData(widget.searchFor), constraints: constraints, noSidebar: true); } ) ) 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