2024-01-01 17:26:09 +00:00
// ignore_for_file: type_literal_in_constant_pattern
2023-06-26 17:13:53 +00:00
import ' dart:io ' ;
2023-07-20 20:56:00 +00:00
import ' package:flutter/foundation.dart ' ;
2024-01-13 18:49:36 +00:00
import ' package:flutter/gestures.dart ' ;
2023-06-17 21:50:52 +00:00
import ' package:flutter/material.dart ' ;
2023-08-21 15:39:04 +00:00
import ' package:flutter_svg/flutter_svg.dart ' ;
2023-09-23 19:09:36 +00:00
import ' package:http/http.dart ' ;
2023-06-17 21:50:52 +00:00
import ' package:intl/intl.dart ' ;
2023-06-23 18:38:15 +00:00
import ' dart:math ' ;
2023-06-22 19:02:49 +00:00
import ' package:fl_chart/fl_chart.dart ' ;
2023-06-17 21:50:52 +00:00
import ' package:shared_preferences/shared_preferences.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:tetra_stats/data_objects/tetrio.dart ' ;
2023-07-10 17:42:20 +00:00
import ' package:tetra_stats/gen/strings.g.dart ' ;
2023-06-17 21:50:52 +00:00
import ' package:tetra_stats/services/tetrio_crud.dart ' ;
2023-10-26 22:38:03 +00:00
import ' package:tetra_stats/main.dart ' show prefs ;
2023-06-17 21:50:52 +00:00
import ' package:tetra_stats/services/crud_exceptions.dart ' ;
2024-02-08 21:39:54 +00:00
import ' package:tetra_stats/utils/text_shadow.dart ' ;
2023-10-08 17:20:42 +00:00
import ' package:tetra_stats/views/ranks_averages_view.dart ' show RankAveragesView ;
2023-07-07 20:32:57 +00:00
import ' package:tetra_stats/views/tl_leaderboard_view.dart ' show TLLeaderboardView ;
2023-06-21 19:17:39 +00:00
import ' package:tetra_stats/views/tl_match_view.dart ' show TlMatchResultView ;
2024-02-08 21:39:54 +00:00
import ' package:tetra_stats/widgets/search_box.dart ' ;
2023-06-17 21:50:52 +00:00
import ' package:tetra_stats/widgets/stat_sell_num.dart ' ;
import ' package:tetra_stats/widgets/tl_thingy.dart ' ;
import ' package:tetra_stats/widgets/user_thingy.dart ' ;
2023-10-08 17:20:42 +00:00
import ' package:window_manager/window_manager.dart ' ;
import ' package:flutter_markdown/flutter_markdown.dart ' ;
2023-12-26 22:02:47 +00:00
import ' package:go_router/go_router.dart ' ;
2023-06-17 21:50:52 +00:00
2024-01-26 20:56:24 +00:00
Future < List > me = Future . delayed ( const Duration ( seconds: 60 ) , ( ) = > [ null , null , null , null , null , null ] ) ; // I love lists shut up
String _searchFor = " 6098518e3d5155e6ec429cdc " ; // who we looking for
2023-06-21 22:05:14 +00:00
String _titleNickname = " dan63047 " ;
2024-01-26 20:56:24 +00:00
final TetrioService teto = TetrioService ( ) ; // thing, that manadge our local DB
/// Each dropdown menu item contains list of dots for the graph
2023-06-26 17:13:53 +00:00
var chartsData = < DropdownMenuItem < List < FlSpot > > > [ ] ;
2024-01-13 18:49:36 +00:00
int _chartsIndex = 0 ;
2024-01-26 20:56:24 +00:00
List _historyShortTitles = [ " TR " , " Glicko " , " RD " , " APM " , " PPS " , " VS " , " APP " , " DS/S " , " DS/P " , " APP + DS/P " , " VS/APM " , " Cheese " , " GbE " , " wAPP " , " Area " , " eTR " , " ±eTR " , " Opener " , " Plonk " , " Inf. DS " , " Stride " ] ;
2024-01-13 18:49:36 +00:00
late ScrollController _scrollController ;
2023-10-26 22:38:03 +00:00
final NumberFormat _timeInSec = NumberFormat ( " #,###.###s. " ) ;
2024-01-05 23:11:45 +00:00
final NumberFormat secs = NumberFormat ( " 00.### " ) ;
2023-10-26 22:38:03 +00:00
final NumberFormat _f2 = NumberFormat . decimalPatternDigits ( locale: LocaleSettings . currentLocale . languageCode , decimalDigits: 2 ) ;
final NumberFormat _f4 = NumberFormat . decimalPatternDigits ( locale: LocaleSettings . currentLocale . languageCode , decimalDigits: 4 ) ;
final DateFormat _dateFormat = DateFormat . yMMMd ( LocaleSettings . currentLocale . languageCode ) . add_Hms ( ) ;
2024-01-26 20:56:24 +00:00
2023-06-17 21:50:52 +00:00
class MainView extends StatefulWidget {
2023-07-09 16:50:17 +00:00
final String ? player ;
2024-01-26 20:56:24 +00:00
/// The very first view, that user see when he launch this programm.
/// By default it loads my or defined in preferences user stats, but
/// if [player] username or id provided, it loads his stats. Also it hides menu drawer and three dots menu.
2024-01-13 18:49:36 +00:00
const MainView ( { super . key , this . player } ) ;
2023-06-17 21:50:52 +00:00
2023-06-21 22:05:14 +00:00
String get title = > " Tetra Stats: $ _titleNickname " ;
2023-06-17 21:50:52 +00:00
@ override
State < MainView > createState ( ) = > _MainState ( ) ;
}
Future < void > copyToClipboard ( String text ) async {
await Clipboard . setData ( ClipboardData ( text: text ) ) ;
}
2024-01-26 20:56:24 +00:00
/// Takes number of [microseconds] and returns readable 40 lines time
2023-10-07 16:44:54 +00:00
String get40lTime ( int microseconds ) {
2024-01-05 23:11:45 +00:00
return microseconds > 60000000 ? " ${ ( microseconds / 1000000 / 60 ) . floor ( ) } : ${ ( secs . format ( microseconds / 1000000 % 60 ) ) } " : _timeInSec . format ( microseconds / 1000000 ) ;
2023-10-07 16:44:54 +00:00
}
2024-01-13 18:49:36 +00:00
class _MainState extends State < MainView > with TickerProviderStateMixin {
2023-06-17 21:50:52 +00:00
final bodyGlobalKey = GlobalKey ( ) ;
2024-01-26 20:56:24 +00:00
bool _showSearchBar = false ;
2023-06-17 21:50:52 +00:00
late TabController _tabController ;
late bool fixedScroll ;
@ override
void initState ( ) {
2023-07-22 12:07:57 +00:00
initDB ( ) ;
2023-06-17 21:50:52 +00:00
_scrollController = ScrollController ( ) ;
2023-06-21 22:05:14 +00:00
_tabController = TabController ( length: 6 , vsync: this ) ;
2024-01-26 20:56:24 +00:00
// We need to show something
if ( widget . player ! = null ) { // if we have user input,
changePlayer ( widget . player ! ) ; // it's gonna be user input
2023-07-09 16:50:17 +00:00
} else {
2024-01-26 20:56:24 +00:00
_getPreferences ( ) // otherwise, checking for preferences
. then ( ( value ) = > changePlayer ( prefs . getString ( " player " ) ? ? " dan63047 " ) ) ; // no preferences - loading me
2023-07-09 16:50:17 +00:00
}
2023-06-17 21:50:52 +00:00
super . initState ( ) ;
}
@ override
void dispose ( ) {
_tabController . dispose ( ) ;
_scrollController . dispose ( ) ;
super . dispose ( ) ;
}
Future < void > _getPreferences ( ) async {
prefs = await SharedPreferences . getInstance ( ) ;
}
2024-01-26 20:56:24 +00:00
/// That function initiate search of data about [player]. If [fetchHistory] is true,
/// also attempting to retrieve players history. Can trow an Exception if fails
2024-02-03 13:02:58 +00:00
void changePlayer ( String player , { bool fetchHistory = false , bool fetchTLmatches = false } ) {
2023-06-17 21:50:52 +00:00
setState ( ( ) {
_searchFor = player ;
2024-02-03 13:02:58 +00:00
me = fetch ( _searchFor , fetchHistory: fetchHistory , fetchTLmatches: fetchTLmatches ) ;
2023-06-17 21:50:52 +00:00
} ) ;
}
2023-07-22 12:07:57 +00:00
void initDB ( ) async {
await teto . open ( ) ;
}
2024-01-26 20:56:24 +00:00
/// Retrieves data from 3 different Tetra Channel API endpoints + 1 endpoint from p1nkl0bst3r's API
2024-02-03 13:02:58 +00:00
/// using [nickOrID] of player.
2024-01-26 20:56:24 +00:00
///
2024-02-03 13:02:58 +00:00
/// If [fetchHistory] is true, also retrieves players history from p1nkl0bst3r's API. If [fetchTLmatches] is true, also retrieves players old Tetra League
/// matches from p1nkl0bst3r's API. Returns list which contains [TetrioPlayer], his records, previous states, TL matches, previous TL state,
/// if player tracked (bool), news entries and topTR.
2024-01-26 20:56:24 +00:00
///
2024-02-03 13:02:58 +00:00
/// If at least one request to Tetra Channel API fails, whole function will throw an exception.
Future < List > fetch ( String nickOrID , { bool fetchHistory = false , bool fetchTLmatches = false } ) async {
2023-07-29 18:01:49 +00:00
TetrioPlayer me ;
2024-01-26 20:56:24 +00:00
// If user trying to search with discord id
2023-07-29 18:01:49 +00:00
if ( nickOrID . startsWith ( " ds: " ) ) {
2024-01-26 20:56:24 +00:00
me = await teto . fetchPlayer ( nickOrID . substring ( 3 ) , isItDiscordID: true ) ; // we trying to get him with that
2023-07-29 18:01:49 +00:00
} else {
2024-01-26 20:56:24 +00:00
me = await teto . fetchPlayer ( nickOrID ) ; // Otherwise it's probably a user id or username
2023-07-29 18:01:49 +00:00
}
2024-01-26 20:56:24 +00:00
_searchFor = me . userId ; // gonna use user id for next requests
// Change view title and window title if avaliable
setState ( ( ) { _titleNickname = me . username ; } ) ;
2023-10-10 20:20:27 +00:00
if ( ! kIsWeb & & ! Platform . isAndroid & & ! Platform . isIOS ) await windowManager . setTitle ( widget . title ) ;
2024-01-26 20:56:24 +00:00
// Requesting Tetra League (alpha), records, news and top TR of player
2023-10-08 17:20:42 +00:00
late List < dynamic > requests ;
late TetraLeagueAlphaStream tlStream ;
late Map < String , dynamic > records ;
late List < News > news ;
late double ? topTR ;
2024-01-26 20:56:24 +00:00
requests = await Future . wait ( [ // all at once
2024-02-01 00:15:32 +00:00
teto . fetchTLStream ( _searchFor ) ,
2024-01-26 20:56:24 +00:00
teto . fetchRecords ( _searchFor ) ,
teto . fetchNews ( _searchFor ) ,
if ( me . tlSeason1 . gamesPlayed > 9 ) teto . fetchTopTR ( _searchFor ) // can retrieve this only if player has TR
] ) ;
tlStream = requests [ 0 ] as TetraLeagueAlphaStream ;
records = requests [ 1 ] as Map < String , dynamic > ;
news = requests [ 2 ] as List < News > ;
topTR = requests . elementAtOrNull ( 3 ) as double ? ; // No TR - no Top TR
// Making list of Tetra League matches
2023-06-26 17:13:53 +00:00
List < TetraLeagueAlphaRecord > tlMatches = [ ] ;
2023-06-22 19:02:49 +00:00
bool isTracking = await teto . isPlayerTracking ( me . userId ) ;
List < TetrioPlayer > states = [ ] ;
2023-06-29 22:41:11 +00:00
TetraLeagueAlpha ? compareWith ;
2024-01-26 20:56:24 +00:00
Set < TetraLeagueAlpha > uniqueTL = { } ;
2023-10-18 21:50:41 +00:00
tlMatches = tlStream . records ;
2024-02-03 13:02:58 +00:00
List < TetraLeagueAlphaRecord > storedRecords = await teto . getTLMatchesbyPlayerID ( me . userId ) ; // get old matches
2024-01-26 20:56:24 +00:00
if ( isTracking ) { // if tracked - save data to local DB
2023-07-22 12:07:57 +00:00
await teto . storeState ( me ) ;
2023-10-07 16:44:54 +00:00
await teto . saveTLMatchesFromStream ( tlStream ) ;
2024-02-01 14:38:11 +00:00
}
// building list of TL matches
2024-02-03 13:02:58 +00:00
if ( fetchTLmatches ) {
try {
List < TetraLeagueAlphaRecord > oldMatches = await teto . fetchAndSaveOldTLmatches ( _searchFor ) ;
storedRecords . addAll ( oldMatches ) ;
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . fetchAndSaveOldTLmatchesResult ( number: oldMatches . length ) ) ) ) ;
} on TetrioHistoryNotExist {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . errors . p1nkl0bst3rTLmatches ) ) ) ;
} on P1nkl0bst3rForbidden {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . errors . p1nkl0bst3rForbidden ) ) ) ;
} on P1nkl0bst3rInternalProblem {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . errors . p1nkl0bst3rinternal ) ) ) ;
} on P1nkl0bst3rTooManyRequests {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . errors . p1nkl0bst3rTooManyRequests ) ) ) ;
}
}
2023-10-18 21:50:41 +00:00
for ( var match in storedRecords ) {
2024-01-26 20:56:24 +00:00
// add stored match to list only if it missing from retrived ones
2023-07-22 12:07:57 +00:00
if ( ! tlMatches . contains ( match ) ) tlMatches . add ( match ) ;
}
2024-01-26 20:56:24 +00:00
tlMatches . sort ( ( a , b ) { // Newest matches gonna be shown at the top of the list
2023-07-22 12:07:57 +00:00
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 ;
2024-02-01 14:38:11 +00:00
} ) ;
2024-01-26 20:56:24 +00:00
// Handling history
2024-02-03 13:02:58 +00:00
if ( fetchHistory ) {
try {
var history = await teto . fetchAndsaveTLHistory ( _searchFor ) ; // Retrieve if needed
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . fetchAndsaveTLHistoryResult ( number: history . length ) ) ) ) ;
} on TetrioHistoryNotExist {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . noHistorySaved ) ) ) ;
} on P1nkl0bst3rForbidden {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . errors . p1nkl0bst3rForbidden ) ) ) ;
} on P1nkl0bst3rInternalProblem {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . errors . p1nkl0bst3rinternal ) ) ) ;
} on P1nkl0bst3rTooManyRequests {
if ( context . mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( t . errors . p1nkl0bst3rTooManyRequests ) ) ) ;
}
}
2023-07-20 20:56:00 +00:00
states . addAll ( await teto . getPlayer ( me . userId ) ) ;
2024-01-26 20:56:24 +00:00
for ( var element in states ) { // For graphs I need only unique entries
if ( uniqueTL . isNotEmpty & & uniqueTL . last ! = element . tlSeason1 ) uniqueTL . add ( element . tlSeason1 ) ;
if ( uniqueTL . isEmpty ) uniqueTL . add ( element . tlSeason1 ) ;
}
2024-01-29 21:13:07 +00:00
// Also i need previous Tetra League State for comparison if avaliable
compareWith = uniqueTL . length > = 2 ? uniqueTL . toList ( ) . elementAtOrNull ( uniqueTL . length - 2 ) : null ;
chartsData = < DropdownMenuItem < List < FlSpot > > > [ // Dumping charts data into dropdown menu items, while cheking if every entry is valid
2023-07-15 16:22:25 +00:00
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . gamesPlayed > 9 ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . rating ) ] , child: Text ( t . statCellNum . tr ) ) ,
2023-06-28 16:50:40 +00:00
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . gamesPlayed > 9 ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . glicko ! ) ] , child: const Text ( " Glicko " ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . gamesPlayed > 9 ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . rd ! ) ] , child: const Text ( " Rating Deviation " ) ) ,
2023-07-15 16:22:25 +00:00
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . apm ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . apm ! ) ] , child: Text ( t . statCellNum . apm . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . pps ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . pps ! ) ] , child: Text ( t . statCellNum . pps . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . vs ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . vs ! ) ] , child: Text ( t . statCellNum . vs . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . app ) ] , child: Text ( t . statCellNum . app . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . dss ) ] , child: Text ( t . statCellNum . dss . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . dsp ) ] , child: Text ( t . statCellNum . dsp . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
2023-06-28 16:50:40 +00:00
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . appdsp ) ] , child: const Text ( " APP + DS/P " ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . vsapm ) ] , child: const Text ( " VS/APM " ) ) ,
2023-07-15 16:22:25 +00:00
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . cheese ) ] , child: Text ( t . statCellNum . cheese . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . gbe ) ] , child: Text ( t . statCellNum . gbe . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . nyaapp ) ] , child: Text ( t . statCellNum . nyaapp . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . nerdStats ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . nerdStats ! . area ) ] , child: Text ( t . statCellNum . area . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . estTr ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . estTr ! . esttr ) ] , child: Text ( t . statCellNum . estOfTR . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . esttracc ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . esttracc ! ) ] , child: Text ( t . statCellNum . accOfEst . replaceAll ( RegExp ( r'\n' ) , " " ) ) ) ,
2024-01-26 20:56:24 +00:00
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . playstyle ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . playstyle ! . opener ) ] , child: const Text ( " Opener " ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . playstyle ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . playstyle ! . plonk ) ] , child: const Text ( " Plonk " ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . playstyle ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . playstyle ! . infds ) ] , child: const Text ( " Inf. DS " ) ) ,
DropdownMenuItem ( value: [ for ( var tl in uniqueTL ) if ( tl . playstyle ! = null ) FlSpot ( tl . timestamp . millisecondsSinceEpoch . toDouble ( ) , tl . playstyle ! . stride ) ] , child: const Text ( " Stride " ) ) ,
2023-06-26 17:13:53 +00:00
] ;
2023-10-08 17:20:42 +00:00
return [ me , records , states , tlMatches , compareWith , isTracking , news , topTR ] ;
2023-06-23 18:38:15 +00:00
}
2024-01-26 20:56:24 +00:00
/// Triggers widgets rebuild
2023-06-17 21:50:52 +00:00
void _justUpdate ( ) {
setState ( ( ) { } ) ;
}
@ override
Widget build ( BuildContext context ) {
2023-07-10 17:42:20 +00:00
final t = Translations . of ( context ) ;
2024-02-08 21:39:54 +00:00
bool bigScreen = MediaQuery . of ( context ) . size . width > 768 ;
2023-06-17 21:50:52 +00:00
return Scaffold (
2024-01-26 20:56:24 +00:00
drawer: widget . player = = null ? NavDrawer ( changePlayer ) : null , // Side menu hidden if player provided
drawerEdgeDragWidth: MediaQuery . of ( context ) . size . width * 0.2 , // 20% of left side of the screen used of Drawer gesture
2023-06-17 21:50:52 +00:00
appBar: AppBar (
2024-02-08 21:39:54 +00:00
title: _showSearchBar ? SearchBox ( onSubmit: changePlayer , bigScreen: bigScreen ) : Text ( widget . title , style: const TextStyle ( shadows: textShadow ) ) ,
2023-06-17 21:50:52 +00:00
backgroundColor: Colors . black ,
2024-01-26 20:56:24 +00:00
actions: widget . player = = null ? [ // search bar and PopupMenuButton hidden if player provided TODO: Subject to change
_showSearchBar
? IconButton (
onPressed: ( ) {
setState ( ( ) {
_showSearchBar = false ;
} ) ;
} ,
icon: const Icon ( Icons . clear ) ,
tooltip: t . closeSearch ,
)
: IconButton (
onPressed: ( ) {
setState ( ( ) {
_showSearchBar = true ;
} ) ;
} ,
icon: const Icon ( Icons . search ) ,
tooltip: t . openSearch ,
) ,
2023-06-17 21:50:52 +00:00
PopupMenuButton (
itemBuilder: ( BuildContext context ) = > < PopupMenuEntry > [
2023-07-10 17:42:20 +00:00
PopupMenuItem (
2023-07-09 16:50:17 +00:00
value: " refresh " ,
2023-07-10 17:42:20 +00:00
child: Text ( t . refresh ) ,
2023-07-09 16:50:17 +00:00
) ,
2023-07-20 20:56:00 +00:00
PopupMenuItem (
2023-12-26 22:02:47 +00:00
value: " history " ,
2023-07-20 20:56:00 +00:00
child: Text ( t . fetchAndsaveTLHistory ) ,
) ,
2024-02-03 13:02:58 +00:00
PopupMenuItem (
value: " TLmatches " ,
child: Text ( t . fetchAndSaveOldTLmatches ) ,
) ,
2023-07-10 17:42:20 +00:00
PopupMenuItem (
2023-06-17 21:50:52 +00:00
value: " /states " ,
2023-07-10 17:42:20 +00:00
child: Text ( t . showStoredData ) ,
2023-06-17 21:50:52 +00:00
) ,
2023-07-10 17:42:20 +00:00
PopupMenuItem (
2023-06-17 21:50:52 +00:00
value: " /calc " ,
2023-07-10 17:42:20 +00:00
child: Text ( t . statsCalc ) ,
2023-06-17 21:50:52 +00:00
) ,
2023-07-10 17:42:20 +00:00
PopupMenuItem (
2023-06-17 21:50:52 +00:00
value: " /settings " ,
2023-07-10 17:42:20 +00:00
child: Text ( t . settings ) ,
2023-06-17 21:50:52 +00:00
) ,
] ,
onSelected: ( value ) {
2024-01-04 23:04:05 +00:00
switch ( value ) {
case " refresh " :
changePlayer ( _searchFor ) ;
break ;
case " history " :
changePlayer ( _searchFor , fetchHistory: true ) ;
break ;
2024-02-03 13:02:58 +00:00
case " TLmatches " :
changePlayer ( _searchFor , fetchTLmatches: true ) ;
break ;
2024-01-04 23:04:05 +00:00
default :
context . go ( value ) ;
}
2023-06-17 21:50:52 +00:00
} ,
) ,
2023-07-11 17:02:35 +00:00
] : null ,
2023-06-17 21:50:52 +00:00
) ,
body: SafeArea (
2023-06-21 22:05:14 +00:00
child: FutureBuilder < List < dynamic > > (
2023-06-17 21:50:52 +00:00
future: me ,
builder: ( context , snapshot ) {
switch ( snapshot . connectionState ) {
case ConnectionState . none:
case ConnectionState . waiting:
case ConnectionState . active:
2023-07-10 17:42:20 +00:00
return const Center ( child: CircularProgressIndicator ( color: Colors . white ) ) ;
2023-06-17 21:50:52 +00:00
case ConnectionState . done:
if ( snapshot . hasData ) {
2023-06-26 17:13:53 +00:00
return RefreshIndicator (
onRefresh: ( ) {
return Future ( ( ) = > changePlayer ( snapshot . data ! [ 0 ] . userId ) ) ;
2023-06-17 21:50:52 +00:00
} ,
2024-01-22 18:56:43 +00:00
notificationPredicate: ( notification ) {
// with NestedScrollView local(depth == 2) OverscrollNotification are not sent
if ( ! kIsWeb & & ( notification is OverscrollNotification | | Platform . isIOS ) ) {
return notification . depth = = 2 ;
}
return notification . depth = = 0 ;
} ,
2023-06-26 17:13:53 +00:00
child: NestedScrollView (
controller: _scrollController ,
physics: const AlwaysScrollableScrollPhysics ( ) ,
headerSliverBuilder: ( context , value ) {
return [
SliverToBoxAdapter (
child: UserThingy (
player: snapshot . data ! [ 0 ] ,
showStateTimestamp: false ,
setState: _justUpdate ,
) ) ,
SliverToBoxAdapter (
child: TabBar (
controller: _tabController ,
2024-01-13 18:49:36 +00:00
padding: const EdgeInsets . all ( 0.0 ) ,
2023-06-26 17:13:53 +00:00
isScrollable: true ,
2023-07-10 17:42:20 +00:00
tabs: [
Tab ( text: t . tetraLeague ) ,
Tab ( text: t . tlRecords ) ,
Tab ( text: t . history ) ,
Tab ( text: t . sprint ) ,
Tab ( text: t . blitz ) ,
Tab ( text: t . other ) ,
] ,
2023-06-26 17:13:53 +00:00
) ,
) ,
] ;
} ,
body: TabBarView (
controller: _tabController ,
children: [
2024-01-22 18:56:43 +00:00
TLThingy ( tl: snapshot . data ! [ 0 ] . tlSeason1 , userID: snapshot . data ! [ 0 ] . userId , states: snapshot . data ! [ 2 ] , topTR: snapshot . data ! [ 7 ] , bot: snapshot . data ! [ 0 ] . role = = " bot " , guest: snapshot . data ! [ 0 ] . role = = " anon " ) ,
2023-06-26 17:13:53 +00:00
_TLRecords ( userID: snapshot . data ! [ 0 ] . userId , data: snapshot . data ! [ 3 ] ) ,
_History ( states: snapshot . data ! [ 2 ] , update: _justUpdate ) ,
2024-02-01 00:15:32 +00:00
_RecordThingy ( record: snapshot . data ! [ 1 ] [ ' sprint ' ] ) ,
_RecordThingy ( record: snapshot . data ! [ 1 ] [ ' blitz ' ] ) ,
2023-10-08 17:20:42 +00:00
_OtherThingy ( zen: snapshot . data ! [ 1 ] [ ' zen ' ] , bio: snapshot . data ! [ 0 ] . bio , distinguishment: snapshot . data ! [ 0 ] . distinguishment , newsletter: snapshot . data ! [ 6 ] , )
2023-06-26 17:13:53 +00:00
] ,
) ,
2023-06-17 21:50:52 +00:00
) ,
) ;
} else if ( snapshot . hasError ) {
2023-07-11 17:02:35 +00:00
String errText = " " ;
switch ( snapshot . error . runtimeType ) {
case TetrioPlayerNotExist:
errText = t . errors . noSuchUser ;
break ;
case ConnectionIssue:
var err = snapshot . error as ConnectionIssue ;
errText = t . errors . connection ( code: err . code , message: err . message ) ;
break ;
2023-09-23 19:09:36 +00:00
case TetrioHistoryNotExist:
errText = t . errors . history ;
break ;
case TetrioForbidden:
errText = t . errors . forbidden ;
break ;
case TetrioTooManyRequests:
errText = t . errors . tooManyRequests ;
break ;
case TetrioOskwareBridgeProblem:
errText = t . errors . oskwareBridge ;
break ;
case TetrioInternalProblem:
errText = kIsWeb ? t . errors . internalWebVersion : t . errors . internal ;
break ;
case ClientException:
errText = t . errors . clientException ;
2023-07-11 17:02:35 +00:00
break ;
default :
errText = snapshot . error . toString ( ) ;
}
2023-09-23 19:09:36 +00:00
return Center ( child: Text ( errText , style: const TextStyle ( fontFamily: " Eurostile Round " , fontSize: 42 , fontWeight: FontWeight . bold ) , textAlign: TextAlign . center ) ) ;
2023-06-17 21:50:52 +00:00
}
break ;
}
2023-10-09 18:48:50 +00:00
return const Center ( child: Text ( ' default case of FutureBuilder ' , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: 42 ) , textAlign: TextAlign . center ) ) ;
2023-06-17 21:50:52 +00:00
} ,
) ,
) ,
) ;
}
}
class NavDrawer extends StatefulWidget {
final Function changePlayer ;
2024-01-26 20:56:24 +00:00
/// Thing, that shows from the left side of the view.
/// Requires [changePlayer] function in order to be able to change players on main view
2023-06-17 21:50:52 +00:00
const NavDrawer ( this . changePlayer , { super . key } ) ;
@ override
State < NavDrawer > createState ( ) = > _NavDrawerState ( ) ;
}
class _NavDrawerState extends State < NavDrawer > {
String homePlayerNickname = " Checking... " ;
@ override
void initState ( ) {
super . initState ( ) ;
_setHomePlayerNickname ( prefs . getString ( " player " ) ) ;
}
@ override
void dispose ( ) {
super . dispose ( ) ;
}
2024-01-26 20:56:24 +00:00
/// Sets username for home button in NavDrawer.
/// Accepts [id] or username. If it's not provided, sets my nickname.
/// Otherwise, sets username or [id] if failed to find
Future < void > _setHomePlayerNickname ( String ? id ) async {
if ( id ! = null ) {
2023-06-17 21:50:52 +00:00
try {
2024-01-26 20:56:24 +00:00
homePlayerNickname = await teto . getNicknameByID ( id ) ;
2023-06-17 21:50:52 +00:00
} on TetrioPlayerNotExist {
2024-01-26 20:56:24 +00:00
homePlayerNickname = id ;
2023-06-17 21:50:52 +00:00
}
} else {
homePlayerNickname = " dan63047 " ;
}
setState ( ( ) { } ) ;
}
@ override
Widget build ( BuildContext context ) {
return Drawer (
child: StreamBuilder (
stream: teto . allPlayers ,
builder: ( context , snapshot ) {
switch ( snapshot . connectionState ) {
case ConnectionState . none:
case ConnectionState . waiting:
case ConnectionState . active:
2023-06-20 20:53:28 +00:00
final allPlayers = ( snapshot . data ! = null )
2024-02-06 20:38:52 +00:00
? snapshot . data as Map < String , String >
: < String , String > { } ;
allPlayers . remove ( prefs . getString ( " player " ) ? ? " 6098518e3d5155e6ec429cdc " ) ; // player from the home button will be delisted
2023-10-08 17:20:42 +00:00
List < String > keys = allPlayers . keys . toList ( ) ;
2023-06-17 21:50:52 +00:00
return NestedScrollView (
headerSliverBuilder: ( context , value ) {
return [
2023-07-11 17:02:35 +00:00
SliverToBoxAdapter (
2023-06-17 21:50:52 +00:00
child: DrawerHeader (
2023-07-11 17:02:35 +00:00
child: Text ( t . playersYouTrack , style: const TextStyle ( color: Colors . white , fontSize: 25 ) ,
2023-06-17 21:50:52 +00:00
) ) ) ,
SliverToBoxAdapter (
2024-01-26 20:56:24 +00:00
child: ListTile ( // Home button
2023-06-17 21:50:52 +00:00
leading: const Icon ( Icons . home ) ,
title: Text ( homePlayerNickname ) ,
onTap: ( ) {
2024-01-26 20:56:24 +00:00
widget . changePlayer ( prefs . getString ( " player " ) ? ? " dan63047 " ) ; // changes player on main view to the one from preferences
Navigator . of ( context ) . pop ( ) ; // and then NavDrawer closes itself.
2023-06-17 21:50:52 +00:00
} ,
) ,
2023-07-07 20:32:57 +00:00
) ,
SliverToBoxAdapter (
2024-01-26 20:56:24 +00:00
child: ListTile ( // Leaderboard button
2023-07-07 20:32:57 +00:00
leading: const Icon ( Icons . leaderboard ) ,
2023-07-10 17:42:20 +00:00
title: Text ( t . tlLeaderboard ) ,
2023-07-07 20:32:57 +00:00
onTap: ( ) {
Navigator . push (
context ,
MaterialPageRoute (
builder: ( context ) = > const TLLeaderboardView ( ) ,
) ,
) ;
} ,
) ,
2023-07-11 17:02:35 +00:00
) ,
2023-09-04 18:00:13 +00:00
SliverToBoxAdapter (
2024-01-26 20:56:24 +00:00
child: ListTile ( // Rank averages button
2023-09-04 18:00:13 +00:00
leading: const Icon ( Icons . compress ) ,
title: Text ( t . rankAveragesViewTitle ) ,
onTap: ( ) {
Navigator . push (
context ,
MaterialPageRoute (
builder: ( context ) = > const RankAveragesView ( ) ,
) ,
) ;
} ,
) ,
) ,
2023-07-11 17:02:35 +00:00
const SliverToBoxAdapter ( child: Divider ( ) )
2023-06-17 21:50:52 +00:00
] ;
} ,
2024-01-26 20:56:24 +00:00
body: ListView . builder ( // Builds list of tracked players.
2023-06-17 21:50:52 +00:00
itemCount: allPlayers . length ,
itemBuilder: ( context , index ) {
2024-01-26 20:56:24 +00:00
var i = allPlayers . length - 1 - index ; // Last players in this map are most recent ones, they are gonna be shown at the top.
2023-06-17 21:50:52 +00:00
return ListTile (
2024-02-06 20:38:52 +00:00
title: Text ( allPlayers [ keys [ i ] ] ? ? keys [ i ] ) , // Takes last known username from list of states
2023-06-17 21:50:52 +00:00
onTap: ( ) {
2024-01-26 20:56:24 +00:00
widget . changePlayer ( keys [ i ] ) ; // changes to chosen player
Navigator . of ( context ) . pop ( ) ; // and closes itself.
2023-06-17 21:50:52 +00:00
} ,
) ;
} ) ) ;
case ConnectionState . done:
2024-01-26 20:56:24 +00:00
return const Center ( child: Text ( ' done case of StreamBuilder ' ) ) ; // what if that thing breaks?
2023-06-17 21:50:52 +00:00
}
} ,
) ,
) ;
}
}
2023-06-20 20:53:28 +00:00
class _TLRecords extends StatelessWidget {
final String userID ;
2023-06-26 17:13:53 +00:00
final List < TetraLeagueAlphaRecord > data ;
2024-01-26 20:56:24 +00:00
/// Widget, that displays Tetra League records.
/// Accepts list of TL records ([data]) and [userID] of player from the view
2023-06-26 17:13:53 +00:00
const _TLRecords ( { required this . userID , required this . data } ) ;
2023-06-20 20:53:28 +00:00
@ override
Widget build ( BuildContext context ) {
2024-02-03 13:02:58 +00:00
if ( data . isEmpty ) return Center ( child: Text ( t . noRecords , style: const TextStyle ( fontFamily: " Eurostile Round " , fontSize: 28 ) ) ) ;
2024-02-08 21:39:54 +00:00
bool bigScreen = MediaQuery . of ( context ) . size . width > 768 ;
2024-02-03 13:02:58 +00:00
return ListView . builder (
physics: const AlwaysScrollableScrollPhysics ( ) ,
itemCount: data . length ,
itemBuilder: ( BuildContext context , int index ) {
2024-02-08 21:39:54 +00:00
var accentColor = data [ index ] . endContext . firstWhere ( ( element ) = > element . userId = = userID ) . success ? Colors . green : Colors . red ;
return Container (
decoration: BoxDecoration (
gradient: LinearGradient (
stops: const [ 0 , 0.05 ] ,
colors: [ accentColor , Colors . transparent ]
)
) ,
child: ListTile (
// tileColor: data[index].endContext.firstWhere((element) => element.userId == userID).success ? Colors.green[900] : Colors.red[900],
leading: Text ( " ${ data [ index ] . endContext . firstWhere ( ( element ) = > element . userId = = userID ) . points } : ${ data [ index ] . endContext . firstWhere ( ( element ) = > element . userId ! = userID ) . points } " ,
style: bigScreen ? const TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: 28 , shadows: textShadow ) : const TextStyle ( fontSize: 28 , shadows: textShadow ) ) ,
title: Text ( " vs. ${ data [ index ] . endContext . firstWhere ( ( element ) = > element . userId ! = userID ) . username } " ) ,
subtitle: Text ( _dateFormat . format ( data [ index ] . timestamp ) ) ,
trailing: Table ( defaultColumnWidth: const IntrinsicColumnWidth ( ) ,
defaultVerticalAlignment: TableCellVerticalAlignment . baseline ,
textBaseline: TextBaseline . alphabetic ,
columnWidths: const {
0 : FixedColumnWidth ( 50 ) ,
2 : FixedColumnWidth ( 50 ) ,
} ,
children: [
TableRow ( children: [ Text ( _f2 . format ( data [ index ] . endContext . firstWhere ( ( element ) = > element . userId = = userID ) . secondary ) , textAlign: TextAlign . right , style: const TextStyle ( height: 1.1 ) ) , const Text ( " : " , style: TextStyle ( height: 1.1 ) ) , Text ( _f2 . format ( data [ index ] . endContext . firstWhere ( ( element ) = > element . userId ! = userID ) . secondary ) , textAlign: TextAlign . right , style: const TextStyle ( height: 1.1 ) ) , const Text ( " APM " , textAlign: TextAlign . right , style: TextStyle ( height: 1.1 ) ) ] ) ,
TableRow ( children: [ Text ( _f2 . format ( data [ index ] . endContext . firstWhere ( ( element ) = > element . userId = = userID ) . tertiary ) , textAlign: TextAlign . right , style: const TextStyle ( height: 1.1 ) ) , const Text ( " : " , style: TextStyle ( height: 1.1 ) ) , Text ( _f2 . format ( data [ index ] . endContext . firstWhere ( ( element ) = > element . userId ! = userID ) . tertiary ) , textAlign: TextAlign . right , style: const TextStyle ( height: 1.1 ) ) , const Text ( " PPS " , textAlign: TextAlign . right , style: TextStyle ( height: 1.1 ) ) ] ) ,
TableRow ( children: [ Text ( _f2 . format ( data [ index ] . endContext . firstWhere ( ( element ) = > element . userId = = userID ) . extra ) , textAlign: TextAlign . right , style: const TextStyle ( height: 1.1 ) ) , const Text ( " : " , style: TextStyle ( height: 1.1 ) ) , Text ( _f2 . format ( data [ index ] . endContext . firstWhere ( ( element ) = > element . userId ! = userID ) . extra ) , textAlign: TextAlign . right , style: const TextStyle ( height: 1.1 ) ) , const Text ( " VS " , textAlign: TextAlign . right , style: TextStyle ( height: 1.1 ) ) ] ) ,
] , ) ,
onTap: ( ) = > Navigator . push ( context , MaterialPageRoute ( builder: ( context ) = > TlMatchResultView ( record: data [ index ] , initPlayerId: userID ) ) ) ,
) ,
2023-06-26 17:13:53 +00:00
) ;
2024-02-03 13:02:58 +00:00
} ) ;
2023-06-20 20:53:28 +00:00
}
}
2023-06-26 17:13:53 +00:00
class _History extends StatelessWidget {
2023-06-22 19:02:49 +00:00
final List < TetrioPlayer > states ;
2023-06-26 17:13:53 +00:00
final Function update ;
2024-01-26 20:56:24 +00:00
/// Widget, that can show history of some stat of the player on the graph.
/// Requires player [states], which is list of states and function [update], which rebuild widgets
2023-06-26 17:13:53 +00:00
const _History ( { required this . states , required this . update } ) ;
2023-06-22 19:02:49 +00:00
@ override
Widget build ( BuildContext context ) {
bool bigScreen = MediaQuery . of ( context ) . size . width > 768 ;
2024-01-13 18:49:36 +00:00
return states . isNotEmpty ?
2023-06-22 19:02:49 +00:00
Column (
children: [
2023-06-26 17:13:53 +00:00
DropdownButton (
items: chartsData ,
2023-10-26 22:38:03 +00:00
value: chartsData [ _chartsIndex ] . value ,
2023-06-26 17:13:53 +00:00
onChanged: ( value ) {
2023-10-26 22:38:03 +00:00
_chartsIndex = chartsData . indexWhere ( ( element ) = > element . value = = value ) ;
2023-06-26 17:13:53 +00:00
update ( ) ;
}
) ,
2024-01-22 18:00:24 +00:00
if ( chartsData [ _chartsIndex ] . value ! . length > 1 ) _HistoryChartThigy ( data: chartsData [ _chartsIndex ] . value ! , yAxisTitle: _historyShortTitles [ _chartsIndex ] , bigScreen: bigScreen , leftSpace: bigScreen ? 80 : 45 , yFormat: bigScreen ? _f2 : NumberFormat . compact ( ) , )
2024-01-01 17:26:09 +00:00
else Center ( child: Text ( t . notEnoughData , style: const TextStyle ( fontFamily: " Eurostile Round " , fontSize: 28 ) ) )
2023-06-22 19:02:49 +00:00
] ,
2024-01-13 18:49:36 +00:00
)
: Center ( child: Text ( t . noHistorySaved , textAlign: TextAlign . center , style: const TextStyle ( fontFamily: " Eurostile Round " , fontSize: 28 ) ) ) ;
2023-06-22 19:02:49 +00:00
}
}
2024-01-13 18:49:36 +00:00
class _HistoryChartThigy extends StatefulWidget {
2023-06-22 19:02:49 +00:00
final List < FlSpot > data ;
final String yAxisTitle ;
final bool bigScreen ;
2023-06-23 18:38:15 +00:00
final double leftSpace ;
final NumberFormat yFormat ;
2024-01-26 20:56:24 +00:00
/// Implements graph for the _History widget. Requires [data] which is a list of dots for the graph. [yAxisTitle] used to keep track of changes.
2024-02-08 21:39:54 +00:00
/// [bigScreen] tells if screen wide enough, [leftSpace] sets size, reserved for titles on the left from the graph and [yFormat] sets number format
2024-01-26 20:56:24 +00:00
/// for left titles
2024-01-22 18:00:24 +00:00
const _HistoryChartThigy ( { required this . data , required this . yAxisTitle , required this . bigScreen , required this . leftSpace , required this . yFormat } ) ;
2024-01-13 18:49:36 +00:00
2023-06-22 19:02:49 +00:00
@ override
2024-01-13 18:49:36 +00:00
State < _HistoryChartThigy > createState ( ) = > _HistoryChartThigyState ( ) ;
}
class _HistoryChartThigyState extends State < _HistoryChartThigy > {
2024-01-22 18:00:24 +00:00
late String previousAxisTitle ;
2024-01-13 18:49:36 +00:00
late double minX ;
late double maxX ;
late double minY ;
2024-01-16 22:55:21 +00:00
late double actualMinY ;
2024-01-13 18:49:36 +00:00
late double maxY ;
2024-01-16 22:55:21 +00:00
late double actualMaxY ;
2024-01-22 18:00:24 +00:00
late double xScale ;
late double yScale ;
String headerTooltip = t . pseudoTooltipHeaderInit ;
String footerTooltip = t . pseudoTooltipFooterInit ;
int hoveredPointId = - 1 ;
double scaleFactor = 5e2 ;
double dragFactor = 7e2 ;
2024-01-13 18:49:36 +00:00
@ override
void initState ( ) {
super . initState ( ) ;
minX = widget . data . first . x ;
maxX = widget . data . last . x ;
2024-01-22 18:00:24 +00:00
setMinMaxY ( ) ;
previousAxisTitle = widget . yAxisTitle ;
actualMaxY = maxY ;
actualMinY = minY ;
recalculateScales ( ) ;
}
@ override
void dispose ( ) {
super . dispose ( ) ;
actualMinY = 0 ;
minY = 0 ;
}
2024-01-26 20:56:24 +00:00
/// Calculates and assignes maximum and minimum values in list of dots
2024-01-22 18:00:24 +00:00
void setMinMaxY ( ) {
actualMinY = widget . data . reduce ( ( value , element ) {
2024-01-13 18:49:36 +00:00
num n = min ( value . y , element . y ) ;
if ( value . y = = n ) {
return value ;
} else {
return element ;
}
} ) . y ;
2024-01-22 18:00:24 +00:00
actualMaxY = widget . data . reduce ( ( value , element ) {
2024-01-13 18:49:36 +00:00
num n = max ( value . y , element . y ) ;
if ( value . y = = n ) {
return value ;
} else {
return element ;
}
} ) . y ;
2024-01-22 18:00:24 +00:00
minY = actualMinY ;
maxY = actualMaxY ;
2024-01-16 22:55:21 +00:00
}
2024-01-26 20:56:24 +00:00
/// Calculates and assignes scales, which is difference between maximum and minimum visible axis value
2024-01-22 18:00:24 +00:00
void recalculateScales ( ) {
xScale = maxX - minX ;
yScale = maxY - minY ;
}
2024-01-26 20:56:24 +00:00
/// Accepts [dragUpdDet] and changes minX, maxX, minY, maxY based on that
2024-01-22 18:00:24 +00:00
void dragHandler ( DragUpdateDetails dragUpdDet ) {
setState ( ( ) {
2024-01-26 20:56:24 +00:00
// Changing min and max values according to drag delta and considering scales
2024-01-22 18:00:24 +00:00
minX - = ( xScale / dragFactor ) * dragUpdDet . delta . dx ;
maxX - = ( xScale / dragFactor ) * dragUpdDet . delta . dx ;
minY + = ( yScale / dragFactor ) * dragUpdDet . delta . dy ;
maxY + = ( yScale / dragFactor ) * dragUpdDet . delta . dy ;
2024-01-26 20:56:24 +00:00
// If values are out of bounds - putting them back
2024-01-22 18:00:24 +00:00
if ( minX < widget . data . first . x ) {
minX = widget . data . first . x ;
maxX = widget . data . first . x + xScale ;
}
if ( maxX > widget . data . last . x ) {
maxX = widget . data . last . x ;
minX = maxX - xScale ;
}
if ( minY < actualMinY ) {
minY = actualMinY ;
maxY = actualMinY + yScale ;
}
if ( maxY > actualMaxY ) {
maxY = actualMaxY ;
minY = actualMaxY - yScale ;
}
} ) ;
}
2024-01-26 20:56:24 +00:00
/// Accepts scale [details] and changes minX, maxX, minY, maxY in a way to change xScale and yScale.
/// [graphKey] required for sizes calculations, as well, as [graphStartX] and [graphEndX].
/// Not used yet, because GestureDetector works like shit
2024-01-22 18:00:24 +00:00
void scaleHandler ( ScaleUpdateDetails details , GlobalKey < State < StatefulWidget > > graphKey , double graphStartX , double graphEndX ) {
RenderBox graphBox = graphKey . currentContext ? . findRenderObject ( ) as RenderBox ;
2024-01-26 20:56:24 +00:00
// calculating relative position of scale gesture
Offset graphPosition = graphBox . localToGlobal ( Offset . zero ) ;
// 0 - very left position of graph; 1 - very right position of graph
double gesturePosRelativeX = ( details . focalPoint . dx - graphStartX ) / ( graphEndX - graphStartX ) ;
// 0 - very top position of graph; 1 - very bottom position of graph
double gesturePosRelativeY = ( details . focalPoint . dy - graphPosition . dy ) / ( graphBox . size . height - 30 ) ; // size - bottom titles height
double newMinX , newMaxX , newMinY , newMaxY ; // calcutating new values based on gesture and considering scales
newMinX = minX - ( xScale / scaleFactor ) * ( details . horizontalScale - 1 ) * gesturePosRelativeX ;
newMaxX = maxX + ( xScale / scaleFactor ) * ( details . horizontalScale - 1 ) * ( 1 - gesturePosRelativeX ) ;
newMinY = minY - ( yScale / scaleFactor ) * ( details . horizontalScale - 1 ) * ( 1 - gesturePosRelativeY ) ;
newMaxY = maxY + ( yScale / scaleFactor ) * ( details . horizontalScale - 1 ) * gesturePosRelativeY ;
// cancel changes if minimum is more, than maximun
if ( ( newMaxX - newMinX ) . isNegative ) return ;
2024-01-22 18:00:24 +00:00
if ( ( newMaxY - newMinY ) . isNegative ) return ;
2024-01-26 20:56:24 +00:00
// apply changes if everything ok + can't go past boundaries
2024-01-22 18:00:24 +00:00
setState ( ( ) {
minX = max ( newMinX , widget . data . first . x ) ;
maxX = min ( newMaxX , widget . data . last . x ) ;
minY = max ( newMinY , actualMinY ) ;
maxY = min ( newMaxY , actualMaxY ) ;
recalculateScales ( ) ;
} ) ;
2024-01-13 18:49:36 +00:00
}
@ override
Widget build ( BuildContext context ) {
2024-01-16 22:55:21 +00:00
GlobalKey graphKey = GlobalKey ( ) ;
2024-01-26 20:56:24 +00:00
double xInterval = widget . bigScreen ? max ( 1 , xScale / 6 ) : max ( 1 , xScale / 3 ) ; // how far away xTitles should be between each other
2024-01-13 18:49:36 +00:00
EdgeInsets padding = widget . bigScreen ? const EdgeInsets . fromLTRB ( 40 , 30 , 40 , 30 ) : const EdgeInsets . fromLTRB ( 0 , 40 , 16 , 48 ) ;
double graphStartX = padding . left + widget . leftSpace ;
double graphEndX = MediaQuery . sizeOf ( context ) . width - padding . right ;
2024-01-22 18:00:24 +00:00
if ( previousAxisTitle ! = widget . yAxisTitle ) {
setMinMaxY ( ) ;
recalculateScales ( ) ;
previousAxisTitle = widget . yAxisTitle ;
}
2023-06-26 17:13:53 +00:00
return SizedBox (
width: MediaQuery . of ( context ) . size . width ,
2024-01-13 18:49:36 +00:00
height: MediaQuery . of ( context ) . size . height - 104 ,
child: Listener (
2024-01-16 22:55:21 +00:00
behavior: HitTestBehavior . translucent ,
2024-01-13 18:49:36 +00:00
onPointerSignal: ( signal ) {
if ( signal is PointerScrollEvent ) {
2024-01-16 22:55:21 +00:00
RenderBox graphBox = graphKey . currentContext ? . findRenderObject ( ) as RenderBox ;
2024-01-26 20:56:24 +00:00
// calculating relative position of pointer
2024-01-16 22:55:21 +00:00
Offset graphPosition = graphBox . localToGlobal ( Offset . zero ) ;
2024-01-26 20:56:24 +00:00
// 0 - very left position of graph; 1 - very right position of graph
2024-01-13 18:49:36 +00:00
double scrollPosRelativeX = ( signal . position . dx - graphStartX ) / ( graphEndX - graphStartX ) ;
2024-01-26 20:56:24 +00:00
// 0 - very top position of graph; 1 - very bottom position of graph
2024-01-16 22:55:21 +00:00
double scrollPosRelativeY = ( signal . position . dy - graphPosition . dy ) / ( graphBox . size . height - 30 ) ; // size - bottom titles height
2024-01-26 20:56:24 +00:00
double newMinX , newMaxX , newMinY , newMaxY ; // calcutating new values based on pointer position and considering scales
2024-01-16 22:55:21 +00:00
newMinX = minX - ( xScale / scaleFactor ) * signal . scrollDelta . dy * scrollPosRelativeX ;
newMaxX = maxX + ( xScale / scaleFactor ) * signal . scrollDelta . dy * ( 1 - scrollPosRelativeX ) ;
newMinY = minY - ( yScale / scaleFactor ) * signal . scrollDelta . dy * ( 1 - scrollPosRelativeY ) ;
newMaxY = maxY + ( yScale / scaleFactor ) * signal . scrollDelta . dy * scrollPosRelativeY ;
2024-01-26 20:56:24 +00:00
// cancel changes if minimum is more, than maximun
2024-01-13 18:49:36 +00:00
if ( ( newMaxX - newMinX ) . isNegative ) return ;
2024-01-16 22:55:21 +00:00
if ( ( newMaxY - newMinY ) . isNegative ) return ;
2024-01-26 20:56:24 +00:00
// apply changes if everything ok + can't go past boundaries
2024-01-13 18:49:36 +00:00
setState ( ( ) {
minX = max ( newMinX , widget . data . first . x ) ;
maxX = min ( newMaxX , widget . data . last . x ) ;
2024-01-16 22:55:21 +00:00
minY = max ( newMinY , actualMinY ) ;
maxY = min ( newMaxY , actualMaxY ) ;
2024-01-22 18:00:24 +00:00
recalculateScales ( ) ;
_scrollController . jumpTo ( _scrollController . position . maxScrollExtent - signal . scrollDelta . dy ) ; // TODO: find a better way to stop scrolling in NestedScrollView
2024-01-13 18:49:36 +00:00
} ) ;
}
} ,
child:
GestureDetector (
2024-01-22 18:00:24 +00:00
behavior: HitTestBehavior . translucent ,
2024-01-13 18:49:36 +00:00
onDoubleTap: ( ) {
setState ( ( ) {
minX = widget . data . first . x ;
maxX = widget . data . last . x ;
2024-01-16 22:55:21 +00:00
minY = actualMinY ;
maxY = actualMaxY ;
2024-01-22 18:00:24 +00:00
recalculateScales ( ) ;
2024-01-13 18:49:36 +00:00
} ) ;
} ,
2024-01-22 18:00:24 +00:00
// TODO: onScaleUpdate:(details) => scaleHandler(details, graphKey, graphStartX, graphEndX),
// TODO: Figure out wtf is going on with gestures
// TODO: Somehow highlight touched spot (handleBuiltInTouches breaks getTooltipItems and getTouchedSpotIndicator)
child: Padding ( padding: padding ,
child: Stack (
children: [
LineChart (
key: graphKey ,
LineChartData (
lineBarsData: [ LineChartBarData ( spots: widget . data ) ] ,
clipData: const FlClipData . all ( ) ,
borderData: FlBorderData ( show: false ) ,
gridData: FlGridData ( verticalInterval: xInterval ) ,
minX: minX ,
maxX: maxX ,
minY: minY ,
maxY: maxY ,
titlesData: FlTitlesData ( topTitles: const AxisTitles ( sideTitles: SideTitles ( showTitles: false ) ) ,
rightTitles: const AxisTitles ( sideTitles: SideTitles ( showTitles: false ) ) ,
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 ,
child: Text ( DateFormat . yMMMd ( LocaleSettings . currentLocale . languageCode ) . format ( DateTime . fromMillisecondsSinceEpoch ( value . floor ( ) ) ) ) ,
) : Container ( ) ;
} ) ) ,
leftTitles: AxisTitles ( sideTitles: SideTitles ( showTitles: true , reservedSize: widget . leftSpace , getTitlesWidget: ( double value , TitleMeta meta ) {
return value ! = meta . min & & value ! = meta . max ? SideTitleWidget (
axisSide: meta . axisSide ,
child: Text ( widget . yFormat . format ( value ) ) ,
) : Container ( ) ;
} ) ) ) ,
lineTouchData: LineTouchData (
handleBuiltInTouches: false ,
touchCallback: ( touchEvent , touchResponse ) {
if ( touchEvent is FlPanUpdateEvent ) {
dragHandler ( touchEvent . details ) ;
return ;
}
if ( touchEvent is FlPointerHoverEvent ) {
setState ( ( ) {
if ( touchResponse ? . lineBarSpots ? . first = = null ) {
2024-01-26 20:56:24 +00:00
hoveredPointId = - 1 ; // not hovering over any point
2024-01-22 18:00:24 +00:00
} else {
hoveredPointId = touchResponse ! . lineBarSpots ! . first . spotIndex ;
headerTooltip = " ${ _f4 . format ( touchResponse . lineBarSpots ! . first . y ) } ${ widget . yAxisTitle } " ;
footerTooltip = _dateFormat . format ( DateTime . fromMillisecondsSinceEpoch ( touchResponse . lineBarSpots ! . first . x . floor ( ) ) ) ;
}
} ) ;
}
if ( touchEvent is FlPointerExitEvent ) {
setState ( ( ) { hoveredPointId = - 1 ; } ) ;
}
} ,
)
)
) ,
Padding (
padding: EdgeInsets . only ( left: widget . leftSpace ) ,
child: Column (
children: [
AnimatedDefaultTextStyle ( style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: 24 , color: Color . fromARGB ( hoveredPointId = = - 1 ? 100 : 255 , 255 , 255 , 255 ) , shadows: hoveredPointId ! = - 1 ? textShadow : null ) , duration: Durations . medium1 , curve: Curves . elasticInOut , child: Text ( headerTooltip ) ) ,
AnimatedDefaultTextStyle ( style: TextStyle ( fontFamily: " Eurostile Round " , color: Color . fromARGB ( hoveredPointId = = - 1 ? 100 : 255 , 255 , 255 , 255 ) , shadows: hoveredPointId ! = - 1 ? textShadow : null ) , duration: Durations . medium1 , curve: Curves . elasticInOut , child: Text ( footerTooltip ) ) ,
] ,
) ,
2024-01-16 22:55:21 +00:00
)
2024-01-22 18:00:24 +00:00
] ,
) ,
2023-06-22 19:02:49 +00:00
) ,
) ,
)
) ;
}
}
2023-06-17 21:50:52 +00:00
class _RecordThingy extends StatelessWidget {
final RecordSingle ? record ;
2024-01-26 20:56:24 +00:00
/// Widget that displays data from [record]
2024-01-13 18:49:36 +00:00
const _RecordThingy ( { required this . record } ) ;
2023-06-17 21:50:52 +00:00
@ override
Widget build ( BuildContext context ) {
2024-02-08 21:39:54 +00:00
if ( record = = null ) return Center ( child: Text ( t . noRecord , textAlign: TextAlign . center , style: const TextStyle ( fontFamily: " Eurostile Round " , fontSize: 28 ) ) ) ;
2023-06-17 21:50:52 +00:00
return LayoutBuilder ( builder: ( context , constraints ) {
bool bigScreen = constraints . maxWidth > 768 ;
return ListView . builder (
2023-06-26 17:13:53 +00:00
physics: const AlwaysScrollableScrollPhysics ( ) ,
2023-06-17 21:50:52 +00:00
itemCount: 1 ,
itemBuilder: ( BuildContext context , int index ) {
return Column (
2024-02-08 21:39:54 +00:00
children: [
2024-01-26 20:56:24 +00:00
// show mode title
if ( record ! . stream . contains ( " 40l " ) ) Text ( t . sprint , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) )
else if ( record ! . stream . contains ( " blitz " ) ) Text ( t . blitz , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) ) ,
// show main metric
if ( record ! . stream . contains ( " 40l " ) ) Text ( get40lTime ( record ! . endContext ! . finalTime . inMicroseconds ) , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) )
else if ( record ! . stream . contains ( " blitz " ) ) Text ( NumberFormat . decimalPattern ( ) . format ( record ! . endContext ! . score ) , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) ) ,
// Show rank if presented
if ( record ! . rank ! = null ) StatCellNum ( playerStat: record ! . rank ! , playerStatLabel: " Leaderboard Placement " , isScreenBig: bigScreen , higherIsBetter: false ) ,
// Show when this record was obtained
Text ( t . obtainDate ( date: _dateFormat . format ( record ! . timestamp ! ) ) , textAlign: TextAlign . center , style: const TextStyle ( fontFamily: " Eurostile Round " , fontSize: 16 ) ) ,
// Show metrics
Padding ( padding: const EdgeInsets . fromLTRB ( 0 , 48 , 0 , 48 ) ,
child: Wrap (
direction: Axis . horizontal ,
alignment: WrapAlignment . spaceAround ,
crossAxisAlignment: WrapCrossAlignment . start ,
clipBehavior: Clip . hardEdge ,
spacing: 25 ,
children: [
if ( record ! . stream . contains ( " blitz " ) ) StatCellNum ( playerStat: record ! . endContext ! . level , playerStatLabel: t . statCellNum . level , isScreenBig: bigScreen , higherIsBetter: true ) ,
if ( record ! . stream . contains ( " blitz " ) ) StatCellNum ( playerStat: record ! . endContext ! . spp , playerStatLabel: t . statCellNum . spp , fractionDigits: 2 , isScreenBig: bigScreen , higherIsBetter: true ) ,
StatCellNum ( playerStat: record ! . endContext ! . piecesPlaced , playerStatLabel: t . statCellNum . pieces , isScreenBig: bigScreen , higherIsBetter: true ) ,
StatCellNum ( playerStat: record ! . endContext ! . pps , playerStatLabel: t . statCellNum . pps , fractionDigits: 2 , isScreenBig: bigScreen , higherIsBetter: true ) ,
if ( record ! . endContext ! . finesse ! = null ) StatCellNum ( playerStat: record ! . endContext ! . finesse ! . faults , playerStatLabel: t . statCellNum . finesseFaults , isScreenBig: bigScreen , higherIsBetter: false ) ,
if ( record ! . endContext ! . finesse ! = null ) StatCellNum ( playerStat: record ! . endContext ! . finessePercentage * 100 , playerStatLabel: t . statCellNum . finessePercentage , fractionDigits: 2 , isScreenBig: bigScreen , higherIsBetter: true ) ,
StatCellNum ( playerStat: record ! . endContext ! . inputs , playerStatLabel: t . statCellNum . keys , isScreenBig: bigScreen , higherIsBetter: false ) ,
StatCellNum ( playerStat: record ! . endContext ! . kpp , playerStatLabel: t . statCellNum . kpp , fractionDigits: 2 , isScreenBig: bigScreen , higherIsBetter: false ) ,
StatCellNum ( playerStat: record ! . endContext ! . kps , playerStatLabel: t . statCellNum . kps , fractionDigits: 2 , isScreenBig: bigScreen , higherIsBetter: true , ) ,
] ,
) ,
) ,
// List of actions
Padding ( padding: const EdgeInsets . fromLTRB ( 0 , 16 , 0 , 48 ) ,
child: SizedBox ( width: bigScreen ? MediaQuery . of ( context ) . size . width * 0.4 : MediaQuery . of ( context ) . size . width * 0.85 ,
child: Column ( crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
2023-06-17 21:50:52 +00:00
children: [
2024-01-26 20:56:24 +00:00
Text ( " ${ t . numOfGameActions . pc } : " , style: const TextStyle ( fontSize: 24 ) ) ,
Text ( record ! . endContext ! . clears . allClears . toString ( ) , style: const TextStyle ( fontSize: 24 ) ) ,
2023-06-17 21:50:52 +00:00
] ,
) ,
2024-01-26 20:56:24 +00:00
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text ( " ${ t . numOfGameActions . hold } : " , style: const TextStyle ( fontSize: 24 ) ) ,
Text ( record ! . endContext ! . holds . toString ( ) , style: const TextStyle ( fontSize: 24 ) ) ,
] ,
2023-06-17 21:50:52 +00:00
) ,
2024-01-26 20:56:24 +00:00
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text ( " ${ t . numOfGameActions . tspinsTotal } : " , style: const TextStyle ( fontSize: 24 ) ) ,
Text ( record ! . endContext ! . tSpins . toString ( ) , style: const TextStyle ( fontSize: 24 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - T-spin zero: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . tSpinZeros . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - T-spin singles: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . tSpinSingles . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - T-spin doubles: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . tSpinDoubles . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - T-spin triples: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . tSpinTriples . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - T-spin mini zero: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . tSpinMiniZeros . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - T-spin mini singles: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . tSpinMiniSingles . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - T-spin mini doubles: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . tSpinMiniDoubles . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row ( mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text ( " ${ t . numOfGameActions . lineClears } : " , style: const TextStyle ( fontSize: 24 ) ) ,
Text ( record ! . endContext ! . lines . toString ( ) , style: const TextStyle ( fontSize: 24 ) ) ,
] ,
) ,
Row (
mainAxisAlignment:
MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - Singles: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . singles . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - Doubles: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . doubles . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - Triples: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . triples . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( " - Quads: " , style: TextStyle ( fontSize: 18 ) ) ,
Text ( record ! . endContext ! . clears . quads . toString ( ) , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
] ,
) ,
) ,
) ,
]
2023-06-17 21:50:52 +00:00
) ;
} ) ;
} ) ;
}
}
class _OtherThingy extends StatelessWidget {
final TetrioZen ? zen ;
final String ? bio ;
2023-08-21 15:39:04 +00:00
final Distinguishment ? distinguishment ;
2023-10-07 16:44:54 +00:00
final List < News > ? newsletter ;
2024-01-26 20:56:24 +00:00
/// Widget, that shows players [distinguishment], [bio], [zen] and [newsletter]
2024-01-13 18:49:36 +00:00
const _OtherThingy ( { required this . zen , required this . bio , required this . distinguishment , this . newsletter } ) ;
2023-06-17 21:50:52 +00:00
2024-01-26 20:56:24 +00:00
/// Distinguishment title is not very predictable thing.
/// Receives [text], which is header and returns sets of widgets for RichText widget
2024-01-01 17:26:09 +00:00
List < InlineSpan > getDistinguishmentTitle ( String ? text ) {
2024-01-26 20:56:24 +00:00
// TWC champions don't have header in their distinguishments
2024-01-01 17:26:09 +00:00
if ( distinguishment ? . type = = " twc " ) return [ const TextSpan ( text: " TETR.IO World Champion " , style: TextStyle ( fontSize: 28 , fontWeight: FontWeight . bold , color: Colors . yellowAccent ) ) ] ;
2024-01-26 20:56:24 +00:00
// In case if it missing for some other reason, return this
2024-01-01 17:26:09 +00:00
if ( text = = null ) return [ const TextSpan ( text: " Header is missing " , style: TextStyle ( fontSize: 28 , fontWeight: FontWeight . bold , color: Colors . redAccent ) ) ] ;
2024-01-26 20:56:24 +00:00
// Handling placeholders for logos
var exploded = text . split ( " " ) ; // wtf PHP reference?
2023-08-21 15:39:04 +00:00
List < InlineSpan > result = [ ] ;
for ( String shit in exploded ) {
2024-01-26 20:56:24 +00:00
switch ( shit ) { // if %% thingy was found, insert svg of icon
2023-08-21 15:39:04 +00:00
case " %osk% " :
result . add ( WidgetSpan ( child: Padding (
padding: const EdgeInsets . only ( left: 8 ) ,
child: SvgPicture . asset ( " res/icons/osk.svg " , height: 28 ) ,
) ) ) ;
break ;
case " %tetrio% " :
result . add ( WidgetSpan ( child: Padding (
padding: const EdgeInsets . only ( left: 8 ) ,
child: SvgPicture . asset ( " res/icons/tetrio-logo.svg " , height: 28 ) ,
) ) ) ;
break ;
2024-01-26 20:56:24 +00:00
default : // if not, insert text span
2024-01-22 18:00:24 +00:00
result . add ( TextSpan ( text: " $ shit " , style: const TextStyle ( fontSize: 28 , fontWeight: FontWeight . bold , color: Colors . white ) ) ) ;
2023-08-21 15:39:04 +00:00
}
}
return result ;
}
2024-01-26 20:56:24 +00:00
/// Distinguishment title is barely predictable thing.
/// Receives [text], which is footer and returns sets of widgets for RichText widget
2024-01-01 17:26:09 +00:00
String getDistinguishmentSubtitle ( String ? text ) {
2024-01-26 20:56:24 +00:00
// TWC champions don't have footer in their distinguishments
2024-01-01 17:26:09 +00:00
if ( distinguishment ? . type = = " twc " ) return " ${ distinguishment ? . detail } TETR.IO World Championship " ;
2024-01-26 20:56:24 +00:00
// In case if it missing for some other reason, return this
2024-01-01 17:26:09 +00:00
if ( text = = null ) return " Footer is missing " ;
2024-01-26 20:56:24 +00:00
// If everything ok, return as it is
2024-01-01 17:26:09 +00:00
return text ;
}
2024-01-26 20:56:24 +00:00
/// Handles [news] entry and returns widget that contains this entry
2023-10-07 16:44:54 +00:00
ListTile getNewsTile ( News news ) {
Map < String , String > gametypes = {
" 40l " : t . sprint ,
" blitz " : t . blitz ,
" 5mblast " : " 5,000,000 Blast "
} ;
2024-01-26 20:56:24 +00:00
// Individuly handle each entry type
2023-10-07 16:44:54 +00:00
switch ( news . type ) {
case " leaderboard " :
return ListTile (
title: RichText (
text: TextSpan (
2024-01-22 18:00:24 +00:00
style: const TextStyle ( fontFamily: ' Eurostile Round ' , fontSize: 16 , color: Colors . white ) ,
2023-10-07 16:44:54 +00:00
text: t . newsParts . leaderboardStart ,
children: [
TextSpan ( text: " № ${ news . data [ " rank " ] } " , style: const TextStyle ( fontWeight: FontWeight . bold ) ) ,
TextSpan ( text: t . newsParts . leaderboardMiddle ) ,
TextSpan ( text: " № ${ gametypes [ news . data [ " gametype " ] ] } " , style: const TextStyle ( fontWeight: FontWeight . bold ) ) ,
]
)
) ,
2023-10-26 22:38:03 +00:00
subtitle: Text ( _dateFormat . format ( news . timestamp ) ) ,
2023-10-07 16:44:54 +00:00
) ;
case " personalbest " :
return ListTile (
title: RichText (
text: TextSpan (
2024-01-22 18:00:24 +00:00
style: const TextStyle ( fontFamily: ' Eurostile Round ' , fontSize: 16 , color: Colors . white ) ,
2023-10-07 16:44:54 +00:00
text: t . newsParts . personalbest ,
children: [
TextSpan ( text: " ${ gametypes [ news . data [ " gametype " ] ] } " , style: const TextStyle ( fontWeight: FontWeight . bold ) ) ,
TextSpan ( text: t . newsParts . personalbestMiddle ) ,
TextSpan ( text: news . data [ " gametype " ] = = " blitz " ? NumberFormat . decimalPattern ( ) . format ( news . data [ " result " ] ) : get40lTime ( ( news . data [ " result " ] * 1000 ) . floor ( ) ) , style: const TextStyle ( fontWeight: FontWeight . bold ) ) ,
]
)
) ,
2023-10-26 22:38:03 +00:00
subtitle: Text ( _dateFormat . format ( news . timestamp ) ) ,
2023-10-08 17:20:42 +00:00
leading: Image . asset (
" res/icons/improvement-local.png " ,
height: 48 ,
width: 48 ,
errorBuilder: ( context , error , stackTrace ) {
return Image . asset ( " res/icons/kagari.png " , height: 64 , width: 64 ) ;
} ,
) ,
2023-10-07 16:44:54 +00:00
) ;
case " badge " :
return ListTile (
title: RichText (
text: TextSpan (
2024-01-22 18:00:24 +00:00
style: const TextStyle ( fontFamily: ' Eurostile Round ' , fontSize: 16 , color: Colors . white ) ,
2023-10-07 16:44:54 +00:00
text: t . newsParts . badgeStart ,
children: [
TextSpan ( text: " ${ news . data [ " label " ] } " , style: const TextStyle ( fontWeight: FontWeight . bold ) ) ,
TextSpan ( text: t . newsParts . badgeEnd )
]
)
) ,
2023-10-26 22:38:03 +00:00
subtitle: Text ( _dateFormat . format ( news . timestamp ) ) ,
2023-10-08 17:20:42 +00:00
leading: Image . asset (
" res/tetrio_badges/ ${ news . data [ " type " ] } .png " ,
height: 48 ,
width: 48 ,
errorBuilder: ( context , error , stackTrace ) {
return Image . asset ( " res/icons/kagari.png " , height: 64 , width: 64 ) ;
} ,
) ,
2023-10-07 16:44:54 +00:00
) ;
case " rankup " :
return ListTile (
title: RichText (
text: TextSpan (
2024-01-22 18:00:24 +00:00
style: const TextStyle ( fontFamily: ' Eurostile Round ' , fontSize: 16 , color: Colors . white ) ,
2023-10-07 16:44:54 +00:00
text: t . newsParts . rankupStart ,
children: [
TextSpan ( text: t . newsParts . rankupMiddle ( r: news . data [ " rank " ] . toString ( ) . toUpperCase ( ) ) , style: const TextStyle ( fontWeight: FontWeight . bold ) ) ,
TextSpan ( text: t . newsParts . rankupEnd )
]
)
) ,
2023-10-26 22:38:03 +00:00
subtitle: Text ( _dateFormat . format ( news . timestamp ) ) ,
2023-10-08 17:20:42 +00:00
leading: Image . asset (
" res/tetrio_tl_alpha_ranks/ ${ news . data [ " rank " ] } .png " ,
height: 48 ,
width: 48 ,
errorBuilder: ( context , error , stackTrace ) {
return Image . asset ( " res/icons/kagari.png " , height: 64 , width: 64 ) ;
} ,
) ,
2023-10-07 16:44:54 +00:00
) ;
case " supporter " :
return ListTile (
title: RichText (
text: TextSpan (
2024-01-22 18:00:24 +00:00
style: const TextStyle ( fontFamily: ' Eurostile Round ' , fontSize: 16 , color: Colors . white ) ,
2023-10-07 16:44:54 +00:00
text: t . newsParts . supporterStart ,
children: [
2023-10-08 17:20:42 +00:00
TextSpan ( text: t . newsParts . tetoSupporter , style: const TextStyle ( fontWeight: FontWeight . bold ) )
2023-10-07 16:44:54 +00:00
]
)
) ,
2023-10-26 22:38:03 +00:00
subtitle: Text ( _dateFormat . format ( news . timestamp ) ) ,
2023-10-08 17:20:42 +00:00
leading: Image . asset (
" res/icons/supporter-tag.png " ,
height: 48 ,
width: 48 ,
errorBuilder: ( context , error , stackTrace ) {
return Image . asset ( " res/icons/kagari.png " , height: 64 , width: 64 ) ;
} ,
) ,
2023-10-07 16:44:54 +00:00
) ;
case " supporter_gift " :
return ListTile (
title: RichText (
text: TextSpan (
2024-01-22 18:00:24 +00:00
style: const TextStyle ( fontFamily: ' Eurostile Round ' , fontSize: 16 , color: Colors . white ) ,
2023-10-07 16:44:54 +00:00
text: t . newsParts . supporterGiftStart ,
children: [
2023-10-08 17:20:42 +00:00
TextSpan ( text: t . newsParts . tetoSupporter , style: const TextStyle ( fontWeight: FontWeight . bold ) )
2023-10-07 16:44:54 +00:00
]
)
) ,
2023-10-26 22:38:03 +00:00
subtitle: Text ( _dateFormat . format ( news . timestamp ) ) ,
2023-10-08 17:20:42 +00:00
leading: Image . asset (
" res/icons/supporter-tag.png " ,
height: 48 ,
width: 48 ,
errorBuilder: ( context , error , stackTrace ) {
return Image . asset ( " res/icons/kagari.png " , height: 64 , width: 64 ) ;
} ,
) ,
2023-10-07 16:44:54 +00:00
) ;
2024-01-26 20:56:24 +00:00
default : // if type is unknown
2023-10-07 16:44:54 +00:00
return ListTile (
title: Text ( t . newsParts . unknownNews ( type: news . type ) ) ,
2023-10-26 22:38:03 +00:00
subtitle: Text ( _dateFormat . format ( news . timestamp ) ) ,
2023-10-07 16:44:54 +00:00
) ;
}
}
2023-06-17 21:50:52 +00:00
@ override
Widget build ( BuildContext context ) {
return LayoutBuilder ( builder: ( context , constraints ) {
bool bigScreen = constraints . maxWidth > 768 ;
return ListView . builder (
2023-06-26 17:13:53 +00:00
physics: const AlwaysScrollableScrollPhysics ( ) ,
2023-10-07 16:44:54 +00:00
itemCount: newsletter ! . length + 1 ,
2023-06-17 21:50:52 +00:00
itemBuilder: ( BuildContext context , int index ) {
2023-10-07 16:44:54 +00:00
return index = = 0 ? Column (
2023-06-17 21:50:52 +00:00
children: [
2023-08-21 15:39:04 +00:00
if ( distinguishment ! = null )
2023-06-17 21:50:52 +00:00
Padding (
2023-08-21 15:39:04 +00:00
padding: const EdgeInsets . fromLTRB ( 0 , 0 , 0 , 48 ) ,
2023-06-17 21:50:52 +00:00
child: Column (
children: [
2023-10-09 18:48:50 +00:00
Text ( t . distinguishment , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) , textAlign: TextAlign . center ) ,
2023-08-21 15:39:04 +00:00
RichText (
2023-10-11 15:32:47 +00:00
textAlign: TextAlign . center ,
2023-08-21 15:39:04 +00:00
text: TextSpan (
style: DefaultTextStyle . of ( context ) . style ,
2024-01-01 17:26:09 +00:00
children: getDistinguishmentTitle ( distinguishment ? . header ) ,
2023-08-21 15:39:04 +00:00
) ,
) ,
2024-01-01 17:26:09 +00:00
Text ( getDistinguishmentSubtitle ( distinguishment ? . footer ) , style: const TextStyle ( fontSize: 18 ) , textAlign: TextAlign . center ) ,
2023-06-17 21:50:52 +00:00
] ,
) ,
) ,
if ( bio ! = null )
Padding (
2023-10-08 17:20:42 +00:00
padding: const EdgeInsets . fromLTRB ( 8 , 0 , 8 , 48 ) ,
2023-06-17 21:50:52 +00:00
child: Column (
children: [
2023-08-21 15:39:04 +00:00
Text ( t . bio , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) ) ,
2023-10-08 17:20:42 +00:00
MarkdownBody ( data: bio ! , styleSheet: MarkdownStyleSheet ( textScaleFactor: 1.5 , textAlign: WrapAlignment . center ) ) // Text(bio!, style: const TextStyle(fontSize: 18)),
2023-06-17 21:50:52 +00:00
] ,
) ,
) ,
2023-08-21 15:39:04 +00:00
if ( zen ! = null )
Padding (
2023-10-07 16:44:54 +00:00
padding: const EdgeInsets . fromLTRB ( 0 , 0 , 0 , 48 ) ,
2023-08-21 15:39:04 +00:00
child: Column (
children: [
Text ( t . zen , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) ) ,
Text ( " ${ t . statCellNum . level } ${ NumberFormat . decimalPattern ( ) . format ( zen ! . level ) } " , style: const TextStyle ( fontSize: 28 , fontWeight: FontWeight . bold ) ) ,
Text ( " ${ t . statCellNum . score } ${ NumberFormat . decimalPattern ( ) . format ( zen ! . score ) } " , style: const TextStyle ( fontSize: 18 ) ) ,
] ,
) ,
) ,
2023-10-07 16:44:54 +00:00
if ( newsletter ! = null & & newsletter ! . isNotEmpty )
Text ( t . news , style: TextStyle ( fontFamily: " Eurostile Round Extended " , fontSize: bigScreen ? 42 : 28 ) ) ,
2023-06-17 21:50:52 +00:00
] ,
2023-10-07 16:44:54 +00:00
) : getNewsTile ( newsletter ! [ index - 1 ] ) ;
2023-06-17 21:50:52 +00:00
} ,
) ;
} ) ;
}
}