Implementado:
No se puede marcar “vista” sin revelar la palabra antes. Se puede volver a ver la palabra durante debate/votación/resultado. Notas online privadas por partida y jugador. Tests añadidos para notas scoped. Ajusté roomId en el payload de inicio para que las notas no se mezclen entre partidas.
This commit is contained in:
229
lib/pantallas/pantalla_notas_online.dart
Normal file
229
lib/pantallas/pantalla_notas_online.dart
Normal file
@@ -0,0 +1,229 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:farolero/l10n/generated/app_localizations.dart';
|
||||
import 'package:farolero/modelos/inicio_partida_multijugador.dart';
|
||||
import 'package:farolero/modelos/jugador.dart';
|
||||
import 'package:farolero/servicios/servicio_notas.dart';
|
||||
import 'package:farolero/tema/tema_app.dart';
|
||||
|
||||
class PantallaNotasOnline extends StatefulWidget {
|
||||
final String partidaId;
|
||||
final List<Jugador> jugadores;
|
||||
final List<JugadorInicioPartida> autoresControlados;
|
||||
|
||||
const PantallaNotasOnline({
|
||||
super.key,
|
||||
required this.partidaId,
|
||||
required this.jugadores,
|
||||
required this.autoresControlados,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PantallaNotasOnline> createState() => _PantallaNotasOnlineState();
|
||||
}
|
||||
|
||||
class _PantallaNotasOnlineState extends State<PantallaNotasOnline> {
|
||||
JugadorInicioPartida? _autor;
|
||||
final Map<String, TextEditingController> _controladores = {};
|
||||
final TextEditingController _notaLibreController = TextEditingController();
|
||||
bool _cargando = false;
|
||||
|
||||
List<Jugador> get _jugadoresActivos =>
|
||||
widget.jugadores.where((jugador) => !jugador.eliminado).toList();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
for (final jugador in _jugadoresActivos) {
|
||||
_controladores[jugador.id] = TextEditingController();
|
||||
}
|
||||
if (widget.autoresControlados.length == 1) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _seleccionarAutor(widget.autoresControlados.first);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _controladores.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
_notaLibreController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _seleccionarAutor(JugadorInicioPartida autor) async {
|
||||
setState(() {
|
||||
_autor = autor;
|
||||
_cargando = true;
|
||||
for (final controller in _controladores.values) {
|
||||
controller.clear();
|
||||
}
|
||||
_notaLibreController.clear();
|
||||
});
|
||||
|
||||
final datos = await ServicioNotas.cargarNotasPartida(
|
||||
partidaId: widget.partidaId,
|
||||
autorJugadorId: autor.jugadorId,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final notas = datos['notas'] as Map<String, String>;
|
||||
for (final entry in notas.entries) {
|
||||
_controladores[entry.key]?.text = entry.value;
|
||||
}
|
||||
_notaLibreController.text = datos['notaLibre'] as String;
|
||||
setState(() => _cargando = false);
|
||||
}
|
||||
|
||||
Future<void> _guardarNotas() async {
|
||||
final autor = _autor;
|
||||
if (autor == null) return;
|
||||
final notas = <String, String>{};
|
||||
for (final entry in _controladores.entries) {
|
||||
final texto = entry.value.text.trim();
|
||||
if (texto.isNotEmpty) notas[entry.key] = texto;
|
||||
}
|
||||
await ServicioNotas.guardarNotasPartida(
|
||||
partidaId: widget.partidaId,
|
||||
autorJugadorId: autor.jugadorId,
|
||||
notasPorJugador: notas,
|
||||
notaLibre: _notaLibreController.text,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
onPopInvokedWithResult: (_, __) => _guardarNotas(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.notesTitle),
|
||||
actions: [
|
||||
if (_autor != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: () async {
|
||||
await _guardarNotas();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.notesSaved)),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _autor == null ? _buildSelector(context) : _buildNotas(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelector(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.whoAreYou,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: widget.autoresControlados
|
||||
.map(
|
||||
(autor) => Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.edit_note),
|
||||
title: Text(autor.nombre),
|
||||
onTap: () => _seleccionarAutor(autor),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotas(BuildContext context) {
|
||||
if (_cargando) return const Center(child: CircularProgressIndicator());
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final autor = _autor!;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.autoresControlados.length > 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () async {
|
||||
await _guardarNotas();
|
||||
if (!mounted) return;
|
||||
setState(() => _autor = null);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.notesOf(autor.nombre),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.notesAboutPlayers,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: TemaApp.colorTextoSecundario,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
..._jugadoresActivos
|
||||
.where((jugador) => jugador.id != autor.jugadorId)
|
||||
.map(
|
||||
(jugador) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: TextField(
|
||||
controller: _controladores[jugador.id],
|
||||
decoration: InputDecoration(
|
||||
labelText: jugador.nombre,
|
||||
prefixIcon: const Icon(Icons.person, size: 20),
|
||||
hintText: l10n.playerNoteHint,
|
||||
),
|
||||
maxLines: 2,
|
||||
minLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.freeNote,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: TemaApp.colorTextoSecundario,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _notaLibreController,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.freeNoteHint,
|
||||
prefixIcon: const Icon(Icons.note, size: 20),
|
||||
),
|
||||
maxLines: 5,
|
||||
minLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user