2024-12-22 20:35:58 +01:00
import ' dart:convert ' ;
import ' dart:math ' ;
2024-12-21 19:41:00 +01:00
import ' package:flutter/material.dart ' ;
2024-12-22 20:35:58 +01:00
import ' package:http/http.dart ' as http ;
import ' package:shared_preferences/shared_preferences.dart ' ;
2024-12-23 21:48:34 +01:00
import ' ../../globals.dart ' ;
2024-12-21 19:41:00 +01:00
2024-12-22 20:35:58 +01:00
class ProfilePage extends StatefulWidget {
@ override
_ProfilePageState createState ( ) = > _ProfilePageState ( ) ;
}
class _ProfilePageState extends State < ProfilePage > {
bool _isLoading = true ;
String ? _name ;
int ? _uid ;
int ? _elo ;
2025-01-25 18:17:04 +01:00
double ? _mu ;
double ? _unc ;
2024-12-22 20:35:58 +01:00
List < dynamic > _matches = [ ] ;
2024-12-21 19:41:00 +01:00
2024-12-23 21:48:34 +01:00
final String _getProfileApiUrl = ' $ apiurl /getprofile ' ;
2024-12-22 20:35:58 +01:00
@ override
void initState ( ) {
super . initState ( ) ;
_fetchProfileData ( ) ;
}
Future < void > _fetchProfileData ( ) async {
final prefs = await SharedPreferences . getInstance ( ) ;
final String ? token = prefs . getString ( ' token ' ) ;
if ( token = = null ) {
_showToast ( ' No token found. Please login again. ' ) ;
return ;
}
try {
final response = await http . post (
Uri . parse ( _getProfileApiUrl ) ,
headers: { ' Content-Type ' : ' application/json ' } ,
body: json . encode ( { ' token ' : token } ) ,
) ;
if ( response . statusCode = = 200 ) {
final data = json . decode ( response . body ) ;
setState ( ( ) {
_name = data [ ' name ' ] ;
_uid = data [ ' uid ' ] ;
_elo = data [ ' elo ' ] ;
2025-01-25 18:17:04 +01:00
_mu = data [ ' osk_mu ' ] ;
_unc = data [ ' osk_sig ' ] ;
2024-12-22 20:35:58 +01:00
_matches = data [ ' matches ' ] ;
_isLoading = false ;
} ) ;
} else {
_showToast ( ' Failed to fetch profile data. ' ) ;
}
} catch ( e ) {
_showToast ( ' Error: $ e ' ) ;
}
}
Color _generateRandomColor ( ) {
final random = Random ( ) ;
return Color . fromARGB (
255 ,
random . nextInt ( 256 ) ,
random . nextInt ( 256 ) ,
random . nextInt ( 256 ) ,
) ;
}
void _showToast ( String message ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar ( content: Text ( message ) ) ,
) ;
}
2024-12-21 19:41:00 +01:00
2025-01-23 22:35:26 +01:00
String getRankImage ( int ? elo ) {
2025-01-23 22:55:32 +01:00
if ( elo = = null ) return ' $ apiurl /assets/none.png ' ;
if ( elo > 1000 ) return ' $ apiurl /assets/U.png ' ;
if ( elo > 750 ) return ' $ apiurl /assets/S.png ' ;
if ( elo > 400 ) return ' $ apiurl /assets/A.png ' ;
if ( elo > 200 ) return ' $ apiurl /assets/B.png ' ;
if ( elo > 100 ) return ' $ apiurl /assets/C.png ' ;
if ( elo > 30 ) return ' $ apiurl /assets/D.png ' ;
return ' $ apiurl /assets/none.png ' ;
2025-01-23 22:35:26 +01:00
}
2025-01-25 18:17:04 +01:00
void _showExplanationDialog ( ) {
showDialog (
context: context ,
builder: ( BuildContext context ) {
return AlertDialog (
title: Text ( ' ELO, OSK, and UNC Explanation ' ) ,
content: SingleChildScrollView (
child: ListBody (
children: < Widget > [
// ELO Section
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
" ELO (Elo Rating): " ,
style: TextStyle ( fontWeight: FontWeight . bold ) ,
) ,
SizedBox ( height: 4.0 ) ,
Text (
" ELO is a widely-used rating system designed to measure the relative skill levels of players in two-player games. It was my initial pick for a testing environment since I only really thought about 1v1 matches, and because it had a readily available Python implementation. I'm lazy. " ,
) ,
SizedBox ( height: 8.0 ) ,
] ,
) ,
// OSK Section
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
" OSK (OpenSkill Mu): " ,
style: TextStyle ( fontWeight: FontWeight . bold ) ,
) ,
SizedBox ( height: 4.0 ) ,
Text (
" OSKmu is a skill rating system based on the OpenSkill model, which is a probabilistic framework for estimating a player's skill. Unlike ELO, which is purely a point-based system, OpenSkill Mu takes into account not just the outcome of matches but also the degree of uncertainty in a player's skill estimation. Since I set up the system to have a 0-base-elo, I had to adapt the OpenSkill implementation to a standard 25 OSK and 8.33 uncertainty. " ,
) ,
SizedBox ( height: 8.0 ) ,
] ,
) ,
// UNC Section
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
" Uncertainty (UNC): " ,
style: TextStyle ( fontWeight: FontWeight . bold ) ,
) ,
SizedBox ( height: 4.0 ) ,
Text (
" This is a measure of how confident the system is about a player's skill rating. A higher uncertainty value means the system is less confident about the accuracy of the player's skill estimation, while a lower uncertainty indicates more confidence in the player's rating. " ,
) ,
] ,
) ,
] ,
) ,
) ,
actions: < Widget > [
TextButton (
child: Text ( ' Close ' ) ,
onPressed: ( ) {
Navigator . of ( context ) . pop ( ) ;
} ,
) ,
] ,
) ;
} ,
) ;
}
2024-12-21 19:41:00 +01:00
@ override
Widget build ( BuildContext context ) {
return Scaffold (
2024-12-22 20:35:58 +01:00
body: _isLoading
? Center ( child: CircularProgressIndicator ( ) )
: RefreshIndicator (
2024-12-23 21:48:34 +01:00
onRefresh: _fetchProfileData ,
child: Column (
2024-12-22 20:35:58 +01:00
children: [
2024-12-23 21:48:34 +01:00
// Profile Details
Container (
padding: EdgeInsets . all ( 16.0 ) ,
child: Row (
children: [
CircleAvatar (
backgroundColor: _generateRandomColor ( ) ,
child: Text (
_name ! = null & & _name ! . isNotEmpty
? _name ! [ 0 ] . toUpperCase ( )
: ' ? ' ,
style: TextStyle ( fontSize: 24 , color: Colors . white ) ,
) ,
radius: 40 ,
2024-12-22 20:35:58 +01:00
) ,
2025-01-23 22:35:26 +01:00
2024-12-23 21:48:34 +01:00
SizedBox ( width: 16 ) ,
// Profile Info
Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
_name ? ? ' Name not available ' ,
style: TextStyle (
fontSize: 24 ,
fontWeight: FontWeight . bold ,
) ,
2024-12-22 20:35:58 +01:00
) ,
2025-01-25 18:17:04 +01:00
SizedBox ( height: 6 ) ,
2024-12-23 21:48:34 +01:00
Text ( ' UID: ${ _uid ? ? ' N/A ' } ' ) ,
2025-01-25 18:17:04 +01:00
Row (
children: [
Text (
' ELO: ${ _elo ? ? ' N/A ' } | OSK: ${ _mu ! = null ? _mu ? . toStringAsFixed ( 3 ) : ' N/A ' } | UNC: ${ _unc ! = null ? _unc ? . toStringAsFixed ( 3 ) : ' N/A ' } ' ) ,
IconButton (
icon: Icon ( Icons . help_outline ) ,
onPressed: _showExplanationDialog ,
tooltip: ' What are ELO, OSK, and UNC? ' ,
) ,
] ,
) ,
2024-12-23 21:48:34 +01:00
] ,
) ,
2025-01-23 22:35:26 +01:00
SizedBox ( width: 25 ) ,
2025-01-23 22:55:32 +01:00
Image . network (
2025-01-23 22:35:26 +01:00
getRankImage ( _elo ) ,
width: 137 ,
height: 137 ,
) ,
2024-12-23 21:48:34 +01:00
] ,
) ,
) ,
// Recent Matches
Expanded (
child: _matches . isEmpty
? Center (
child: Text (
" You haven't played any matches yet " ,
style: TextStyle (
fontSize: 16 ,
color: Colors . grey ,
2024-12-22 20:35:58 +01:00
) ,
2024-12-23 21:48:34 +01:00
) ,
)
: ListView . builder (
itemCount: _matches . length ,
itemBuilder: ( context , index ) {
final match = _matches [ index ] ;
final result = match [ ' result ' ] ;
final eloChange = match [ ' elo_change ' ] ;
return Card (
margin: EdgeInsets . symmetric (
horizontal: 16.0 , vertical: 8.0 ) ,
child: Padding (
padding: const EdgeInsets . all ( 16.0 ) ,
child: Column (
crossAxisAlignment:
CrossAxisAlignment . start ,
children: [
Text (
' Match ID: ${ match [ ' match_id ' ] } ' ,
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
) ,
) ,
Text (
' Opponent: ${ match [ ' opponent_name ' ] } ' ) ,
Row (
children: [
Text (
' $ result ' ,
style: TextStyle (
fontWeight: FontWeight . bold ,
color: result = = ' Win '
? Colors . green
: Colors . red ,
) ,
) ,
SizedBox ( width: 16 ) ,
if ( eloChange ! = null )
Text (
' ELO Change: ${ eloChange > 0 ? ' + ' : ' ' } $ eloChange ' ,
style: TextStyle (
color: eloChange > 0
? Colors . green
: Colors . red ,
) ,
) ,
] ,
) ,
] ,
2024-12-22 20:35:58 +01:00
) ,
) ,
2024-12-23 21:48:34 +01:00
) ;
} ,
2024-12-22 20:35:58 +01:00
) ,
2024-12-23 21:48:34 +01:00
) ,
] ,
2024-12-21 19:41:00 +01:00
) ,
2024-12-22 20:35:58 +01:00
) ,
2024-12-21 19:41:00 +01:00
) ;
}
}