HopSpotFrontend/lib/pages/wheel_page.dart

671 lines
19 KiB
Dart

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<WheelPage> createState() => _WheelPageState();
}
class _WheelPageState extends State<WheelPage> {
final StreamController<int> controller = StreamController<int>();
String ownPlayerId = "";
bool isMyTurn = false;
int round = 1;
int playerTurn = 1;
String currentPlayerUsername = "";
Map<int, dynamic> turnOrder = {};
Map<String, dynamic> challenges = {};
List<Map<String, dynamic>> 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<String> 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<void> initializeEverything() async {
final user = Provider.of<UserProvider>(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<String> 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<String, dynamic> && 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<void> 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<dynamic> data = jsonDecode(utf8.decode(response.bodyBytes));
setState(() {
participants =
data
.map<Map<String, dynamic>>(
(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<void> 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<String, dynamic> && player.containsKey('username')) {
currentPlayerUsername = player['username'];
}
}
}
void checkIfMyTurn() {
final currentPlayer = turnOrder[playerTurn];
final currentPlayerId =
currentPlayer is Map<String, dynamic> ? currentPlayer['userId'] : null;
isMyTurn = (ownPlayerId == currentPlayerId);
}
void handlefetchTurnOrder() async {
await fetchTurnOrder();
}
Future<void> 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<String, dynamic> decoded = jsonDecode(
utf8.decode(response.bodyBytes),
);
turnOrder = decoded.map((key, value) => MapEntry(int.parse(key), value));
checkIfMyTurn();
updateCurrentPlayerUsername();
}
}
Future<void> 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<void> _showLeaveConfirmationDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Lämna spelet?'),
content: const SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('Är du säker på att du vill lämna spelet?'),
],
),
),
actions: <Widget>[
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: <FortuneIndicator>[_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<Color> segmentColors = [Color(0xFFFF6B81), Color(0xFFA88BEB)];
List<FortuneItem> _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<void> 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<dynamic> challengeList = challenges[selectedTitle];
challengeList.removeWhere(
(challenge) =>
challenge is Map<String, dynamic> &&
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<void> 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<String, dynamic>;
}
}
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),
),
],
);
}
}