import 'dart:async'; import 'package:flutter/material.dart'; import 'dart:math'; import 'package:flutter_fortune_wheel/flutter_fortune_wheel.dart'; import 'package:provider/provider.dart'; import 'package:pvt15/api/backend_api.dart'; import 'package:pvt15/pages/game_ended.dart'; import 'package:pvt15/pages/game_page.dart'; import 'dart:convert'; import 'package:google_fonts/google_fonts.dart'; import 'package:pvt15/providers/user_provider.dart'; import 'package:pvt15/services/sse_service.dart'; import 'package:flutter/services.dart'; import 'package:shake/shake.dart'; import 'package:pvt15/hop_colors.dart'; class WheelPage extends StatefulWidget { final String sessionID; final String difficulty; const WheelPage({ super.key, required this.sessionID, required this.difficulty, }); @override State createState() => _WheelPageState(); } class _WheelPageState extends State { final StreamController controller = StreamController(); String ownPlayerId = ""; bool isMyTurn = false; int round = 1; int playerTurn = 1; String currentPlayerUsername = ""; Map turnOrder = {}; Map challenges = {}; List> participants = []; int get MAX_ROUNDS => 1; // Det här är rundor aka när alla spelare har kört en gång ökar den, inte turer. //Andreas int outcome = 0; List wheelItems = []; late ShakeDetector shakeDetector; Timer? _hapticSpinTimer; final Duration _wheelAnimationDuration = const Duration(seconds: 5); static const Duration _minHapticInterval = Duration(milliseconds: 80); static const Duration _maxHapticInterval = Duration(milliseconds: 600); DateTime? _animationStartTime; bool isSpinning = false; bool isLoading = true; @override void dispose() { controller.close(); _hapticSpinTimer?.cancel(); shakeDetector.stopListening(); super.dispose(); } @override void initState() { super.initState(); initializeEverything(); } Future initializeEverything() async { final user = Provider.of(context, listen: false).user; ownPlayerId = user!.googleID; await fetchTurnOrder(); await updateCurrentScore(); await initializeChallenge(); shakeDetector = ShakeDetector.autoStart( onPhoneShake: (ShakeEvent event) { _spin(); }, ); if (mounted) { setState(() { isLoading = false; }); } SSEService().onChallangeSelected = (String challenge) { List parts = challenge.split(','); String selectedTitle = parts.isNotEmpty ? parts[0].trim() : ''; String selectedDescription = parts.length > 1 ? parts[1].trim() : ''; String playerId = ""; if (turnOrder.containsKey(playerTurn)) { var player = turnOrder[playerTurn]; if (player is Map && player.containsKey('userId')) { playerId = player['userId']; } } isSpinning = false; Navigator.push( context, MaterialPageRoute( builder: (context) => GamePage( title: selectedTitle, description: selectedDescription, sessionID: widget.sessionID, playerId: playerId, isMyTurn: isMyTurn, currentPlayerUsername: currentPlayerUsername, ), ), ).then((result) { if (result == 'finished') { int antalSpelare = turnOrder.length; if (round <= MAX_ROUNDS) { round += 1; } setState(() { if (playerTurn < antalSpelare) { playerTurn += 1; checkIfMyTurn(); updateCurrentPlayerUsername(); handleUpdateCurrentScore(); } else if (round <= MAX_ROUNDS) { playerTurn = 1; checkIfMyTurn(); updateCurrentPlayerUsername(); handleUpdateCurrentScore(); } else { handleSaveGameSessionScore(); Navigator.push( context, MaterialPageRoute( builder: (context) => GameEndedPage(sessionID: widget.sessionID), ), ); } }); } }); }; } void handleUpdateCurrentScore() async { await updateCurrentScore(); } Future updateCurrentScore() async { final response = await authHttpRequest( context: context, url: 'https://group-1-15.pvt.dsv.su.se/api/participants/${widget.sessionID}', //http://10.0.2.2:8080/ https://group-1-15.pvt.dsv.su.se method: 'GET', ); if (response.statusCode == 200) { final List data = jsonDecode(utf8.decode(response.bodyBytes)); setState(() { participants = data .map>( (participant) => { 'name': participant['username'], 'points': participant['score'] ?? 0, }, ) .toList() ..sort( (a, b) => (b['points'] as int).compareTo(a['points'] as int), ); }); } else { print("fel vid updateCurrentScore"); } } void handleSaveGameSessionScore() async { await saveGameSessionScore(); } Future saveGameSessionScore() async { final response = await authHttpRequest( context: context, url: 'https://group-1-15.pvt.dsv.su.se/api/participants/${widget.sessionID}/score/save', //http://10.0.2.2:8080/ https://group-1-15.pvt.dsv.su.se method: 'PUT', ); if (response.statusCode != 200) { print("fel vid sparande av poängen"); } } void updateCurrentPlayerUsername() { if (turnOrder.containsKey(playerTurn)) { var player = turnOrder[playerTurn]; if (player is Map && player.containsKey('username')) { currentPlayerUsername = player['username']; } } } void checkIfMyTurn() { final currentPlayer = turnOrder[playerTurn]; final currentPlayerId = currentPlayer is Map ? currentPlayer['userId'] : null; isMyTurn = (ownPlayerId == currentPlayerId); } void handlefetchTurnOrder() async { await fetchTurnOrder(); } Future fetchTurnOrder() async { final response = await authHttpRequest( context: context, url: 'https://group-1-15.pvt.dsv.su.se/api/game-sessions/${widget.sessionID}/participants/turnOrder', //http://10.0.2.2:8080/ https://group-1-15.pvt.dsv.su.se method: 'GET', ); if (response.statusCode == 200) { final Map decoded = jsonDecode( utf8.decode(response.bodyBytes), ); turnOrder = decoded.map((key, value) => MapEntry(int.parse(key), value)); checkIfMyTurn(); updateCurrentPlayerUsername(); } } Future initializeChallenge() async { await fetchChallenges(); _loadChallenges(); } void _spin() { if (isMyTurn && !isSpinning) { isSpinning = true; HapticFeedback.mediumImpact(); outcome = Random().nextInt(wheelItems.length); controller.add(outcome); } } String _getDescription(String title) { List list = challenges[title]; Map item = list[Random().nextInt(list.length)]; return item['description']; } void leaveGame() { Navigator.popUntil( context, (route) => route.settings.name == '/gameCategory', ); } @override Widget build(BuildContext context) { if (isLoading) { return Scaffold( body: Container( decoration: HopColors.hopBackgroundDecoration, child: const Center(child: CircularProgressIndicator()), ), ); } return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, dynamic result) async { if (!didPop) { await _showLeaveConfirmationDialog(); } }, child: Scaffold( body: Stack( children: [ Container( decoration: HopColors.hopBackgroundDecoration, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( currentPlayerUsername.isNotEmpty ? "Tur $playerTurn: $currentPlayerUsername" : '', style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Color.fromARGB(221, 255, 255, 255), ), textAlign: TextAlign.center, ), const SizedBox(height: 20), _buildWheelContainer(), const SizedBox(height: 60), _buildLeaderboardPodium(), ], ), ), ), Positioned( top: 40, left: 10, child: IconButton( icon: const Icon( Icons.arrow_back, color: Colors.white, size: 30, ), onPressed: () { _showLeaveConfirmationDialog(); }, ), ), ], ), ), ); } Future _showLeaveConfirmationDialog() async { return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text('Lämna spelet?'), content: const SingleChildScrollView( child: ListBody( children: [ Text('Är du säker på att du vill lämna spelet?'), ], ), ), actions: [ TextButton( child: const Text('Nej'), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( child: const Text('Ja'), onPressed: () { Navigator.of(context).pop(); leaveGame(); }, ), ], ); }, ); } Widget _buildWheelContainer() { return Container( height: 480, width: 370, decoration: BoxDecoration( color: const Color(0x99FED4FF), borderRadius: BorderRadius.circular(40), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.25), spreadRadius: 2, blurRadius: 12, offset: Offset(0, 6), ), ], ), child: Stack( alignment: Alignment.center, children: [_buildContainerText(), _buildWheel()], ), ); } Widget _buildWheel() { return SizedBox( width: 320, height: 320, child: GestureDetector( onTap: _spin, child: FortuneWheel( animateFirst: false, selected: controller.stream, duration: _wheelAnimationDuration, onAnimationStart: () { _hapticSpinTimer?.cancel(); _animationStartTime = DateTime.now(); _scheduleNextHapticFeedback(); }, onAnimationEnd: _handleWheelResult, items: _buildWheelItems(), indicators: [_buildFortuneIndicator()], ), ), ); } void _scheduleNextHapticFeedback() { if (_animationStartTime == null) return; final elapsedTime = DateTime.now().difference(_animationStartTime!); double progress = elapsedTime.inMilliseconds / _wheelAnimationDuration.inMilliseconds; if (progress >= 0.98) { _hapticSpinTimer?.cancel(); return; } final currentIntervalValue = _minHapticInterval.inMilliseconds + (_maxHapticInterval.inMilliseconds - _minHapticInterval.inMilliseconds) * progress; final currentInterval = Duration( milliseconds: currentIntervalValue.round().clamp( _minHapticInterval.inMilliseconds, _maxHapticInterval.inMilliseconds, ), ); HapticFeedback.selectionClick(); _hapticSpinTimer = Timer(currentInterval, _scheduleNextHapticFeedback); } FortuneIndicator _buildFortuneIndicator() { return FortuneIndicator( alignment: Alignment.topCenter, child: Transform.translate( offset: const Offset(0, -10), child: Stack( alignment: Alignment.center, children: [ TriangleIndicator( color: Color(0xFF6750A4), width: 38.0, height: 40.0, elevation: 0, ), Positioned( top: 1.8, child: TriangleIndicator( color: Color(0xFF50D89B), width: 30.0, height: 32.0, elevation: 0, ), ), ], ), ), ); } Widget _buildContainerText() { return Positioned( top: 20, child: Text( 'Klicka på hjulet!', style: GoogleFonts.poppins( fontSize: 22, fontWeight: FontWeight.w600, color: Colors.white, ), ), ); } List segmentColors = [Color(0xFFFF6B81), Color(0xFFA88BEB)]; List _buildWheelItems() { return List.generate(wheelItems.length, (index) { String text = wheelItems[index]; bool isLongText = text.length > 15; return FortuneItem( child: Padding( padding: const EdgeInsets.all(6.0), child: Center( child: Text( text, textAlign: TextAlign.center, maxLines: isLongText ? 2 : 1, overflow: TextOverflow.ellipsis, style: GoogleFonts.poppins( fontSize: isLongText ? 10 : 14, fontWeight: FontWeight.w600, color: Colors.white, height: 1.1, ), ), ), ), style: FortuneItemStyle( color: segmentColors[index % segmentColors.length], borderColor: Color(0xFF6750A4), borderWidth: 3.5, ), ); }); } Future sendChallenge(String challenge) async { final response = await authHttpRequest( context: context, url: 'https://group-1-15.pvt.dsv.su.se/sse/send/${widget.sessionID}', //http://10.0.2.2:8080/ https://group-1-15.pvt.dsv.su.se method: 'POST', body: challenge, ); if (response.statusCode != 200) { print("Fel vid skickande av challenge"); } } void handleSendChallenge(String challenge) async { await sendChallenge(challenge); } void removeSelectedChallenge( String selectedTitle, String selectedDescription, ) { if (challenges.containsKey(selectedTitle)) { List challengeList = challenges[selectedTitle]; challengeList.removeWhere( (challenge) => challenge is Map && challenge['description'] == selectedDescription, ); if (challengeList.isEmpty) { challenges.remove(selectedTitle); _loadChallenges(); } else { challenges[selectedTitle] = challengeList; } } } void _handleWheelResult() { _hapticSpinTimer?.cancel(); _animationStartTime = null; HapticFeedback.heavyImpact(); String selectedTitle = wheelItems[outcome]; String selectedDescription = _getDescription(selectedTitle); if (selectedTitle == "Vem är mest trolig att") { int randomIndex = Random().nextInt(turnOrder.length) + 1; var randomUser = turnOrder[randomIndex]; String username = randomUser['username']; selectedDescription = '$selectedDescription $username'; } else { removeSelectedChallenge(selectedTitle, selectedDescription); } String message = "challenge:${wheelItems[outcome]},$selectedDescription"; handleSendChallenge(message); } Future fetchChallenges() async { final response = await authHttpRequest( context: context, url: 'https://group-1-15.pvt.dsv.su.se/api/challenges/difficulty/${widget.difficulty}', // http://10.0.2.2:8080 https://group-1-15.pvt.dsv.su.se ); if (response.statusCode == 200) { challenges = jsonDecode(utf8.decode(response.bodyBytes)) as Map; } } void _loadChallenges() async { setState(() { wheelItems = challenges.keys.toList(); }); } Widget _buildLeaderboardPodium() { final count = participants.length.clamp(1, 3); final barHeights = { 1: [100.0], 2: [80.0, 110.0], 3: [80.0, 110.0, 60.0], }[count]!; final visualOrder = switch (count) { 1 => [0], 2 => [1, 0], 3 => [1, 0, 2], _ => [0], }; return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), constraints: BoxConstraints(maxHeight: 160, maxWidth: 370), decoration: BoxDecoration( color: const Color(0x99FED4FF), borderRadius: BorderRadius.circular(24), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: List.generate(count, (i) { final index = visualOrder[i]; final participant = participants[index]; return Row( children: [ if (i > 0) const SizedBox(width: 12), _podiumSpot( position: index + 1, name: participant['name'] ?? 'Okänd', barHeight: barHeights[i], ), ], ); }), ), ); } Widget _podiumSpot({ required int position, required String name, required double barHeight, }) { return Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 70, height: barHeight, alignment: Alignment.topCenter, padding: const EdgeInsets.only(top: 8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.85), borderRadius: BorderRadius.circular(8), ), child: Text( position.toString(), style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.deepPurple, ), ), ), const SizedBox(height: 4), Text( name, style: GoogleFonts.poppins(fontSize: 12, color: Colors.white), ), ], ); } }