refactorización de pantallas

This commit is contained in:
2026-05-11 23:16:38 +02:00
parent 1929d86689
commit 4599678e77
48 changed files with 1446 additions and 1463 deletions
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "تصويت {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "يبدأ {name} بقول كلمته.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Vot de {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "Comença {name} dient la seva paraula.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Stimme von {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} beginnt und sagt sein/ihr Wort.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -333,5 +333,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Vote from {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} starts by saying their word.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -369,5 +369,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Voto de {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "Empieza {name} diciendo su palabra.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "{name}(r)en botoa",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} hasiko da bere hitza esanez.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Vote de {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} commence en disant son mot.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "{name} का वोट",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} अपनी शब्द बोलकर शुरू करता है।",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Voto di {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} inizia dicendo la sua parola.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "{name} の投票",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} が自分のワードを言って始めます。",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "{name}의 투표",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name}님이 자신의 단어를 말하며 시작합니다.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Stem van {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} begint door het woord te zeggen.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Głos gracza {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} zaczyna, mówiąc swoje słowo.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Voto de {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} começa dizendo a sua palavra.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "Голос игрока {name}",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} начинает, называя своё слово.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "{name} için oy",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} kelimesini söyleyerek başlar.",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "{name} 的投票",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} 先说出自己的词。",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+16
View File
@@ -301,5 +301,21 @@
"type": "String" "type": "String"
} }
} }
},
"voteOf": "{name} 的投票",
"@voteOf": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"firstTurnInstruction": "{name} 先說出自己的詞。",
"@firstTurnInstruction": {
"placeholders": {
"name": {
"type": "String"
}
}
} }
} }
+13
View File
@@ -520,6 +520,19 @@ abstract class AppLocalizations {
/// **'Jugadores en debate'** /// **'Jugadores en debate'**
String get playersInDebate; String get playersInDebate;
/// No description provided for @voteOf.
///
/// In en, this message translates to:
/// **'Vote from {name}'**
String voteOf(String name);
/// No description provided for @firstTurnInstruction.
///
/// In en, this message translates to:
/// **'{name} starts by saying their word.'**
String firstTurnInstruction(String name);
/// No description provided for @activePlayersInfo. /// No description provided for @activePlayersInfo.
/// ///
/// In es, this message translates to: /// In es, this message translates to:
@@ -213,6 +213,16 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get playersInDebate => 'اللاعبون في النقاش'; String get playersInDebate => 'اللاعبون في النقاش';
@override
String voteOf(String name) {
return "تصويت $name";
}
@override
String firstTurnInstruction(String name) {
return "يبدأ $name بقول كلمته.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active نشطون • $impostors منتحل(ون) مختبئون'; return '$active نشطون • $impostors منتحل(ون) مختبئون';
@@ -214,6 +214,16 @@ class AppLocalizationsCa extends AppLocalizations {
@override @override
String get playersInDebate => 'Jugadors en debat'; String get playersInDebate => 'Jugadors en debat';
@override
String voteOf(String name) {
return "Vot de $name";
}
@override
String firstTurnInstruction(String name) {
return "Comença $name dient la seva paraula.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active actius • $impostors impostor(s) ocults'; return '$active actius • $impostors impostor(s) ocults';
@@ -216,6 +216,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get playersInDebate => 'Spieler in der Diskussion'; String get playersInDebate => 'Spieler in der Diskussion';
@override
String voteOf(String name) {
return "Stimme von $name";
}
@override
String firstTurnInstruction(String name) {
return "$name beginnt und sagt sein/ihr Wort.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active aktiv • $impostors versteckte(r) Hochstapler'; return '$active aktiv • $impostors versteckte(r) Hochstapler';
@@ -213,6 +213,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get playersInDebate => 'Players in discussion'; String get playersInDebate => 'Players in discussion';
@override
String voteOf(String name) {
return "Vote from $name";
}
@override
String firstTurnInstruction(String name) {
return "$name starts by saying their word.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active active • $impostors hidden impostor(s)'; return '$active active • $impostors hidden impostor(s)';
@@ -213,6 +213,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get playersInDebate => 'Jugadores en debate'; String get playersInDebate => 'Jugadores en debate';
@override
String voteOf(String name) {
return "Voto de $name";
}
@override
String firstTurnInstruction(String name) {
return "Empieza $name diciendo su palabra.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active activos • $impostors impostor(es) ocultos'; return '$active activos • $impostors impostor(es) ocultos';
@@ -216,6 +216,16 @@ class AppLocalizationsEu extends AppLocalizations {
@override @override
String get playersInDebate => 'Eztabaidan diren jokalariak'; String get playersInDebate => 'Eztabaidan diren jokalariak';
@override
String voteOf(String name) {
return "$name(r)en botoa";
}
@override
String firstTurnInstruction(String name) {
return "$name hasiko da bere hitza esanez.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active aktibo • $impostors inpostore ezkutu'; return '$active aktibo • $impostors inpostore ezkutu';
@@ -214,6 +214,16 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get playersInDebate => 'Joueurs en débat'; String get playersInDebate => 'Joueurs en débat';
@override
String voteOf(String name) {
return "Vote de $name";
}
@override
String firstTurnInstruction(String name) {
return "$name commence en disant son mot.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active actifs • $impostors imposteur(s) caché(s)'; return '$active actifs • $impostors imposteur(s) caché(s)';
@@ -213,6 +213,16 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get playersInDebate => 'बहस में खिलाड़ी'; String get playersInDebate => 'बहस में खिलाड़ी';
@override
String voteOf(String name) {
return "$name का वोट";
}
@override
String firstTurnInstruction(String name) {
return "$name अपनी शब्द बोलकर शुरू करता है।";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active सक्रिय • $impostors धोखेबाज़ छिपे हुए'; return '$active सक्रिय • $impostors धोखेबाज़ छिपे हुए';
@@ -214,6 +214,16 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get playersInDebate => 'Giocatori in discussione'; String get playersInDebate => 'Giocatori in discussione';
@override
String voteOf(String name) {
return "Voto di $name";
}
@override
String firstTurnInstruction(String name) {
return "$name inizia dicendo la sua parola.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active attivi • $impostors impostore/i nascosti'; return '$active attivi • $impostors impostore/i nascosti';
@@ -213,6 +213,16 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get playersInDebate => '議論中のプレイヤー'; String get playersInDebate => '議論中のプレイヤー';
@override
String voteOf(String name) {
return "$name の投票";
}
@override
String firstTurnInstruction(String name) {
return "$name が自分のワードを言って始めます。";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active 人参加中 • $impostors 人のインポスターが潜伏中'; return '$active 人参加中 • $impostors 人のインポスターが潜伏中';
@@ -213,6 +213,16 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get playersInDebate => '토론 중인 플레이어'; String get playersInDebate => '토론 중인 플레이어';
@override
String voteOf(String name) {
return "$name의 투표";
}
@override
String firstTurnInstruction(String name) {
return "$name님이 자신의 단어를 말하며 시작합니다.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active명 참여 중 • $impostors명의 임포스터 잠복 중'; return '$active명 참여 중 • $impostors명의 임포스터 잠복 중';
@@ -214,6 +214,16 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get playersInDebate => 'Spelers in debat'; String get playersInDebate => 'Spelers in debat';
@override
String voteOf(String name) {
return "Stem van $name";
}
@override
String firstTurnInstruction(String name) {
return "$name begint door het woord te zeggen.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active actief • $impostors verborgen bedrieger(s)'; return '$active actief • $impostors verborgen bedrieger(s)';
@@ -214,6 +214,16 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get playersInDebate => 'Gracze w debacie'; String get playersInDebate => 'Gracze w debacie';
@override
String voteOf(String name) {
return "Głos gracza $name";
}
@override
String firstTurnInstruction(String name) {
return "$name zaczyna, mówiąc swoje słowo.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active aktywnych • $impostors ukrytych oszustów'; return '$active aktywnych • $impostors ukrytych oszustów';
@@ -215,6 +215,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get playersInDebate => 'Jogadores no debate'; String get playersInDebate => 'Jogadores no debate';
@override
String voteOf(String name) {
return "Voto de $name";
}
@override
String firstTurnInstruction(String name) {
return "$name começa dizendo a sua palavra.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active ativos • $impostors impostor(es) ocultos'; return '$active ativos • $impostors impostor(es) ocultos';
@@ -214,6 +214,16 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get playersInDebate => 'Игроки в обсуждении'; String get playersInDebate => 'Игроки в обсуждении';
@override
String voteOf(String name) {
return "Голос игрока $name";
}
@override
String firstTurnInstruction(String name) {
return "$name начинает, называя своё слово.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active активных • $impostors скрытый(-х) самозванец(-ев)'; return '$active активных • $impostors скрытый(-х) самозванец(-ев)';
@@ -213,6 +213,16 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get playersInDebate => 'Tartışmadaki oyuncular'; String get playersInDebate => 'Tartışmadaki oyuncular';
@override
String voteOf(String name) {
return "$name için oy";
}
@override
String firstTurnInstruction(String name) {
return "$name kelimesini söyleyerek başlar.";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active aktif • $impostors gizli sahtekar'; return '$active aktif • $impostors gizli sahtekar';
@@ -213,6 +213,16 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get playersInDebate => '参与讨论的玩家'; String get playersInDebate => '参与讨论的玩家';
@override
String voteOf(String name) {
return "$name 的投票";
}
@override
String firstTurnInstruction(String name) {
return "$name 先说出自己的词。";
}
@override @override
String activePlayersInfo(int active, int impostors) { String activePlayersInfo(int active, int impostors) {
return '$active 名在场 • $impostors 名冒牌者潜伏中'; return '$active 名在场 • $impostors 名冒牌者潜伏中';
+77 -131
View File
@@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:farolero/l10n/generated/app_localizations.dart'; import 'package:farolero/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../estado/estado_juego.dart'; import '../estado/estado_juego.dart';
@@ -30,6 +30,22 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
setState(() => _acierto = resultado); setState(() => _acierto = resultado);
} }
void _continuarTrasNoAdivinar(EstadoJuego estado) {
final fin = estado.comprobarFinPartida();
if (fin) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const PantallaFinPartida()),
);
} else {
estado.siguienteRonda();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const PantallaDebate()),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
@@ -44,29 +60,40 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
), ),
body: FondoFarolero( body: FondoFarolero(
intenso: true, intenso: true,
child: SafeArea(
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(24),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const ArteGameplayFarolero.fase(height: 132), const ArteGameplayFarolero.fase(height: 132),
const SizedBox(height: 12), const SizedBox(height: 12),
EncabezadoFarolero( TarjetaFaseFarolero(
icono: Icons.theater_comedy, icono: Icons.theater_comedy,
titulo: l10n.impostorCanGuess, titulo: l10n.impostorCanGuess,
subtitulo: l10n.ifCorrectImpostorsWin, subtitulo: l10n.ifCorrectImpostorsWin,
color: TemaApp.colorAcento, color: TemaApp.colorAcento,
trailing: Image.asset( child: _buildContenido(context, l10n, estado, partida.palabraSecreta),
'assets/ui/generated/meta/result_verdict_art.webp', ),
width: 42, ],
height: 42,
opacity: const AlwaysStoppedAnimation(0.64),
), ),
), ),
const SizedBox(height: 32), ),
),
),
);
}
if (_acierto == null) ...[ Widget _buildContenido(
BuildContext context,
AppLocalizations l10n,
EstadoJuego estado,
String palabraSecreta,
) {
if (_acierto == null) {
return Column(
children: [
TextField( TextField(
controller: _controlador, controller: _controlador,
decoration: InputDecoration( decoration: InputDecoration(
@@ -76,167 +103,86 @@ class _PantallaAdivinanzaState extends State<PantallaAdivinanza> {
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20), style: const TextStyle(fontSize: 20),
onSubmitted: (_) => _intentarAdivinar(), onChanged: (value) => setState(() {}),
onSubmitted: (value) => _intentarAdivinar(),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: OutlinedButton( child: BotonFarolero.oscuro(
onPressed: () { texto: l10n.dontGuess,
// No intenta adivinar, siguiente ronda icono: Icons.skip_next,
final fin = estado.comprobarFinPartida(); onPressed: () => _continuarTrasNoAdivinar(estado),
if (fin) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const PantallaFinPartida(),
),
);
} else {
estado.siguienteRonda();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const PantallaDebate(),
),
);
}
},
child: Text(l10n.dontGuess),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
flex: 2, flex: 2,
child: ElevatedButton.icon( child: BotonFarolero(
onPressed: _controlador.text.trim().isNotEmpty texto: l10n.guess,
? _intentarAdivinar icono: Icons.send,
: null, onPressed: _controlador.text.trim().isNotEmpty ? _intentarAdivinar : null,
icon: const Icon(Icons.send),
label: Text(l10n.guess),
), ),
), ),
], ],
), ),
], ],
);
}
if (_acierto == true) ...[ final acierto = _acierto == true;
const SizedBox(height: 16), final color = acierto ? TemaApp.colorAcento : TemaApp.colorVerde;
Container( return Column(
children: [
PanelFarolero(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( borderColor: color,
color: TemaApp.colorAcento.withValues(alpha: 0.3), color: color.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: TemaApp.colorAcento),
),
child: Column( child: Column(
children: [ children: [
const Text('🎭🎉', style: TextStyle(fontSize: 48)), Icon(
acierto ? Icons.celebration : Icons.cancel,
color: color,
size: 52,
),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
l10n.correctGuess, acierto ? l10n.correctGuess : l10n.wrongGuess,
style: Theme.of(context) textAlign: TextAlign.center,
.textTheme style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: color),
.headlineMedium
?.copyWith(color: TemaApp.colorAcento),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
l10n.theWordWas(partida.palabraSecreta), acierto ? l10n.theWordWas(palabraSecreta) : l10n.gameContinues,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
if (acierto) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
l10n.impostorsWin, l10n.impostorsWin,
style: Theme.of(context).textTheme.bodyLarge?.copyWith( textAlign: TextAlign.center,
color: TemaApp.colorNaranja, style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: TemaApp.colorNaranja),
),
), ),
], ],
],
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( BotonFarolero(
width: double.infinity, texto: acierto ? l10n.seeEndResult : l10n.nextRound,
height: 56, icono: acierto ? Icons.emoji_events : Icons.skip_next,
child: ElevatedButton.icon( onPressed: acierto
onPressed: () { ? () {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (_) => const PantallaFinPartida()),
builder: (_) => const PantallaFinPartida(),
),
);
},
icon: const Icon(Icons.emoji_events),
label: Text(l10n.seeEndResult),
),
),
],
if (_acierto == false) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: TemaApp.colorVerde.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: TemaApp.colorVerde),
),
child: Column(
children: [
const Text('', style: TextStyle(fontSize: 48)),
const SizedBox(height: 12),
Text(
l10n.wrongGuess,
style: Theme.of(context)
.textTheme
.headlineMedium
?.copyWith(color: TemaApp.colorVerde),
),
const SizedBox(height: 8),
Text(
l10n.gameContinues,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () {
final fin = estado.comprobarFinPartida();
if (fin) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const PantallaFinPartida(),
),
);
} else {
estado.siguienteRonda();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const PantallaDebate(),
),
); );
} }
}, : () => _continuarTrasNoAdivinar(estado),
icon: const Icon(Icons.skip_next),
label: Text(l10n.nextRound),
),
), ),
], ],
],
),
),
),
),
); );
} }
} }
+29 -130
View File
@@ -4,7 +4,6 @@ import 'package:farolero/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../estado/estado_juego.dart'; import '../estado/estado_juego.dart';
import '../tema/componentes_farolero.dart'; import '../tema/componentes_farolero.dart';
import '../tema/tema_app.dart';
import 'pantalla_notas.dart'; import 'pantalla_notas.dart';
import 'pantalla_votacion.dart'; import 'pantalla_votacion.dart';
@@ -67,9 +66,6 @@ class _PantallaDebateState extends State<PantallaDebate> {
if (partida == null) return const SizedBox.shrink(); if (partida == null) return const SizedBox.shrink();
final tieneTemporizador = partida.config.tiempoDebateSegundos != null; final tieneTemporizador = partida.config.tiempoDebateSegundos != null;
final progreso = tieneTemporizador
? _segundosRestantes / partida.config.tiempoDebateSegundos!
: 0.0;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -78,167 +74,69 @@ class _PantallaDebateState extends State<PantallaDebate> {
), ),
body: FondoFarolero( body: FondoFarolero(
intenso: true, intenso: true,
child: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
const ArteGameplayFarolero.fase(height: 110), const ArteGameplayFarolero.fase(height: 110),
const SizedBox(height: 10), const SizedBox(height: 10),
// Temporizador
if (tieneTemporizador) ...[ if (tieneTemporizador) ...[
Container( TemporizadorFarolero(
width: double.infinity, etiqueta: _tiempoAgotado ? l10n.timeUp : l10n.timeRemaining,
padding: const EdgeInsets.all(20), tiempo: _formatearTiempo(_segundosRestantes),
decoration: BoxDecoration( agotado: _tiempoAgotado,
color: _tiempoAgotado
? TemaApp.colorAcento.withValues(alpha: 0.3)
: TemaApp.colorTarjeta,
borderRadius: BorderRadius.circular(16),
border: _tiempoAgotado
? Border.all(color: TemaApp.colorAcento, width: 2)
: null,
),
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Image.asset(
'assets/ui/generated/gameplay/gameplay_phase_emblem.webp',
fit: BoxFit.contain,
opacity: const AlwaysStoppedAnimation(0.36),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_tiempoAgotado ? l10n.timeUp : l10n.timeRemaining,
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
color: _tiempoAgotado
? TemaApp.colorAcento
: TemaApp.colorTextoSecundario,
),
),
const SizedBox(height: 8),
Text(
_formatearTiempo(_segundosRestantes),
style:
Theme.of(context).textTheme.headlineLarge?.copyWith(
fontSize: 48,
fontWeight: FontWeight.bold,
color: _segundosRestantes < 10 &&
!_tiempoAgotado
? TemaApp.colorAcento
: TemaApp.colorTexto,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progreso,
backgroundColor: TemaApp.colorSuperficie,
valueColor: AlwaysStoppedAnimation(
_segundosRestantes < 10
? TemaApp.colorAcento
: TemaApp.colorVerde,
),
minHeight: 6,
),
),
],
),
],
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// Jugadores activos
Expanded( Expanded(
child: Card( child: TarjetaFaseFarolero(
child: Padding( icono: Icons.forum,
padding: const EdgeInsets.all(16), titulo: l10n.playersInDebate,
child: Column( subtitulo: l10n.activePlayersInfo(
crossAxisAlignment: CrossAxisAlignment.start, partida.jugadoresActivos.length,
children: [ partida.impostoresActivos.length,
Text(
l10n.playersInDebate,
style: Theme.of(context).textTheme.titleLarge,
), ),
const SizedBox(height: 4), child: Expanded(
Text( child: ListView.separated(
l10n.activePlayersInfo(partida.jugadoresActivos.length, partida.impostoresActivos.length),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
Expanded(
child: ListView.builder(
itemCount: partida.jugadores.length, itemCount: partida.jugadores.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final j = partida.jugadores[index]; final jugador = partida.jugadores[index];
return ListTile( return EstadoJugadorFarolero(
leading: CircleAvatar( nombre: '${index + 1}. ${jugador.nombre}',
backgroundColor: j.eliminado subtitulo: jugador.eliminado ? l10n.eliminated : null,
? Colors.grey icono: jugador.eliminado ? Icons.person_off : Icons.record_voice_over,
: TemaApp.colorAcento, destacado: !jugador.eliminado,
child: Text( completado: !jugador.eliminado,
j.eliminado ? '💀' : '${index + 1}',
style: const TextStyle(
color: Colors.white, fontSize: 14),
),
),
title: Text(
j.nombre,
style: TextStyle(
decoration: j.eliminado
? TextDecoration.lineThrough
: null,
color: j.eliminado
? TemaApp.colorTextoSecundario
: TemaApp.colorTexto,
),
),
subtitle: j.eliminado
? Text(l10n.eliminated)
: null,
dense: true,
); );
}, },
), ),
), ),
],
),
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Botones
Row( Row(
children: [ children: [
Expanded( Expanded(
child: OutlinedButton.icon( child: BotonFarolero.oscuro(
texto: l10n.notes,
icono: Icons.edit_note,
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (_) => const PantallaNotas()),
builder: (_) => const PantallaNotas(),
),
); );
}, },
icon: const Text('📝', style: TextStyle(fontSize: 18)),
label: Text(l10n.notes),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
flex: 2, flex: 2,
child: ElevatedButton.icon( child: BotonFarolero(
texto: l10n.goToVoting,
icono: Icons.how_to_vote,
onPressed: _irAVotacion, onPressed: _irAVotacion,
icon: const Text('🗳️', style: TextStyle(fontSize: 18)),
label: Text(l10n.goToVoting),
), ),
), ),
], ],
@@ -247,6 +145,7 @@ class _PantallaDebateState extends State<PantallaDebate> {
), ),
), ),
), ),
),
); );
} }
} }
+33 -108
View File
@@ -1,4 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:farolero/l10n/generated/app_localizations.dart'; import 'package:farolero/l10n/generated/app_localizations.dart';
import 'package:farolero/modelos/inicio_partida_multijugador.dart'; import 'package:farolero/modelos/inicio_partida_multijugador.dart';
@@ -11,8 +11,6 @@ import 'package:farolero/tema/componentes_farolero.dart';
import 'package:farolero/tema/tema_app.dart'; import 'package:farolero/tema/tema_app.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/// Pantalla que ve el jugador durante la fase de debate (multidispositivo).
/// El cliente recibe el cambio de fase via Nearby y se navega aquí.
class PantallaDebateCliente extends StatefulWidget { class PantallaDebateCliente extends StatefulWidget {
final int? tiempoDebateSegundos; final int? tiempoDebateSegundos;
final String? primerTurnoNombre; final String? primerTurnoNombre;
@@ -87,9 +85,7 @@ class _PantallaDebateClienteState extends State<PantallaDebateCliente> {
void dispose() { void dispose() {
_timer?.cancel(); _timer?.cancel();
final listener = _listener; final listener = _listener;
if (listener != null) { if (listener != null) _nearby?.removeMensajeListener(listener);
_nearby?.removeMensajeListener(listener);
}
super.dispose(); super.dispose();
} }
@@ -160,141 +156,70 @@ class _PantallaDebateClienteState extends State<PantallaDebateCliente> {
), ),
body: FondoFarolero( body: FondoFarolero(
intenso: true, intenso: true,
child: Padding( child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
children: [ children: [
const ArteGameplayFarolero.fase(height: 124), const ArteGameplayFarolero.fase(height: 124),
const SizedBox(height: 10), const SizedBox(height: 12),
const Spacer(), TarjetaFaseFarolero(
icono: Icons.forum,
// Timer titulo: l10n.debate,
subtitulo: l10n.debateInstructions,
child: Column(
children: [
if (widget.tiempoDebateSegundos != null) ...[ if (widget.tiempoDebateSegundos != null) ...[
Container( TemporizadorFarolero(
padding: const EdgeInsets.all(32), etiqueta: _segundosRestantes == 0
decoration: BoxDecoration(
color: _segundosRestantes == 0
? TemaApp.colorAcento.withValues(alpha: 0.3)
: TemaApp.colorTarjeta,
borderRadius: BorderRadius.circular(24),
),
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Image.asset(
'assets/ui/generated/gameplay/gameplay_phase_emblem.webp',
fit: BoxFit.contain,
opacity: const AlwaysStoppedAnimation(0.42),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_segundosRestantes == 0
? l10n.timeUp ? l10n.timeUp
: l10n.timeRemaining, : l10n.timeRemaining,
style: Theme.of(context).textTheme.titleMedium, tiempo: _formatearTiempo(_segundosRestantes),
agotado: _segundosRestantes == 0,
), ),
const SizedBox(height: 8), const SizedBox(height: 16),
Text(
_formatearTiempo(_segundosRestantes),
style:
Theme.of(context).textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _segundosRestantes == 0
? TemaApp.colorAcento
: TemaApp.colorTexto,
),
),
],
),
],
),
),
const SizedBox(height: 32),
] else ...[ ] else ...[
Text( Text(
l10n.debatePhaseActive, l10n.debatePhaseActive,
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// Instrucciones
if (widget.primerTurnoNombre != null) ...[ if (widget.primerTurnoNombre != null) ...[
Container( EstadoJugadorFarolero(
width: double.infinity, nombre: l10n.firstTurnInstruction(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: TemaApp.colorNaranja.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: TemaApp.colorNaranja.withValues(alpha: 0.65),
),
),
child: Row(
children: [
const Icon(
Icons.record_voice_over,
color: TemaApp.colorNaranja,
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.primerTurnoNombre!, widget.primerTurnoNombre!,
style: Theme.of(context).textTheme.titleMedium,
), ),
destacado: true,
completado: true,
icono: Icons.record_voice_over,
), ),
const SizedBox(height: 12),
], ],
), BotonFarolero.secundario(
), texto: _votacionSolicitada
const SizedBox(height: 16), ? l10n.votacionSolicitada
], : l10n.solicitarVotacion,
icono: _votacionSolicitada
Text( ? Icons.hourglass_empty
l10n.debateInstructions, : Icons.how_to_vote,
textAlign: TextAlign.center,
style: TextStyle(
color: TemaApp.colorTextoSecundario,
fontSize: 16,
),
),
const Spacer(),
// Botón solicitar votación
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: _votacionSolicitada onPressed: _votacionSolicitada
? null ? null
: () { : () {
setState(() => _votacionSolicitada = true); setState(() => _votacionSolicitada = true);
widget.onSolicitarVotacion(); widget.onSolicitarVotacion();
}, },
icon: Icon(_votacionSolicitada ? Icons.hourglass_empty : Icons.how_to_vote),
label: Text(
_votacionSolicitada
? l10n.votacionSolicitada
: l10n.solicitarVotacion,
),
style: ElevatedButton.styleFrom(
backgroundColor: _votacionSolicitada
? TemaApp.colorTarjeta
: TemaApp.colorAcento,
foregroundColor: Colors.white,
textStyle: const TextStyle(fontSize: 16),
), ),
],
), ),
), ),
], ],
), ),
), ),
), ),
),
); );
} }
+158 -486
View File
@@ -7,12 +7,14 @@ import '../estado/estado_juego.dart';
import '../modelos/gamificacion_usuario.dart'; import '../modelos/gamificacion_usuario.dart';
import '../modelos/inicio_partida_multijugador.dart'; import '../modelos/inicio_partida_multijugador.dart';
import '../modelos/jugador.dart'; import '../modelos/jugador.dart';
import '../modelos/palabra.dart';
import '../modelos/partida.dart'; import '../modelos/partida.dart';
import '../modelos/snapshot_partida_online.dart'; import '../modelos/snapshot_partida_online.dart';
import '../servicios/servicio_historial_partidas.dart'; import '../servicios/servicio_historial_partidas.dart';
import '../servicios/servicio_nearby.dart'; import '../servicios/servicio_nearby.dart';
import '../servicios/servicio_perfil_usuario.dart'; import '../servicios/servicio_perfil_usuario.dart';
import '../tema/componentes_farolero.dart'; import '../tema/componentes_farolero.dart';
import '../tema/componentes_resultado_farolero.dart';
import '../tema/tema_app.dart'; import '../tema/tema_app.dart';
import 'pantalla_notas_online.dart'; import 'pantalla_notas_online.dart';
import 'pantalla_revision_palabra.dart'; import 'pantalla_revision_palabra.dart';
@@ -279,10 +281,13 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
const SizedBox(height: 8), const SizedBox(height: 8),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: OutlinedButton.icon( child: SizedBox(
width: 260,
child: BotonFarolero.oscuro(
texto: AppLocalizations.of(context)!.assumeOnThisPhone,
icono: Icons.person_add_alt_1,
onPressed: () => nearby.asumirUsuariosDesconectados(), onPressed: () => nearby.asumirUsuariosDesconectados(),
icon: const Icon(Icons.person_add_alt_1), ),
label: Text(AppLocalizations.of(context)!.assumeOnThisPhone),
), ),
), ),
], ],
@@ -404,68 +409,36 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
bool todosListos, bool todosListos,
ServicioNearby nearby, ServicioNearby nearby,
) { ) {
return Card( return TarjetaFaseFarolero(
child: Padding( icono: Icons.visibility,
padding: const EdgeInsets.all(16), titulo: l10n.waitingPlayersSeeWord,
subtitulo: l10n.connectedPlayers,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(
l10n.waitingPlayersSeeWord,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
l10n.connectedPlayers,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
_buildJugadorTile(nearby.miNombre ?? 'Host', true, _hostListo), _buildJugadorTile(nearby.miNombre ?? 'Host', true, _hostListo),
...nearby.jugadores.map( ...nearby.jugadores.map(
(j) => _buildJugadorTile( (jugador) => _buildJugadorTile(
j.nombre, jugador.nombre,
false, false,
_clientesListos[j.endpointId] ?? false, _clientesListos[jugador.endpointId] ?? false,
),
),
const Spacer(),
// Botón para que el host vea su palabra
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _mostrarPalabraHost(context),
icon: const Icon(Icons.visibility),
label: Text(l10n.seeYourWord),
style: ElevatedButton.styleFrom(
backgroundColor: TemaApp.colorNaranja,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
if (todosListos) BotonFarolero(
Container( texto: l10n.seeYourWord,
padding: const EdgeInsets.all(12), icono: Icons.visibility,
decoration: BoxDecoration( onPressed: () => _mostrarPalabraHost(context),
color: TemaApp.colorVerde.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
), ),
child: Row( if (todosListos) ...[
mainAxisAlignment: MainAxisAlignment.center, const SizedBox(height: 12),
children: [ EstadoJugadorFarolero(
const Icon(Icons.check_circle, color: TemaApp.colorVerde), nombre: l10n.allSeenStartDebate,
const SizedBox(width: 8), completado: true,
Text( icono: Icons.check_circle,
l10n.allSeenStartDebate,
style: const TextStyle(color: TemaApp.colorVerde),
), ),
], ],
),
),
], ],
), ),
),
); );
} }
@@ -563,67 +536,33 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
final estado = context.read<EstadoJuego>(); final estado = context.read<EstadoJuego>();
final tiempo = estado.partida?.config.tiempoDebateSegundos; final tiempo = estado.partida?.config.tiempoDebateSegundos;
return Card( return TarjetaFaseFarolero(
child: Padding( icono: Icons.forum,
padding: const EdgeInsets.all(16), titulo: l10n.debate,
subtitulo: l10n.debateInstructions,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (tiempo != null) ...[ if (tiempo != null) ...[
Text(l10n.debate, style: Theme.of(context).textTheme.titleLarge), TemporizadorFarolero(
const SizedBox(height: 16), etiqueta: _segundosRestantes == 0
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _segundosRestantes == 0
? TemaApp.colorAcento.withValues(alpha: 0.3)
: TemaApp.colorTarjeta,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(
_segundosRestantes == 0
? l10n.timeUp ? l10n.timeUp
: l10n.timeRemaining, : l10n.timeRemaining,
style: Theme.of(context).textTheme.bodyMedium, tiempo: _formatearTiempo(_segundosRestantes),
), agotado: _segundosRestantes == 0,
Text(
_formatearTiempo(_segundosRestantes),
style: Theme.of(context).textTheme.headlineLarge,
),
],
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
_buildPrimerTurno(context), _buildPrimerTurno(context),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(l10n.activePlayers, style: Theme.of(context).textTheme.titleMedium),
l10n.activePlayers,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8), const SizedBox(height: 8),
Expanded( _buildJugadorTile(nearby.miNombre ?? 'Host', true, true),
child: ListView.builder( ...nearby.jugadores.map(
itemCount: nearby.jugadores.length + 1, (jugador) => _buildJugadorTile(jugador.nombre, false, true),
itemBuilder: (context, index) {
if (index == 0) {
return _buildJugadorTile(
nearby.miNombre ?? 'Host',
true,
true,
);
}
final j = nearby.jugadores[index - 1];
return _buildJugadorTile(j.nombre, false, true);
},
),
), ),
], ],
), ),
),
); );
} }
@@ -644,7 +583,7 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'Empieza $nombre diciendo su palabra.', AppLocalizations.of(context)!.firstTurnInstruction(nombre),
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@@ -664,90 +603,43 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
final votosEmitidos = estado.votos.length; final votosEmitidos = estado.votos.length;
final progreso = totalVotos == 0 ? 0.0 : votosEmitidos / totalVotos; final progreso = totalVotos == 0 ? 0.0 : votosEmitidos / totalVotos;
return Card( return TarjetaFaseFarolero(
child: Padding( icono: Icons.how_to_vote,
padding: const EdgeInsets.all(16), titulo: l10n.voting,
subtitulo: l10n.votesProgress(votosEmitidos, totalVotos),
color: TemaApp.colorAcento,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(l10n.voting, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: TemaApp.colorTarjeta,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(l10n.votesProgress(votosEmitidos, totalVotos)),
const SizedBox(height: 8),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: progreso.clamp(0.0, 1.0).toDouble(), value: progreso.clamp(0.0, 1.0).toDouble(),
backgroundColor: TemaApp.colorSuperficie, minHeight: 14,
valueColor: const AlwaysStoppedAnimation( backgroundColor: Colors.black.withValues(alpha: 0.35),
TemaApp.colorAcento, valueColor: const AlwaysStoppedAnimation(TemaApp.colorAcento),
),
minHeight: 8,
),
),
],
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _hostYaVoto(context) ? null : () => _abrirVotacionHost(context),
icon: const Icon(Icons.how_to_vote),
label: Text(
_hostYaVoto(context)
? l10n.playersVoted
: l10n.confirmVote,
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( if (!_hostYaVoto(context))
l10n.playersVoted, BotonFarolero.secundario(
style: Theme.of(context).textTheme.titleMedium, texto: l10n.votar,
icono: Icons.how_to_vote,
onPressed: () => _abrirVotacionHost(context),
), ),
const SizedBox(height: 8), if (!_hostYaVoto(context)) const SizedBox(height: 16),
Expanded( ...partida.jugadoresActivos.map((jugador) {
child: ListView.builder(
itemCount: partida.jugadoresActivos.length,
itemBuilder: (context, index) {
final jugador = partida.jugadoresActivos[index];
final haVotado = estado.votos.containsKey(jugador.id); final haVotado = estado.votos.containsKey(jugador.id);
return _buildJugadorTile(jugador.nombre, false, haVotado); return _buildJugadorTile(jugador.nombre, false, haVotado);
}, }),
),
),
if (todosVotaron) if (todosVotaron)
Container( EstadoJugadorFarolero(
padding: const EdgeInsets.all(12), nombre: l10n.allVoted,
decoration: BoxDecoration( completado: true,
color: TemaApp.colorVerde.withValues(alpha: 0.2), icono: Icons.check_circle,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle, color: TemaApp.colorVerde),
const SizedBox(width: 8),
Text(
l10n.allVoted,
style: const TextStyle(color: TemaApp.colorVerde),
), ),
], ],
), ),
),
],
),
),
); );
} }
@@ -760,132 +652,11 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
return Center(child: Text(l10n.noResult)); return Center(child: Text(l10n.noResult));
} }
final conteo = <String, int>{}; return SingleChildScrollView(
for (final votadoId in resultado.votos.values) { padding: const EdgeInsets.only(bottom: 8),
conteo[votadoId] = (conteo[votadoId] ?? 0) + 1; child: ResultadoRondaFarolero(
} resultado: resultado,
final maxVotos = conteo.values.isEmpty jugadores: partida.jugadores,
? 1
: conteo.values.reduce((a, b) => a > b ? a : b);
final ranking = conteo.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.result, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: TemaApp.decoracionPanel(
color: resultado.eraImpostor
? TemaApp.colorVerde.withValues(alpha: 0.18)
: TemaApp.colorAcento.withValues(alpha: 0.18),
borderColor: resultado.eraImpostor
? TemaApp.colorVerde
: TemaApp.colorAcento,
),
child: Column(
children: [
Text(
resultado.eliminadoNombre,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 6),
Text(
resultado.eraImpostor
? l10n.wasImpostor
: l10n.wasInnocent,
style: TextStyle(
color: resultado.eraImpostor
? TemaApp.colorVerde
: TemaApp.colorAcento,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
Text(l10n.votesThisRound,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
Expanded(
child: ListView(
children: [
...ranking.map((entry) {
final jugador = partida.jugadores.firstWhere(
(j) => j.id == entry.key,
orElse: () => partida.jugadores.first,
);
return _buildBarraResultado(
context,
nombre: jugador.nombre,
votos: entry.value,
maxVotos: maxVotos,
destacado: entry.key == resultado.eliminadoId,
);
}),
const Divider(height: 24),
...resultado.votos.entries.map((entry) {
final votante = partida.jugadores.firstWhere(
(j) => j.id == entry.key,
orElse: () => partida.jugadores.first,
);
final votado = partida.jugadores.firstWhere(
(j) => j.id == entry.value,
orElse: () => partida.jugadores.first,
);
return ListTile(
dense: true,
leading: const Icon(Icons.how_to_vote),
title: Text('${votante.nombre}${votado.nombre}'),
);
}),
],
),
),
],
),
),
);
}
Widget _buildBarraResultado(
BuildContext context, {
required String nombre,
required int votos,
required int maxVotos,
required bool destacado,
}) {
final color = destacado ? TemaApp.colorAcento : TemaApp.colorNaranja;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: Text(nombre)),
Text('$votos',
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
value: (votos / maxVotos).clamp(0.0, 1.0).toDouble(),
minHeight: 10,
backgroundColor: TemaApp.colorSuperficie,
valueColor: AlwaysStoppedAnimation(color),
),
),
],
), ),
); );
} }
@@ -895,102 +666,63 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
final ultimo = partida?.historialVotaciones.isNotEmpty == true final ultimo = partida?.historialVotaciones.isNotEmpty == true
? partida!.historialVotaciones.last ? partida!.historialVotaciones.last
: null; : null;
return Card( return TarjetaFaseFarolero(
child: Padding( icono: Icons.psychology,
padding: const EdgeInsets.all(20), titulo: l10n.impostorGuessTitle,
child: Column( subtitulo: ultimo == null
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.psychology, size: 56, color: TemaApp.colorNaranja),
const SizedBox(height: 16),
Text(
l10n.impostorGuessTitle,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
ultimo == null
? l10n.impostorCanGuess ? l10n.impostorCanGuess
: '${ultimo.eliminadoNombre}: ${l10n.impostorCanGuess}', : '${ultimo.eliminadoNombre}: ${l10n.impostorCanGuess}',
textAlign: TextAlign.center, color: TemaApp.colorNaranja,
), child: const ArteGameplayFarolero.resultado(height: 132),
],
),
),
); );
} }
Widget _buildFaseFinOnline(BuildContext context, AppLocalizations l10n) { Widget _buildFaseFinOnline(BuildContext context, AppLocalizations l10n) {
final partida = context.watch<EstadoJuego>().partida; final partida = context.watch<EstadoJuego>().partida;
final ganaronJugadores = partida?.ganador == 'jugadores'; if (partida == null) return Center(child: Text(l10n.noResult));
return Card(
child: Padding( final ganaronJugadores = partida.ganador == 'jugadores';
padding: const EdgeInsets.all(24), final color = ganaronJugadores ? TemaApp.colorVerde : TemaApp.colorAcento;
final impostores = partida.jugadores.where((j) => j.esImpostor).toList();
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 8),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( HeroFinalPartidaFarolero(
ganaronJugadores ? '🎉' : '🎭', encabezado: l10n.gameOver,
style: const TextStyle(fontSize: 64), titulo: ganaronJugadores ? l10n.playersWin : l10n.impostorsWin,
), icono: ganaronJugadores ? Icons.emoji_events : Icons.theater_comedy,
const SizedBox(height: 16), color: color,
Text(
ganaronJugadores ? l10n.playersWin : l10n.impostorsWin,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( if (_progresoGamificacion == null)
partida == null ? '' : l10n.theWordWas(partida.palabraSecreta), const TarjetaRecompensaCargandoPremium()
textAlign: TextAlign.center, else
TarjetaProgresoGamificacionPremium(
progreso: _progresoGamificacion!,
), ),
if (_progresoGamificacion != null) ...[ const SizedBox(height: 18),
const SizedBox(height: 16), TarjetaSecretoPremium(
_buildProgresoGamificacion(context, _progresoGamificacion!), palabra: partida.palabraSecreta,
], categoria: BancoPalabras.nombreBonitoCategoria(
], partida.categoriaReal,
l10n,
), ),
), ),
); const SizedBox(height: 18),
} TarjetaImpostoresPremium(
titulo: impostores.length == 1
Widget _buildProgresoGamificacion( ? l10n.theImpostorWas
BuildContext context, : l10n.theImpostorsWere,
ProgresoGamificacionUsuario progreso, impostores: impostores,
) {
final nuevas = progreso.nuevasMedallas;
return PanelFarolero(
padding: const EdgeInsets.all(14),
color: TemaApp.colorSuperficie.withValues(alpha: 0.82),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Tu progreso', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 10),
Row(
children: [
const Text('🔥', style: TextStyle(fontSize: 22)),
const SizedBox(width: 8),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: progreso.despues.fuego / 100,
minHeight: 8,
color: TemaApp.colorNaranja,
backgroundColor: Colors.black.withValues(alpha: 0.35),
), ),
const SizedBox(height: 18),
if (partida.historialVotaciones.isNotEmpty)
TarjetaHistorialVotosPremium(
historial: partida.historialVotaciones,
jugadores: partida.jugadores,
), ),
),
const SizedBox(width: 8),
Text('${progreso.despues.fuego}%'),
],
),
if (nuevas.isNotEmpty) ...[
const SizedBox(height: 10),
MedallasCompactasFarolero(ids: nuevas, max: nuevas.length),
],
], ],
), ),
); );
@@ -1046,28 +778,11 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
} }
Widget _buildJugadorTile(String nombre, bool esHost, bool listo) { Widget _buildJugadorTile(String nombre, bool esHost, bool listo) {
return Container( return EstadoJugadorFarolero(
margin: const EdgeInsets.only(bottom: 8), nombre: nombre,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), destacado: esHost,
decoration: BoxDecoration( completado: listo,
color: listo icono: esHost ? Icons.phone_android : Icons.devices,
? TemaApp.colorVerde.withValues(alpha: 0.2)
: TemaApp.colorTarjeta,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
esHost ? Icons.phone_android : Icons.devices,
color: esHost ? TemaApp.colorDorado : TemaApp.colorNaranja,
size: 22,
),
const SizedBox(width: 8),
Expanded(child: Text(nombre)),
if (listo)
const Icon(Icons.check_circle, color: TemaApp.colorVerde, size: 20),
],
),
); );
} }
@@ -1080,60 +795,36 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
) { ) {
switch (fase) { switch (fase) {
case FaseJuego.verPalabra: case FaseJuego.verPalabra:
return SizedBox( return BotonFarolero(
width: double.infinity, texto: todosListos ? l10n.allSeenStartDebate : l10n.waitingPlayersSeeWord,
height: 56, icono: Icons.forum,
child: ElevatedButton.icon( onPressed: todosListos ? () => _avanzarAFase(FaseJuego.debate) : null,
onPressed: todosListos
? () => _avanzarAFase(FaseJuego.debate)
: null,
icon: const Icon(Icons.forum),
label: Text(
todosListos
? l10n.allSeenStartDebate
: l10n.waitingPlayersSeeWord,
),
),
); );
case FaseJuego.debate: case FaseJuego.debate:
return SizedBox( return BotonFarolero.secundario(
width: double.infinity, texto: l10n.goToVoting,
height: 56, icono: Icons.how_to_vote,
child: ElevatedButton.icon(
onPressed: () => _avanzarAFase(FaseJuego.votacion), onPressed: () => _avanzarAFase(FaseJuego.votacion),
icon: const Icon(Icons.how_to_vote),
label: Text(l10n.goToVoting),
),
); );
case FaseJuego.votacion: case FaseJuego.votacion:
return SizedBox( return BotonFarolero(
width: double.infinity, texto: todosVotaron ? l10n.revealResult : l10n.waitingVoting,
height: 56, icono: Icons.visibility,
child: ElevatedButton.icon( onPressed: todosVotaron ? () => _avanzarAFase(FaseJuego.resultado) : null,
onPressed: todosVotaron
? () => _avanzarAFase(FaseJuego.resultado)
: null,
icon: const Icon(Icons.visibility),
label: Text(todosVotaron ? l10n.revealResult : l10n.waitingVoting),
),
); );
case FaseJuego.resultado: case FaseJuego.resultado:
return _buildAccionesResultado(context, l10n); return _buildAccionesResultado(context, l10n);
case FaseJuego.adivinanza: case FaseJuego.adivinanza:
return _buildAccionesAdivinanza(context, l10n); return _buildAccionesAdivinanza(context, l10n);
case FaseJuego.finPartida: case FaseJuego.finPartida:
return SizedBox( return BotonFarolero.oscuro(
width: double.infinity, texto: l10n.mainMenu,
height: 56, icono: Icons.home,
child: OutlinedButton.icon(
onPressed: () async { onPressed: () async {
final nearby = context.read<ServicioNearby>(); final nearby = context.read<ServicioNearby>();
await nearby.desconectar(); await nearby.desconectar();
widget.onPartidaFin(); widget.onPartidaFin();
}, },
icon: const Icon(Icons.home),
label: Text(l10n.mainMenu),
),
); );
default: default:
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -1149,80 +840,58 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
if (partida == null || resultado == null) return const SizedBox.shrink(); if (partida == null || resultado == null) return const SizedBox.shrink();
if (_hayFinTrasVotacion(partida)) { if (_hayFinTrasVotacion(partida)) {
return SizedBox( return BotonFarolero(
width: double.infinity, texto: l10n.seeEndResult,
height: 56, icono: Icons.emoji_events,
child: ElevatedButton.icon(
onPressed: () => _finalizarPartidaOnline(context), onPressed: () => _finalizarPartidaOnline(context),
icon: const Icon(Icons.emoji_events),
label: Text(l10n.seeEndResult),
),
); );
} }
if (resultado.eraImpostor) { if (resultado.eraImpostor) {
return Column( return Column(
children: [ children: [
SizedBox( BotonFarolero.oscuro(
width: double.infinity, texto: l10n.impostorGuessWord,
height: 56, icono: Icons.psychology,
child: OutlinedButton.icon(
onPressed: () => _iniciarAdivinanzaOnline(context), onPressed: () => _iniciarAdivinanzaOnline(context),
icon: const Icon(Icons.psychology),
label: Text(l10n.impostorGuessWord),
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
SizedBox( BotonFarolero(
width: double.infinity, texto: l10n.nextRound,
height: 56, icono: Icons.skip_next,
child: ElevatedButton.icon(
onPressed: () => _siguienteRondaOnline(context), onPressed: () => _siguienteRondaOnline(context),
icon: const Icon(Icons.skip_next),
label: Text(l10n.nextRound),
),
), ),
], ],
); );
} }
return SizedBox( return BotonFarolero(
width: double.infinity, texto: l10n.nextRound,
height: 56, icono: Icons.skip_next,
child: ElevatedButton.icon(
onPressed: () => _siguienteRondaOnline(context), onPressed: () => _siguienteRondaOnline(context),
icon: const Icon(Icons.skip_next),
label: Text(l10n.nextRound),
),
); );
} }
Widget _buildAccionesAdivinanza(BuildContext context, AppLocalizations l10n) { Widget _buildAccionesAdivinanza(BuildContext context, AppLocalizations l10n) {
return Column( return Column(
children: [ children: [
SizedBox( BotonFarolero(
width: double.infinity, texto: l10n.guess,
height: 56, icono: Icons.check_circle,
child: ElevatedButton.icon(
onPressed: () => _resolverAdivinanzaOnline(context), onPressed: () => _resolverAdivinanzaOnline(context),
icon: const Icon(Icons.check_circle),
label: Text(l10n.guess),
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
SizedBox( BotonFarolero.oscuro(
width: double.infinity, texto: l10n.dontGuess,
height: 56, icono: Icons.skip_next,
child: OutlinedButton.icon(
onPressed: () => _siguienteRondaOnline(context), onPressed: () => _siguienteRondaOnline(context),
icon: const Icon(Icons.skip_next),
label: Text(l10n.dontGuess),
),
), ),
], ],
); );
} }
Future<void> _finalizarPartidaOnline
Future<void> _finalizarPartidaOnline(BuildContext context) async { Future<void> _finalizarPartidaOnline(BuildContext context) async {
final estado = context.read<EstadoJuego>(); final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>(); final nearby = context.read<ServicioNearby>();
@@ -1441,7 +1110,12 @@ class _PantallaRevelarPalabraHostState
if (widget.esImpostor && widget.pistaActiva) ...[ if (widget.esImpostor && widget.pistaActiva) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Categoría: ${widget.categoria}', l10n.clueCategory(
BancoPalabras.nombreBonitoCategoria(
widget.categoria,
l10n,
),
),
style: Theme.of(context).textTheme.bodyLarge style: Theme.of(context).textTheme.bodyLarge
?.copyWith(color: TemaApp.colorNaranja), ?.copyWith(color: TemaApp.colorNaranja),
), ),
@@ -1450,7 +1124,11 @@ class _PantallaRevelarPalabraHostState
) )
: Column( : Column(
children: [ children: [
const Text('🔒', style: TextStyle(fontSize: 48)), const Icon(
Icons.lock,
color: TemaApp.colorDorado,
size: 48,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
l10n.holdToSeeWord, l10n.holdToSeeWord,
@@ -1496,21 +1174,15 @@ class _PantallaRevelarPalabraHostState
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( BotonFarolero(
width: double.infinity, texto: _haRevelado ? l10n.iveSeenIt : l10n.tapToSee,
height: 56, icono: Icons.check,
child: ElevatedButton.icon(
onPressed: _haRevelado onPressed: _haRevelado
? () { ? () {
widget.onVisto(); widget.onVisto();
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
: null, : null,
icon: const Icon(Icons.check),
label: Text(
_haRevelado ? l10n.iveSeenIt : l10n.tapToSee,
),
),
), ),
], ],
), ),
+4 -16
View File
@@ -5,7 +5,7 @@ import 'package:farolero/tema/componentes_farolero.dart';
import 'package:farolero/tema/tema_app.dart'; import 'package:farolero/tema/tema_app.dart';
/// Pantalla que ve cada jugador cuando recibe su palabra (modo multidispositivo). /// Pantalla que ve cada jugador cuando recibe su palabra (modo multidispositivo).
/// El cliente recibe la palabra via ServicioNearby y se navega aquí. /// El cliente recibe la palabra vía ServicioNearby y se navega aquí.
/// NO es la pantalla del host. /// NO es la pantalla del host.
class PantallaPalabraCliente extends StatefulWidget { class PantallaPalabraCliente extends StatefulWidget {
final String palabra; final String palabra;
@@ -166,23 +166,11 @@ class _PantallaPalabraClienteState extends State<PantallaPalabraCliente> {
), ),
const Spacer(), const Spacer(),
// Botón confirmar // Botón confirmar
SizedBox( BotonFarolero(
width: double.infinity, texto: _haRevelado ? l10n.iveSeenIt : l10n.tapToSee,
height: 56, icono: Icons.check,
child: ElevatedButton.icon(
onPressed: _haRevelado ? widget.onVisto : null, onPressed: _haRevelado ? widget.onVisto : null,
icon: const Icon(Icons.check),
label: Text(
_haRevelado ? l10n.iveSeenIt : l10n.tapToSee,
),
style: ElevatedButton.styleFrom(
backgroundColor: TemaApp.colorAcento,
foregroundColor: Colors.white,
textStyle: const TextStyle(fontSize: 16),
),
),
), ),
], ],
), ),
+4 -10
View File
@@ -130,18 +130,12 @@ class _PantallaPalabrasClienteState extends State<PantallaPalabrasCliente> {
style: TextStyle(color: TemaApp.colorTextoSecundario), style: TextStyle(color: TemaApp.colorTextoSecundario),
), ),
const Spacer(), const Spacer(),
SizedBox( BotonFarolero(
width: double.infinity, texto: _actualRevelado
height: 56,
child: ElevatedButton.icon(
onPressed: _actualRevelado ? _continuar : null,
icon: Icon(_esUltimo ? Icons.check : Icons.arrow_forward),
label: Text(
_actualRevelado
? (_esUltimo ? l10n.iveSeenIt : l10n.next) ? (_esUltimo ? l10n.iveSeenIt : l10n.next)
: l10n.tapToSee, : l10n.tapToSee,
), icono: _esUltimo ? Icons.check : Icons.arrow_forward,
), onPressed: _actualRevelado ? _continuar : null,
), ),
], ],
), ),
+3 -1
View File
@@ -223,7 +223,9 @@ class _PantallaResultadoOnlineState extends State<PantallaResultadoOnline> {
AppLocalizations l10n, AppLocalizations l10n,
) { ) {
return Center( return Center(
child: Card( child: PanelFarolero(
margin: const EdgeInsets.all(16),
borderColor: TemaApp.colorNaranja.withValues(alpha: 0.48),
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
+6 -12
View File
@@ -29,15 +29,12 @@ Future<void> mostrarRevisionPalabraOnline({
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
...jugadoresControlados.map( ...jugadoresControlados.map(
(jugador) => Card( (jugador) => EstadoJugadorFarolero(
color: TemaApp.colorTarjeta, nombre: jugador.nombre,
child: ListTile( icono: Icons.visibility,
leading: const Icon(Icons.visibility),
title: Text(jugador.nombre),
onTap: () => Navigator.pop(sheetContext, jugador), onTap: () => Navigator.pop(sheetContext, jugador),
), ),
), ),
),
], ],
), ),
), ),
@@ -121,13 +118,10 @@ class _DialogoRevisionPalabra extends StatelessWidget {
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
SizedBox( BotonFarolero.secundario(
width: double.infinity, texto: l10n.back,
child: ElevatedButton.icon( icono: Icons.check,
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.check),
label: Text(l10n.back),
),
), ),
], ],
), ),
+54 -111
View File
@@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:farolero/l10n/generated/app_localizations.dart'; import 'package:farolero/l10n/generated/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../estado/estado_juego.dart'; import '../estado/estado_juego.dart';
@@ -24,8 +24,7 @@ class _PantallaVerPalabraState extends State<PantallaVerPalabra> {
final partida = estado.partida; final partida = estado.partida;
if (partida == null) return const SizedBox.shrink(); if (partida == null) return const SizedBox.shrink();
final todosHanVisto = final todosHanVisto = partida.jugadores.every((j) => _hanVisto.contains(j.id));
partida.jugadores.every((j) => _hanVisto.contains(j.id));
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -34,82 +33,58 @@ class _PantallaVerPalabraState extends State<PantallaVerPalabra> {
), ),
body: FondoFarolero( body: FondoFarolero(
intenso: true, intenso: true,
child: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
const ArteGameplayFarolero.fase(height: 110), const ArteGameplayFarolero.fase(height: 110),
const SizedBox(height: 10), const SizedBox(height: 10),
EncabezadoFarolero( TarjetaFaseFarolero(
icono: Icons.visibility, icono: Icons.visibility,
titulo: l10n.roundNumber(partida.rondaActual), titulo: l10n.roundNumber(partida.rondaActual),
subtitulo: l10n.eachPlayerMustSee, subtitulo: l10n.eachPlayerMustSee,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Expanded( Expanded(
child: ListView.builder( child: ListView.separated(
itemCount: partida.jugadores.length, itemCount: partida.jugadores.length,
separatorBuilder: (context, index) => const SizedBox(height: 10),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final jugador = partida.jugadores[index]; final jugador = partida.jugadores[index];
final haVisto = _hanVisto.contains(jugador.id); final haVisto = _hanVisto.contains(jugador.id);
return Card( return EstadoJugadorFarolero(
color: haVisto nombre: '${index + 1}. ${jugador.nombre}',
? TemaApp.colorVerde.withValues(alpha: 0.2) subtitulo: haVisto ? l10n.alreadySeen : l10n.tapToSee,
: TemaApp.colorTarjeta, icono: haVisto ? Icons.check_circle : Icons.visibility,
child: ListTile( destacado: haVisto,
leading: CircleAvatar( completado: haVisto,
backgroundColor: haVisto onTap: haVisto ? null : () => _mostrarPalabra(context, jugador.id),
? TemaApp.colorVerde
: TemaApp.colorAcento,
child: Text(
haVisto ? '' : '${index + 1}',
style:
const TextStyle(color: Colors.white),
),
),
title: Text(jugador.nombre),
subtitle: Text(
haVisto ? l10n.alreadySeen : l10n.tapToSee,
),
trailing: haVisto
? const Icon(Icons.check_circle,
color: TemaApp.colorVerde)
: const Icon(Icons.visibility,
color: TemaApp.colorTextoSecundario),
onTap: haVisto
? null
: () => _mostrarPalabra(context, jugador.id),
),
); );
}, },
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( BotonFarolero(
width: double.infinity, texto: todosHanVisto
height: 56, ? l10n.allSeenStartDebate
child: ElevatedButton.icon( : l10n.playersRemaining(partida.jugadores.length - _hanVisto.length),
icono: Icons.forum,
onPressed: todosHanVisto onPressed: todosHanVisto
? () { ? () {
estado.iniciarDebate(); estado.iniciarDebate();
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (_) => const PantallaDebate()),
builder: (_) => const PantallaDebate(),
),
); );
} }
: null, : null,
icon: const Icon(Icons.forum),
label: Text(todosHanVisto
? l10n.allSeenStartDebate
: l10n.playersRemaining(partida.jugadores.length - _hanVisto.length)),
),
), ),
], ],
), ),
), ),
), ),
),
); );
} }
@@ -154,8 +129,7 @@ class _PantallaRevelarPalabra extends StatefulWidget {
}); });
@override @override
State<_PantallaRevelarPalabra> createState() => State<_PantallaRevelarPalabra> createState() => _PantallaRevelarPalabraState();
_PantallaRevelarPalabraState();
} }
class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> { class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
@@ -165,66 +139,48 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final colorEstado = widget.esImpostor ? TemaApp.colorAcento : TemaApp.colorVerde;
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(widget.nombre)), appBar: AppBar(title: Text(widget.nombre)),
body: FondoFarolero( body: FondoFarolero(
intenso: true, intenso: true,
child: Center( child: SafeArea(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(24),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const ArteGameplayFarolero.fase(height: 128), const ArteGameplayFarolero.fase(height: 128),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( TarjetaFaseFarolero(
widget.nombre, icono: widget.esImpostor ? Icons.theater_comedy : Icons.search,
style: Theme.of(context).textTheme.headlineMedium, titulo: widget.nombre,
subtitulo: l10n.holdToSeeWord,
color: colorEstado,
), ),
const SizedBox(height: 32), const SizedBox(height: 18),
// Zona de revelación
AnimatedContainer( AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: TemaApp.decoracionPanel(
color: _manteniendo color: (_manteniendo ? colorEstado : TemaApp.colorTarjeta).withValues(alpha: _manteniendo ? 0.22 : 0.92),
? (widget.esImpostor borderColor: _manteniendo ? colorEstado : TemaApp.colorBorde,
? TemaApp.colorAcento.withValues(alpha: 0.3) radius: 24,
: TemaApp.colorVerde.withValues(alpha: 0.3))
: TemaApp.colorTarjeta,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _manteniendo
? (widget.esImpostor
? TemaApp.colorAcento
: TemaApp.colorVerde)
: Colors.transparent,
width: 2,
),
), ),
child: _manteniendo child: _manteniendo
? Column( ? Column(
children: [ children: [
Text( Icon(
widget.esImpostor ? '🎭' : '🔍', widget.esImpostor ? Icons.theater_comedy : Icons.search,
style: const TextStyle(fontSize: 48), color: colorEstado,
size: 52,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
widget.esImpostor widget.esImpostor ? l10n.youAreImpostor : l10n.yourWordIs,
? l10n.youAreImpostor textAlign: TextAlign.center,
: l10n.yourWordIs, style: Theme.of(context).textTheme.titleLarge?.copyWith(color: colorEstado),
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
color: widget.esImpostor
? TemaApp.colorAcento
: TemaApp.colorVerde,
),
), ),
if (!widget.esImpostor) ...[ if (!widget.esImpostor) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -234,19 +190,15 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
l10n.clueCategory(BancoPalabras.nombreBonitoCategoria(widget.categoria, l10n)), l10n.clueCategory(BancoPalabras.nombreBonitoCategoria(widget.categoria, l10n)),
style: Theme.of(context) textAlign: TextAlign.center,
.textTheme style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: TemaApp.colorNaranja),
.bodyLarge
?.copyWith(
color: TemaApp.colorNaranja,
),
), ),
], ],
], ],
) )
: Column( : Column(
children: [ children: [
const Text('🔒', style: TextStyle(fontSize: 48)), const Icon(Icons.lock, color: TemaApp.colorDorado, size: 52),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
l10n.holdToSeeWord, l10n.holdToSeeWord,
@@ -257,23 +209,20 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
Text( Text(
l10n.makeSureNoOneLooks, l10n.makeSureNoOneLooks,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
), ),
], ],
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Botón mantener pulsado
GestureDetector( GestureDetector(
onLongPressStart: (_) { onLongPressStart: (details) {
setState(() { setState(() {
_manteniendo = true; _manteniendo = true;
_visto = true; _visto = true;
}); });
}, },
onLongPressEnd: (_) { onLongPressEnd: (details) => setState(() => _manteniendo = false),
setState(() => _manteniendo = false);
},
child: Container( child: Container(
width: double.infinity, width: double.infinity,
height: 64, height: 64,
@@ -283,13 +232,11 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
? [TemaApp.colorNaranja, TemaApp.colorAcento] ? [TemaApp.colorNaranja, TemaApp.colorAcento]
: [TemaApp.colorAcento, TemaApp.colorAcento], : [TemaApp.colorAcento, TemaApp.colorAcento],
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(18),
), ),
child: Center( child: Center(
child: Text( child: Text(
_manteniendo _manteniendo ? l10n.showingWord : l10n.holdToSee,
? l10n.showingWord
: l10n.holdToSee,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
@@ -300,18 +247,14 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> {
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
if (_visto) if (_visto)
SizedBox( BotonFarolero.secundario(
width: double.infinity, texto: l10n.seenMyWord,
child: OutlinedButton.icon( icono: Icons.check,
onPressed: () { onPressed: () {
widget.onVisto(); widget.onVisto();
Navigator.pop(context); Navigator.pop(context);
}, },
icon: const Icon(Icons.check),
label: Text(l10n.seenMyWord),
),
), ),
], ],
), ),
+44 -112
View File
@@ -25,12 +25,6 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
final activos = partida.jugadoresActivos; final activos = partida.jugadoresActivos;
final todosVotaron = estado.todosHanVotado(); final todosVotaron = estado.todosHanVotado();
// Modo un solo móvil
if (!partida.config.modoMultimovil) {
return _construirVotacionUnMovil(context, estado, partida, activos, todosVotaron);
}
// Modo multimóvil sería similar pero controlado por Nearby
return _construirVotacionUnMovil(context, estado, partida, activos, todosVotaron); return _construirVotacionUnMovil(context, estado, partida, activos, todosVotaron);
} }
@@ -41,10 +35,7 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
List activos, List activos,
bool todosVotaron, bool todosVotaron,
) { ) {
// Encontrar el siguiente votante que no haya votado final sinVotar = activos.where((j) => !estado.votos.containsKey(j.id)).toList();
final sinVotar = activos
.where((j) => !estado.votos.containsKey(j.id))
.toList();
if (todosVotaron) { if (todosVotaron) {
return _construirTodosVotaron(context, estado); return _construirTodosVotaron(context, estado);
@@ -61,122 +52,68 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
), ),
body: FondoFarolero( body: FondoFarolero(
intenso: true, intenso: true,
child: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
const ArteGameplayFarolero.fase(height: 108), const ArteGameplayFarolero.fase(height: 108),
const SizedBox(height: 10), const SizedBox(height: 10),
// Progreso de votos TarjetaFaseFarolero(
Container( icono: Icons.how_to_vote,
width: double.infinity, titulo: l10n.voteOf(votanteActual.nombre),
padding: const EdgeInsets.all(16), subtitulo: l10n.votesProgress(estado.votos.length, activos.length),
decoration: TemaApp.decoracionPanel( color: TemaApp.colorAcento,
color: TemaApp.colorTarjeta.withValues(alpha: 0.90), child: ClipRRect(
borderColor: TemaApp.colorNaranja.withValues(alpha: 0.38),
),
child: Column(
children: [
Text(
l10n.turnToVote,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
votanteActual.nombre,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: TemaApp.colorNaranja,
),
),
const SizedBox(height: 8),
Text(
l10n.votesProgress(estado.votos.length, activos.length),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: estado.votos.length / activos.length, value: estado.votos.length / activos.length,
backgroundColor: TemaApp.colorSuperficie, backgroundColor: TemaApp.colorSuperficie,
valueColor: const AlwaysStoppedAnimation(TemaApp.colorAcento), valueColor: const AlwaysStoppedAnimation(TemaApp.colorAcento),
minHeight: 6, minHeight: 7,
), ),
), ),
],
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Expanded(
EncabezadoFarolero( child: TarjetaFaseFarolero(
icono: Icons.how_to_vote, icono: Icons.person_search,
titulo: l10n.whoIsImpostor, titulo: l10n.whoIsImpostor,
subtitulo: l10n.selectOnePlayer, subtitulo: l10n.selectOnePlayer,
color: TemaApp.colorAcento, color: TemaApp.colorNaranja,
trailing: Image.asset( child: Expanded(
'assets/ui/generated/meta/result_verdict_art.webp', child: ListView.separated(
width: 42,
height: 42,
opacity: const AlwaysStoppedAnimation(0.64),
),
),
const SizedBox(height: 12),
// Lista de candidatos
Expanded(
child: ListView.builder(
itemCount: puedenRecibir.length, itemCount: puedenRecibir.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final candidato = puedenRecibir[index]; final candidato = puedenRecibir[index];
final seleccionado = _seleccionado == candidato.id; final seleccionado = _seleccionado == candidato.id;
return Card( return SelectorVotoFarolero(
color: seleccionado nombre: '${index + 1}. ${candidato.nombre}',
? TemaApp.colorAcento.withValues(alpha: 0.3) seleccionado: seleccionado,
: TemaApp.colorTarjeta, onTap: () => setState(() => _seleccionado = candidato.id),
child: ListTile(
leading: CircleAvatar(
backgroundColor: seleccionado
? TemaApp.colorAcento
: TemaApp.colorSuperficie,
child: Text('${index + 1}',
style: const TextStyle(color: Colors.white)),
),
title: Text(candidato.nombre),
trailing: seleccionado
? const Icon(Icons.check_circle,
color: TemaApp.colorAcento)
: const Icon(Icons.radio_button_unchecked),
onTap: () {
setState(() => _seleccionado = candidato.id);
},
),
); );
}, },
), ),
), ),
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
BotonFarolero(
SizedBox( texto: l10n.confirmVote,
width: double.infinity, icono: Icons.how_to_vote,
height: 56,
child: ElevatedButton.icon(
onPressed: _seleccionado != null onPressed: _seleccionado != null
? () { ? () {
estado.registrarVoto( estado.registrarVoto(votanteActual.id, _seleccionado!);
votanteActual.id, _seleccionado!); setState(() => _seleccionado = null);
setState(() {
_seleccionado = null;
});
} }
: null, : null,
icon: const Icon(Icons.how_to_vote),
label: Text(l10n.confirmVote),
),
), ),
], ],
), ),
), ),
), ),
),
); );
} }
@@ -190,49 +127,44 @@ class _PantallaVotacionState extends State<PantallaVotacion> {
), ),
body: FondoFarolero( body: FondoFarolero(
intenso: true, intenso: true,
child: SafeArea(
child: Center( child: Center(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(24),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [ children: [
const ArteGameplayFarolero.fase(height: 132), const ArteGameplayFarolero.fase(height: 132),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( TarjetaFaseFarolero(
l10n.allVoted, icono: Icons.check_circle,
style: Theme.of(context).textTheme.headlineMedium, titulo: l10n.allVoted,
subtitulo: l10n.tapToReveal,
color: TemaApp.colorVerde,
child: const SizedBox.shrink(),
), ),
const SizedBox(height: 16), const SizedBox(height: 28),
Text( BotonFarolero(
l10n.tapToReveal, texto: l10n.revealResult,
style: Theme.of(context).textTheme.bodyMedium, icono: Icons.visibility,
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () { onPressed: () {
final resultado = estado.procesarVotacion(); final resultado = estado.procesarVotacion();
if (resultado != null) { if (resultado != null) {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => builder: (_) => PantallaResultado(resultado: resultado),
PantallaResultado(resultado: resultado),
), ),
); );
} }
}, },
icon: const Icon(Icons.visibility),
label: Text(l10n.revealResult),
),
), ),
], ],
), ),
), ),
), ),
), ),
),
); );
} }
} }
+10 -40
View File
@@ -183,21 +183,12 @@ class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( BotonFarolero.secundario(
width: double.infinity, texto: l10n.votar,
height: 56, icono: Icons.how_to_vote,
child: ElevatedButton.icon(
onPressed: _votacionCompleta onPressed: _votacionCompleta
? () => widget.onVotos(Map.unmodifiable(_votosPorVotante)) ? () => widget.onVotos(Map.unmodifiable(_votosPorVotante))
: null, : null,
icon: const Icon(Icons.how_to_vote),
label: Text(l10n.votar),
style: ElevatedButton.styleFrom(
backgroundColor: TemaApp.colorAcento,
foregroundColor: Colors.white,
textStyle: const TextStyle(fontSize: 16),
),
),
), ),
], ],
), ),
@@ -232,16 +223,15 @@ class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
BuildContext context, BuildContext context,
JugadorInicioPartida votante, JugadorInicioPartida votante,
) { ) {
return Card( return PanelFarolero(
color: TemaApp.colorSuperficie,
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
child: Padding( padding: const EdgeInsets.all(14),
padding: const EdgeInsets.all(12), borderColor: TemaApp.colorAcento.withValues(alpha: 0.48),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Voto de ${votante.nombre}', AppLocalizations.of(context)!.voteOf(votante.nombre),
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -259,7 +249,6 @@ class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
}), }),
], ],
), ),
),
); );
} }
@@ -269,29 +258,10 @@ class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
required bool selected, required bool selected,
required VoidCallback onTap, required VoidCallback onTap,
}) { }) {
return Card( return SelectorVotoFarolero(
color: selected nombre: '${index + 1}. ${jugador.nombre}',
? TemaApp.colorAcento.withValues(alpha: 0.3) seleccionado: selected,
: TemaApp.colorTarjeta,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: selected
? TemaApp.colorAcento
: TemaApp.colorAcento.withValues(alpha: 0.3),
child: Text(
'${index + 1}',
style: TextStyle(
color: selected ? Colors.white : TemaApp.colorTexto,
),
),
),
title: Text(jugador.nombre),
trailing: selected
? const Icon(Icons.check_circle, color: TemaApp.colorAcento)
: null,
onTap: onTap, onTap: onTap,
),
); );
} }
} }
+247
View File
@@ -335,6 +335,253 @@ class BotonFarolero extends StatelessWidget {
} }
} }
class TarjetaFaseFarolero extends StatelessWidget {
final IconData icono;
final String titulo;
final String? subtitulo;
final Color color;
final Widget child;
final EdgeInsetsGeometry padding;
const TarjetaFaseFarolero({
super.key,
required this.icono,
required this.titulo,
this.subtitulo,
this.color = TemaApp.colorNaranja,
this.child = const SizedBox.shrink(),
this.padding = const EdgeInsets.all(18),
});
@override
Widget build(BuildContext context) {
return PanelFarolero(
padding: padding,
borderColor: color.withValues(alpha: 0.48),
color: TemaApp.colorSuperficie.withValues(alpha: 0.84),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 54,
height: 54,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
color.withValues(alpha: 0.42),
Colors.black.withValues(alpha: 0.62),
],
),
border: Border.all(color: color.withValues(alpha: 0.72)),
),
child: Icon(icono, color: color, size: 30),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
titulo,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: TemaApp.colorDorado,
fontWeight: FontWeight.w900,
),
),
if (subtitulo != null) ...[
const SizedBox(height: 3),
Text(
subtitulo!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: TemaApp.colorTextoSecundario,
),
),
],
],
),
),
],
),
const SizedBox(height: 16),
child,
],
),
);
}
}
class EstadoJugadorFarolero extends StatelessWidget {
final String nombre;
final bool destacado;
final bool completado;
final IconData icono;
final String? subtitulo;
final VoidCallback? onTap;
const EstadoJugadorFarolero({
super.key,
required this.nombre,
this.destacado = false,
this.completado = false,
this.icono = Icons.person,
this.subtitulo,
this.onTap,
});
@override
Widget build(BuildContext context) {
final color = completado ? TemaApp.colorVerde : TemaApp.colorNaranja;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: completado
? TemaApp.colorVerde.withValues(alpha: 0.14)
: TemaApp.colorTarjeta.withValues(alpha: 0.84),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: destacado
? TemaApp.colorDorado.withValues(alpha: 0.72)
: color.withValues(alpha: completado ? 0.54 : 0.28),
),
),
child: Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color.withValues(alpha: 0.16),
border: Border.all(color: color.withValues(alpha: 0.72)),
),
child: Icon(icono, color: color, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nombre,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: destacado ? FontWeight.w900 : FontWeight.w700,
),
),
if (subtitulo != null) ...[
const SizedBox(height: 2),
Text(
subtitulo!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: TemaApp.colorTextoSecundario,
),
),
],
],
),
),
Icon(
completado ? Icons.check_circle : Icons.hourglass_bottom,
color: completado ? TemaApp.colorVerde : TemaApp.colorTextoSecundario,
),
],
),
),
),
),
);
}
}
class SelectorVotoFarolero extends StatelessWidget {
final String nombre;
final bool seleccionado;
final VoidCallback onTap;
const SelectorVotoFarolero({
super.key,
required this.nombre,
required this.seleccionado,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return EstadoJugadorFarolero(
nombre: nombre,
destacado: seleccionado,
completado: seleccionado,
icono: seleccionado ? Icons.check : Icons.person_search,
onTap: onTap,
);
}
}
class TemporizadorFarolero extends StatelessWidget {
final String etiqueta;
final String tiempo;
final bool agotado;
const TemporizadorFarolero({
super.key,
required this.etiqueta,
required this.tiempo,
this.agotado = false,
});
@override
Widget build(BuildContext context) {
final color = agotado ? TemaApp.colorAcento : TemaApp.colorNaranja;
return PanelFarolero(
padding: const EdgeInsets.all(22),
borderColor: color.withValues(alpha: 0.62),
color: color.withValues(alpha: agotado ? 0.18 : 0.10),
child: Stack(
alignment: Alignment.center,
children: [
Image.asset(
'assets/ui/generated/gameplay/gameplay_phase_emblem.webp',
height: 132,
fit: BoxFit.contain,
opacity: const AlwaysStoppedAnimation(0.34),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
etiqueta,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
tiempo,
style: Theme.of(context).textTheme.displaySmall?.copyWith(
color: color,
fontWeight: FontWeight.w900,
),
),
],
),
],
),
);
}
}
class MarcoBotonFaroleroPainter extends CustomPainter { class MarcoBotonFaroleroPainter extends CustomPainter {
final LinearGradient gradient; final LinearGradient gradient;
final bool destacado; final bool destacado;