1028 lines
No EOL
42 KiB
Dart
1028 lines
No EOL
42 KiB
Dart
// ===========================================================================
|
|
// FILE: lib/ui/home/home_screen.dart
|
|
// ===========================================================================
|
|
|
|
import 'dart:ui';
|
|
import 'dart:math';
|
|
import 'dart:io' show Platform;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'dart:async';
|
|
import 'package:app_links/app_links.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:upgrader/upgrader.dart';
|
|
import 'package:in_app_update/in_app_update.dart';
|
|
|
|
import '../../logic/game_controller.dart';
|
|
import '../../core/theme_manager.dart';
|
|
import '../../core/app_colors.dart';
|
|
import '../../services/storage_service.dart';
|
|
import '../../services/audio_service.dart';
|
|
import '../../services/multiplayer_service.dart';
|
|
import '../multiplayer/lobby_screen.dart';
|
|
import '../admin/admin_screen.dart';
|
|
import '../settings/settings_screen.dart';
|
|
import '../game/game_screen.dart';
|
|
import 'package:tetraq/l10n/app_localizations.dart';
|
|
|
|
import '../../widgets/painters.dart';
|
|
import '../../widgets/cyber_border.dart';
|
|
import '../../widgets/music_theme_widgets.dart';
|
|
import '../../widgets/home_buttons.dart';
|
|
import 'dialog.dart';
|
|
import 'home_modals.dart';
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
State<HomeScreen> createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|
|
|
int _debugTapCount = 0;
|
|
late AppLinks _appLinks;
|
|
StreamSubscription<Uri>? _linkSubscription;
|
|
StreamSubscription<QuerySnapshot>? _favoritesSubscription;
|
|
StreamSubscription<QuerySnapshot>? _invitesSubscription;
|
|
|
|
Map<String, DateTime> _lastOnlineNotifications = {};
|
|
|
|
final int _selectedRadius = 4;
|
|
final ArenaShape _selectedShape = ArenaShape.classic;
|
|
final bool _isPublicRoom = true;
|
|
|
|
bool _isLoading = false;
|
|
String? _myRoomCode;
|
|
bool _roomStarted = false;
|
|
|
|
String _appVersion = '';
|
|
bool _updateAvailable = false;
|
|
|
|
final MultiplayerService _multiplayerService = MultiplayerService();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (StorageService.instance.playerName.isEmpty) {
|
|
HomeModals.showNameDialog(context, () {
|
|
StorageService.instance.syncLeaderboard();
|
|
_listenToInvites();
|
|
setState(() {});
|
|
});
|
|
} else {
|
|
StorageService.instance.syncLeaderboard();
|
|
_listenToInvites();
|
|
}
|
|
_checkThemeSafety();
|
|
});
|
|
_checkClipboardForInvite();
|
|
_initDeepLinks();
|
|
_listenToFavoritesOnline();
|
|
_loadAppVersion();
|
|
_checkStoreForUpdate();
|
|
}
|
|
|
|
Future<void> _loadAppVersion() async {
|
|
try {
|
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
|
if (mounted) {
|
|
setState(() {
|
|
_appVersion = "v. ${packageInfo.version}";
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Errore lettura versione: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> _checkStoreForUpdate() async {
|
|
|
|
if (kIsWeb) return;
|
|
try {
|
|
if (Platform.isAndroid) {
|
|
final info = await InAppUpdate.checkForUpdate();
|
|
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
|
|
if (mounted) setState(() => _updateAvailable = true);
|
|
}
|
|
} else if (Platform.isIOS || Platform.isMacOS) {
|
|
final upgrader = Upgrader();
|
|
await upgrader.initialize();
|
|
if (upgrader.isUpdateAvailable()) {
|
|
if (mounted) setState(() => _updateAvailable = true);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Errore controllo aggiornamenti: $e");
|
|
}
|
|
|
|
}
|
|
|
|
void _triggerUpdate() async {
|
|
if (kIsWeb) return;
|
|
if (Platform.isAndroid) {
|
|
try {
|
|
final info = await InAppUpdate.checkForUpdate();
|
|
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
|
|
await InAppUpdate.performImmediateUpdate();
|
|
}
|
|
} catch(e) {
|
|
Upgrader().sendUserToAppStore();
|
|
}
|
|
} else {
|
|
Upgrader().sendUserToAppStore();
|
|
}
|
|
}
|
|
|
|
void _checkThemeSafety() {
|
|
String themeStr = StorageService.instance.getTheme();
|
|
bool exists = AppThemeType.values.any((e) => e.toString() == themeStr);
|
|
if (!exists) {
|
|
context.read<ThemeManager>().setTheme(AppThemeType.doodle);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_cleanupGhostRoom();
|
|
_linkSubscription?.cancel();
|
|
_favoritesSubscription?.cancel();
|
|
_invitesSubscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) {
|
|
_checkClipboardForInvite();
|
|
_listenToFavoritesOnline();
|
|
} else if (state == AppLifecycleState.detached) {
|
|
_cleanupGhostRoom();
|
|
}
|
|
}
|
|
|
|
void _cleanupGhostRoom() {
|
|
if (_myRoomCode != null && !_roomStarted) {
|
|
FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete();
|
|
_myRoomCode = null;
|
|
}
|
|
}
|
|
|
|
Future<void> _initDeepLinks() async {
|
|
_appLinks = AppLinks();
|
|
try {
|
|
final initialUri = await _appLinks.getInitialLink();
|
|
if (initialUri != null) _handleDeepLink(initialUri);
|
|
} catch (e) { debugPrint("Errore lettura link iniziale: $e"); }
|
|
_linkSubscription = _appLinks.uriLinkStream.listen((uri) { _handleDeepLink(uri); }, onError: (err) { debugPrint("Errore stream link: $err"); });
|
|
}
|
|
|
|
void _handleDeepLink(Uri uri) {
|
|
if (uri.scheme == 'tetraq' && uri.host == 'join') {
|
|
String? code = uri.queryParameters['code'];
|
|
if (code != null && code.length == 5) {
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
if (mounted) HomeModals.showJoinPromptDialog(context, code.toUpperCase(), _joinRoomByCode);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _checkClipboardForInvite() async {
|
|
try {
|
|
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
|
String? text = data?.text;
|
|
if (text != null && text.contains("TetraQ") && text.contains("codice:")) {
|
|
RegExp regExp = RegExp(r'codice:\s*([A-Z0-9]{5})', caseSensitive: false);
|
|
Match? match = regExp.firstMatch(text);
|
|
if (match != null) {
|
|
String roomCode = match.group(1)!.toUpperCase();
|
|
await Clipboard.setData(const ClipboardData(text: ''));
|
|
if (mounted && ModalRoute.of(context)?.isCurrent == true) {
|
|
HomeModals.showJoinPromptDialog(context, roomCode, _joinRoomByCode);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) { debugPrint("Errore lettura appunti: $e"); }
|
|
}
|
|
|
|
void _listenToFavoritesOnline() {
|
|
_favoritesSubscription?.cancel();
|
|
final favs = StorageService.instance.favorites;
|
|
if (favs.isEmpty) return;
|
|
|
|
List<String> favUids = favs.map((f) => f['uid']!).toList();
|
|
if (favUids.length > 10) favUids = favUids.sublist(0, 10);
|
|
|
|
_favoritesSubscription = FirebaseFirestore.instance
|
|
.collection('leaderboard')
|
|
.where(FieldPath.documentId, whereIn: favUids)
|
|
.snapshots()
|
|
.listen((snapshot) {
|
|
if (!mounted) return;
|
|
|
|
for (var change in snapshot.docChanges) {
|
|
if (change.type == DocumentChangeType.modified || change.type == DocumentChangeType.added) {
|
|
var data = change.doc.data();
|
|
if (data != null && data['lastActive'] != null) {
|
|
Timestamp lastActive = data['lastActive'];
|
|
int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds;
|
|
|
|
if (diffInSeconds.abs() < 180) {
|
|
String name = data['name'] ?? 'Un amico';
|
|
if (ModalRoute.of(context)?.isCurrent == true) {
|
|
_showFavoriteOnlinePopup(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _showFavoriteOnlinePopup(String name) {
|
|
if (!mounted) return;
|
|
|
|
if (_lastOnlineNotifications.containsKey(name)) {
|
|
if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 1) return;
|
|
}
|
|
_lastOnlineNotifications[name] = DateTime.now();
|
|
|
|
final overlay = Overlay.of(context);
|
|
late OverlayEntry entry;
|
|
bool removed = false;
|
|
|
|
entry = OverlayEntry(
|
|
builder: (context) => Positioned(
|
|
top: MediaQuery.of(context).padding.top + 85,
|
|
left: 20,
|
|
right: 20,
|
|
child: FavoriteOnlinePopup(
|
|
name: name,
|
|
onDismiss: () {
|
|
if (!removed) {
|
|
removed = true;
|
|
entry.remove();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
);
|
|
overlay.insert(entry);
|
|
}
|
|
|
|
void _listenToInvites() {
|
|
final user = FirebaseAuth.instance.currentUser;
|
|
if (user == null) return;
|
|
|
|
_invitesSubscription?.cancel();
|
|
_invitesSubscription = FirebaseFirestore.instance
|
|
.collection('invites')
|
|
.where('toUid', isEqualTo: user.uid)
|
|
.snapshots()
|
|
.listen((snapshot) {
|
|
if (!mounted) return;
|
|
|
|
for (var change in snapshot.docChanges) {
|
|
if (change.type == DocumentChangeType.added) {
|
|
var data = change.doc.data();
|
|
if (data != null) {
|
|
String code = data['roomCode'];
|
|
String from = data['fromName'];
|
|
String inviteId = change.doc.id;
|
|
|
|
Timestamp? ts = data['timestamp'];
|
|
if (ts != null) {
|
|
if (DateTime.now().difference(ts.toDate()).inMinutes > 2) {
|
|
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (ModalRoute.of(context)?.isCurrent == true) {
|
|
_showInvitePopup(from, code, inviteId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _showInvitePopup(String fromName, String roomCode, String inviteId) {
|
|
final themeType = context.read<ThemeManager>().currentThemeType;
|
|
final theme = context.read<ThemeManager>().currentColors;
|
|
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: themeType == AppThemeType.doodle ? Colors.white : theme.background,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: theme.playerRed, width: 2)),
|
|
title: Row(
|
|
children: [
|
|
Icon(Icons.warning_amber_rounded, color: theme.playerRed),
|
|
const SizedBox(width: 10),
|
|
Text("SFIDA IN ARRIVO!", style: getSharedTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 18))),
|
|
],
|
|
),
|
|
content: Text("$fromName ti ha sfidato a duello!\nAccetti la sfida?", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: Text("RIFIUTA", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.grey, fontWeight: FontWeight.bold))),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
|
onPressed: () {
|
|
FirebaseFirestore.instance.collection('invites').doc(inviteId).delete();
|
|
Navigator.pop(ctx);
|
|
_joinRoomByCode(roomCode);
|
|
},
|
|
child: Text("ACCETTA!", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
|
|
),
|
|
],
|
|
)
|
|
);
|
|
}
|
|
|
|
void _startDirectChallengeFlow(String targetUid, String targetName) {
|
|
HomeModals.showChallengeSetupDialog(
|
|
context,
|
|
targetName,
|
|
(int radius, ArenaShape shape, String timeMode) {
|
|
_executeSendChallenge(targetUid, targetName, radius, shape, timeMode);
|
|
}
|
|
);
|
|
}
|
|
|
|
Future<void> _executeSendChallenge(String targetUid, String targetName, int radius, ArenaShape shape, String timeMode) async {
|
|
setState(() => _isLoading = true);
|
|
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
final rnd = Random();
|
|
String roomCode = String.fromCharCodes(Iterable.generate(5, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
|
|
|
|
try {
|
|
int gameSeed = rnd.nextInt(9999999);
|
|
|
|
await FirebaseFirestore.instance.collection('games').doc(roomCode).set({
|
|
'status': 'waiting',
|
|
'hostName': StorageService.instance.playerName,
|
|
'hostUid': FirebaseAuth.instance.currentUser?.uid,
|
|
'radius': radius,
|
|
'shape': shape.name,
|
|
'timeMode': timeMode,
|
|
'isPublic': false,
|
|
'createdAt': FieldValue.serverTimestamp(),
|
|
'players': [FirebaseAuth.instance.currentUser?.uid],
|
|
'turn': 0,
|
|
'moves': [],
|
|
'seed': gameSeed,
|
|
});
|
|
|
|
await FirebaseFirestore.instance.collection('invites').add({
|
|
'toUid': targetUid,
|
|
'fromName': StorageService.instance.playerName,
|
|
'roomCode': roomCode,
|
|
'timestamp': FieldValue.serverTimestamp(),
|
|
});
|
|
|
|
setState(() => _isLoading = false);
|
|
|
|
if (mounted) {
|
|
HomeModals.showWaitingDialog(
|
|
context: context,
|
|
code: roomCode,
|
|
isPublicRoom: false,
|
|
selectedRadius: radius,
|
|
selectedShape: shape,
|
|
selectedTimeMode: timeMode,
|
|
multiplayerService: _multiplayerService,
|
|
onRoomStarted: () {},
|
|
onCleanup: () {
|
|
FirebaseFirestore.instance.collection('games').doc(roomCode).delete();
|
|
}
|
|
);
|
|
}
|
|
} catch (e) {
|
|
setState(() => _isLoading = false);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e", style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _joinRoomByCode(String code) async {
|
|
if (_isLoading) return;
|
|
FocusScope.of(context).unfocus();
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
String playerName = StorageService.instance.playerName;
|
|
if (playerName.isEmpty) playerName = "GUEST";
|
|
|
|
Map<String, dynamic>? roomData = await _multiplayerService.joinGameRoom(code, playerName);
|
|
|
|
if (!mounted) return;
|
|
setState(() => _isLoading = false);
|
|
|
|
if (roomData != null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("La sfida inizierà a breve..."), backgroundColor: Colors.green));
|
|
|
|
int hostRadius = roomData['radius'] ?? 4;
|
|
String shapeStr = roomData['shape'] ?? 'classic';
|
|
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
|
|
|
|
String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax');
|
|
|
|
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
|
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore di connessione: $e", style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
|
}
|
|
}
|
|
}
|
|
|
|
BoxDecoration _glassBoxDecoration(ThemeColors theme, AppThemeType themeType) {
|
|
return BoxDecoration(
|
|
color: themeType == AppThemeType.doodle ? Colors.white : null,
|
|
gradient: themeType == AppThemeType.doodle ? null : LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
Colors.white.withOpacity(0.25),
|
|
Colors.white.withOpacity(0.05),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(25),
|
|
border: Border.all(
|
|
color: themeType == AppThemeType.doodle ? theme.text : Colors.white.withOpacity(0.3),
|
|
width: themeType == AppThemeType.doodle ? 2 : 1.5,
|
|
),
|
|
boxShadow: themeType == AppThemeType.doodle
|
|
? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 4), blurRadius: 0)]
|
|
: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 10)],
|
|
);
|
|
}
|
|
|
|
Widget _buildTopBar(BuildContext context, ThemeColors theme, AppThemeType themeType, String playerName, int playerLevel) {
|
|
Color inkColor = const Color(0xFF111122);
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 5.0, left: 15.0, right: 15.0, bottom: 10.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () => HomeModals.showNameDialog(context, () => setState(() {})),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: _glassBoxDecoration(theme, themeType),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 18,
|
|
backgroundColor: theme.playerBlue.withOpacity(0.2),
|
|
child: Icon(Icons.person, color: theme.playerBlue, size: 20),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
playerName.toUpperCase(),
|
|
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold, fontSize: 16)),
|
|
overflow: TextOverflow.visible,
|
|
softWrap: false,
|
|
),
|
|
Text(
|
|
"LIV. $playerLevel",
|
|
style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 11)),
|
|
overflow: TextOverflow.visible,
|
|
softWrap: false,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// --- BOX STATISTICHE ---
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: _glassBoxDecoration(theme, themeType),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.microphone : Icons.emoji_events, color: Colors.amber.shade600, size: 16),
|
|
const SizedBox(width: 6),
|
|
|
|
Text(
|
|
"${StorageService.instance.wins}",
|
|
style: getSharedTextStyle(themeType, TextStyle(
|
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
|
fontWeight: FontWeight.w900,
|
|
fontSize: 16,
|
|
)),
|
|
overflow: TextOverflow.visible,
|
|
softWrap: false,
|
|
),
|
|
|
|
const SizedBox(width: 10),
|
|
Icon(themeType == AppThemeType.music ? FontAwesomeIcons.compactDisc : Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 16),
|
|
const SizedBox(width: 6),
|
|
|
|
Text(
|
|
"${StorageService.instance.losses}",
|
|
style: getSharedTextStyle(themeType, TextStyle(
|
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
|
fontWeight: FontWeight.w900,
|
|
fontSize: 16,
|
|
)),
|
|
overflow: TextOverflow.visible,
|
|
softWrap: false,
|
|
),
|
|
|
|
const SizedBox(width: 10),
|
|
Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)),
|
|
const SizedBox(width: 10),
|
|
|
|
AnimatedBuilder(
|
|
animation: AudioService.instance,
|
|
builder: (context, child) {
|
|
bool isMuted = AudioService.instance.isMuted;
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
AudioService.instance.toggleMute();
|
|
},
|
|
child: Icon(
|
|
isMuted ? Icons.volume_off : Icons.volume_up,
|
|
color: isMuted ? theme.playerRed : (themeType == AppThemeType.doodle ? inkColor : theme.text),
|
|
size: 20,
|
|
),
|
|
);
|
|
}
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCyberCard(Widget card, AppThemeType themeType) {
|
|
if (themeType == AppThemeType.cyberpunk) return AnimatedCyberBorder(child: card);
|
|
return card;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
|
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
|
|
|
final themeManager = context.watch<ThemeManager>();
|
|
final themeType = themeManager.currentThemeType;
|
|
final theme = themeManager.currentColors;
|
|
Color inkColor = const Color(0xFF111122);
|
|
final loc = AppLocalizations.of(context)!;
|
|
|
|
String? bgImage;
|
|
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
|
|
if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
|
|
if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg';
|
|
if (themeType == AppThemeType.arcade) bgImage = 'assets/images/arcade.jpg';
|
|
if (themeType == AppThemeType.grimorio) bgImage = 'assets/images/grimorio.jpg';
|
|
|
|
String playerName = StorageService.instance.playerName;
|
|
if (playerName.isEmpty) playerName = "GUEST";
|
|
int playerLevel = StorageService.instance.playerLevel;
|
|
|
|
final double screenHeight = MediaQuery.of(context).size.height;
|
|
final double vScale = (screenHeight / 920.0).clamp(0.50, 1.0);
|
|
|
|
Widget uiContent = SafeArea(
|
|
child: Column(
|
|
children: [
|
|
_buildTopBar(context, theme, themeType, playerName, playerLevel),
|
|
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SizedBox(height: 20 * vScale),
|
|
Center(
|
|
child: Transform.rotate(
|
|
angle: themeType == AppThemeType.doodle ? -0.04 : 0,
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
_debugTapCount++;
|
|
|
|
// CHEAT LOCALE VIVO SOLO IN DEBUG MODE (ORA CON PIPPO!)
|
|
if (kDebugMode && playerName.toUpperCase() == 'PIPPO' && _debugTapCount == 5) {
|
|
StorageService.instance.addXP(2000);
|
|
setState(() {});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text("🛠 DEBUG MODE: +20 Livelli!", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), backgroundColor: Colors.purpleAccent, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)))
|
|
);
|
|
}
|
|
// ACCESSO DASHBOARD
|
|
else if (_debugTapCount >= 7) {
|
|
_debugTapCount = 0;
|
|
|
|
if (kDebugMode && playerName.toUpperCase() == 'PIPPO') {
|
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminScreen()));
|
|
} else {
|
|
bool isAdmin = await StorageService.instance.isUserAdmin();
|
|
|
|
if (isAdmin && mounted) {
|
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminScreen()));
|
|
} else if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text("Accesso Negato: Non sei un Amministratore 🛑", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
|
|
backgroundColor: Colors.redAccent,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
loc.appTitle.toUpperCase(),
|
|
style: getSharedTextStyle(themeType, TextStyle(
|
|
fontSize: 65 * vScale,
|
|
fontWeight: FontWeight.w900,
|
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
|
letterSpacing: 10 * vScale,
|
|
shadows: themeType == AppThemeType.doodle
|
|
? [const Shadow(color: Colors.white, offset: Offset(2.5, 2.5), blurRadius: 2), const Shadow(color: Colors.white, offset: Offset(-2.5, -2.5), blurRadius: 2)]
|
|
: [Shadow(color: Colors.black.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 8), Shadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20)]
|
|
)),
|
|
overflow: TextOverflow.visible,
|
|
softWrap: false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 40 * vScale),
|
|
|
|
if (themeType == AppThemeType.music) ...[
|
|
MusicCassetteCard(title: loc.onlineTitle, subtitle: loc.onlineSub, neonColor: Colors.blueAccent, angle: -0.04, leftIcon: FontAwesomeIcons.sliders, rightIcon: FontAwesomeIcons.globe, themeType: themeType, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen())); }),
|
|
SizedBox(height: 12 * vScale),
|
|
MusicCassetteCard(title: loc.cpuTitle, subtitle: loc.cpuSub, neonColor: Colors.purpleAccent, angle: 0.03, leftIcon: FontAwesomeIcons.desktop, rightIcon: FontAwesomeIcons.music, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, true)),
|
|
SizedBox(height: 12 * vScale),
|
|
MusicCassetteCard(title: loc.localTitle, subtitle: loc.localSub, neonColor: Colors.deepPurpleAccent, angle: -0.02, leftIcon: FontAwesomeIcons.headphones, rightIcon: FontAwesomeIcons.headphones, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, false)),
|
|
SizedBox(height: 30 * vScale),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)))),
|
|
Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))),
|
|
Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))),
|
|
Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))),
|
|
],
|
|
),
|
|
] else ...[
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildCyberCard(FeatureCard(title: loc.onlineTitle, subtitle: loc.onlineSub, icon: Icons.public, color: Colors.lightBlue.shade200, theme: theme, themeType: themeType, isFeatured: true, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen())); }), themeType),
|
|
SizedBox(height: 12 * vScale),
|
|
_buildCyberCard(FeatureCard(title: loc.cpuTitle, subtitle: loc.cpuSub, icon: Icons.smart_toy, color: Colors.purple.shade200, theme: theme, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, true)), themeType),
|
|
SizedBox(height: 12 * vScale),
|
|
_buildCyberCard(FeatureCard(title: loc.localTitle, subtitle: loc.localSub, icon: Icons.people_alt, color: Colors.red.shade200, theme: theme, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, false)), themeType),
|
|
SizedBox(height: 12 * vScale),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)), compact: true), themeType)),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)),
|
|
],
|
|
),
|
|
|
|
SizedBox(height: 12 * vScale),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildCyberCard(FeatureCard(title: loc.themesTitle, subtitle: "Personalizza", icon: Icons.palette, color: Colors.teal.shade200, theme: theme, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())), compact: true), themeType)),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: _buildCyberCard(FeatureCard(title: loc.tutorialTitle, subtitle: "Come giocare", icon: Icons.school, color: Colors.indigo.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()), compact: true), themeType)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
SizedBox(height: 40 * vScale),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
return Scaffold(
|
|
backgroundColor: bgImage != null ? Colors.transparent : theme.background,
|
|
extendBodyBehindAppBar: true,
|
|
body: Stack(
|
|
children: [
|
|
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
|
|
|
|
if (themeType == AppThemeType.doodle)
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
|
|
),
|
|
),
|
|
|
|
if (bgImage != null)
|
|
Positioned.fill(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: AssetImage(bgImage!),
|
|
fit: BoxFit.cover,
|
|
colorFilter: themeType == AppThemeType.doodle
|
|
? ColorFilter.mode(Colors.white.withOpacity(0.5), BlendMode.lighten)
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music || themeType == AppThemeType.arcade || themeType == AppThemeType.grimorio))
|
|
Positioned.fill(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter, end: Alignment.bottomCenter,
|
|
colors: [Colors.black.withOpacity(0.4), Colors.black.withOpacity(0.8)]
|
|
)
|
|
),
|
|
),
|
|
),
|
|
|
|
if (themeType == AppThemeType.music)
|
|
Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: CustomPaint(
|
|
painter: AudioCablesPainter(),
|
|
),
|
|
),
|
|
),
|
|
|
|
Positioned.fill(child: uiContent),
|
|
|
|
// --- NUMERO DI VERSIONE APP E BADGE AGGIORNAMENTO ---
|
|
if (_appVersion.isNotEmpty)
|
|
Positioned(
|
|
bottom: MediaQuery.of(context).padding.bottom + 10,
|
|
left: 20,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Opacity(
|
|
opacity: 0.6,
|
|
child: Text(
|
|
_appVersion,
|
|
style: getSharedTextStyle(themeType, TextStyle(
|
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 1.0,
|
|
)),
|
|
),
|
|
),
|
|
if (_updateAvailable) ...[
|
|
const SizedBox(width: 15),
|
|
_PulsingUpdateBadge(
|
|
themeType: themeType,
|
|
theme: theme,
|
|
onTap: _triggerUpdate,
|
|
),
|
|
]
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// --- NUOVO WIDGET: BADGE AGGIORNAMENTO PULSANTE ---
|
|
class _PulsingUpdateBadge extends StatefulWidget {
|
|
final AppThemeType themeType;
|
|
final ThemeColors theme;
|
|
final VoidCallback onTap;
|
|
|
|
const _PulsingUpdateBadge({
|
|
required this.themeType,
|
|
required this.theme,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
State<_PulsingUpdateBadge> createState() => _PulsingUpdateBadgeState();
|
|
}
|
|
|
|
class _PulsingUpdateBadgeState extends State<_PulsingUpdateBadge> with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _scaleAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true);
|
|
_scaleAnimation = Tween<double>(begin: 0.95, end: 1.05).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Color badgeColor = widget.themeType == AppThemeType.doodle ? Colors.red.shade700 : widget.theme.playerRed;
|
|
Color textColor = widget.themeType == AppThemeType.doodle ? Colors.white : widget.theme.playerRed;
|
|
Color bgColor = widget.themeType == AppThemeType.doodle ? badgeColor : badgeColor.withOpacity(0.15);
|
|
|
|
return GestureDetector(
|
|
onTap: widget.onTap,
|
|
child: ScaleTransition(
|
|
scale: _scaleAnimation,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: bgColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: badgeColor, width: 1.5),
|
|
boxShadow: widget.themeType == AppThemeType.doodle
|
|
? [const BoxShadow(color: Colors.black26, offset: Offset(2, 2))]
|
|
: [BoxShadow(color: badgeColor.withOpacity(0.4), blurRadius: 8)],
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.system_update_alt, color: textColor, size: 14),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
"AGGIORNAMENTO DISPONIBILE",
|
|
style: getSharedTextStyle(widget.themeType, TextStyle(
|
|
color: textColor,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w900,
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
// ----------------------------------------------------
|
|
|
|
class FullScreenGridPainter extends CustomPainter {
|
|
final Color gridColor;
|
|
FullScreenGridPainter(this.gridColor);
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke;
|
|
double paperStep = 20.0;
|
|
for (double i = 0; i <= size.width; i += paperStep) canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
|
|
for (double i = 0; i <= size.height; i += paperStep) canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|
|
|
|
class FavoriteOnlinePopup extends StatefulWidget {
|
|
final String name;
|
|
final VoidCallback onDismiss;
|
|
|
|
const FavoriteOnlinePopup({super.key, required this.name, required this.onDismiss});
|
|
|
|
@override
|
|
State<FavoriteOnlinePopup> createState() => _FavoriteOnlinePopupState();
|
|
}
|
|
|
|
class _FavoriteOnlinePopupState extends State<FavoriteOnlinePopup> with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<Offset> _offsetAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
|
|
_offsetAnimation = Tween<Offset>(begin: const Offset(0.0, -1.5), end: Offset.zero)
|
|
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack));
|
|
|
|
_controller.forward();
|
|
|
|
Future.delayed(const Duration(seconds: 3), () {
|
|
if (mounted) {
|
|
_controller.reverse().then((_) => widget.onDismiss());
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final themeManager = context.watch<ThemeManager>();
|
|
final themeType = themeManager.currentThemeType;
|
|
final theme = themeManager.currentColors;
|
|
Color inkColor = const Color(0xFF111122);
|
|
|
|
return SlideTransition(
|
|
position: _offsetAnimation,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
elevation: 100,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: themeType == AppThemeType.doodle ? Colors.white : theme.background,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue,
|
|
width: 2
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5)
|
|
)
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.circle, color: Colors.greenAccent, size: 14),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
"${widget.name} è online!",
|
|
style: getSharedTextStyle(
|
|
themeType,
|
|
TextStyle(
|
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 15
|
|
)
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |