import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../../globals.dart'; class ProfilePage extends StatefulWidget { @override _ProfilePageState createState() => _ProfilePageState(); } class _ProfilePageState extends State { bool _isLoading = true; String? _name; int? _uid; int? _elo; double? _mu; double? _unc; List _matches = []; final String _getProfileApiUrl = '$apiurl/getprofile'; @override void initState() { super.initState(); _fetchProfileData(); } Future _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']; _mu = data['osk_mu']; _unc = data['osk_sig']; _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)), ); } String getRankImage(int? elo) { 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'; } void _showExplanationDialog() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('ELO, OSK, and UNC Explanation'), content: SingleChildScrollView( child: ListBody( children: [ // 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: [ TextButton( child: Text('Close'), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold( body: _isLoading ? Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _fetchProfileData, child: Column( children: [ // 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, ), SizedBox(width: 16), // Profile Info Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _name ?? 'Name not available', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), SizedBox(height: 6), Text('UID: ${_uid ?? 'N/A'}'), 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?', ), ], ), ], ), SizedBox(width: 25), Image.network( getRankImage(_elo), width: 137, height: 137, ), ], ), ), // Recent Matches Expanded( child: _matches.isEmpty ? Center( child: Text( "You haven't played any matches yet", style: TextStyle( fontSize: 16, color: Colors.grey, ), ), ) : 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, ), ), ], ), ], ), ), ); }, ), ), ], ), ), ); } }