Compare commits
2 Commits
3df3ae1e95
...
911bd4c4a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
911bd4c4a3 | ||
|
|
d3fc3386f9 |
58
.atl/skill-registry.md
Normal file
58
.atl/skill-registry.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Skill Registry
|
||||||
|
|
||||||
|
**Delegator use only.** Any agent that launches sub-agents reads this registry to resolve compact rules, then injects them directly into sub-agent prompts. Sub-agents do NOT read this registry or individual SKILL.md files.
|
||||||
|
|
||||||
|
See `_shared/skill-resolver.md` for the full resolution protocol.
|
||||||
|
|
||||||
|
## User Skills
|
||||||
|
|
||||||
|
| Trigger | Skill | Path |
|
||||||
|
|---------|-------|------|
|
||||||
|
| When creating a pull request, opening a PR, or preparing changes for review | branch-pr | /Users/freetlab/.config/opencode/skills/branch-pr/SKILL.md |
|
||||||
|
| When creating a GitHub issue, reporting a bug, or requesting a feature | issue-creation | /Users/freetlab/.config/opencode/skills/issue-creation/SKILL.md |
|
||||||
|
| When user says "judgment day", "judgment-day", "review adversarial", "dual review", "doble review", "juzgar", "que lo juzguen" | judgment-day | /Users/freetlab/.config/opencode/skills/judgment-day/SKILL.md |
|
||||||
|
| When user asks to create a new skill, add agent instructions, or document patterns for AI | skill-creator | /Users/freetlab/.config/opencode/skills/skill-creator/SKILL.md |
|
||||||
|
|
||||||
|
## Compact Rules
|
||||||
|
|
||||||
|
Pre-digested rules per skill. Delegators copy matching blocks into sub-agent prompts as `## Project Standards (auto-resolved)`.
|
||||||
|
|
||||||
|
### branch-pr
|
||||||
|
- Every PR MUST link an approved issue — no exceptions
|
||||||
|
- Every PR MUST have exactly one `type:*` label
|
||||||
|
- Automated checks must pass before merge is possible
|
||||||
|
- Branch names must match: `^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)/[a-z0-9._-]+$`
|
||||||
|
- Conventional commits: `^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9\._-]+\))?!?: .+`
|
||||||
|
- Commit type determines PR label: feat→type:feature, fix→type:bug, docs→type:docs, refactor→type:refactor, chore→type:chore, style→type:chore, perf→type:feature, test→type:chore, build→type:chore, ci→type:chore, revert→type:bug
|
||||||
|
- PR body must contain: Closes #N (linked issue), PR type checkbox, Summary, Changes Table, Test Plan
|
||||||
|
|
||||||
|
### issue-creation
|
||||||
|
- Blank issues are disabled — MUST use a template (bug report or feature request)
|
||||||
|
- Every issue gets `status:needs-review` automatically on creation
|
||||||
|
- A maintainer MUST add `status:approved` before any PR can be opened
|
||||||
|
- Questions go to Discussions, not issues
|
||||||
|
- Bug report template required fields: Pre-flight Checks, Bug Description, Steps to Reproduce, Expected Behavior, Actual Behavior, Operating System, Agent/Client, Shell
|
||||||
|
- Feature request template required fields: Pre-flight Checks, Problem Description, Proposed Solution, Affected Area
|
||||||
|
|
||||||
|
### judgment-day
|
||||||
|
- Launch TWO sub-agents via delegate (async, parallel — never sequential)
|
||||||
|
- Each agent receives the same target but works independently
|
||||||
|
- Neither agent knows about the other — no cross-contamination
|
||||||
|
- Classify warnings as WARNING (real) or WARNING (theoretical)
|
||||||
|
- If confirmed CRITICALs or real WARNINGs exist → delegate Fix Agent
|
||||||
|
- After Fix Agent completes → re-launch both judges in parallel
|
||||||
|
- After 2 fix iterations, if issues remain → escalate to user
|
||||||
|
|
||||||
|
### skill-creator
|
||||||
|
- Create a skill when: pattern is used repeatedly, project-specific conventions differ, complex workflows need steps, decision trees help AI
|
||||||
|
- Don't create a skill when: documentation exists, pattern is trivial, one-off task
|
||||||
|
- Skill structure: frontmatter (name, description, triggers, allowed-tools), Critical Rules, When to Use, Patterns, Commands
|
||||||
|
|
||||||
|
## Project Conventions
|
||||||
|
|
||||||
|
| File | Path | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| SPEC.md | /Users/freetlab/Proyectos/farolero/SPEC.md | Existing SDD artifacts with Explore/Propose/Spec/Tasks/Apply/Verify phases |
|
||||||
|
| .gga | /Users/freetlab/Proyectos/farolero/.gga | Gentleman Guardian Angel config (AI provider, file patterns, rules file) |
|
||||||
|
|
||||||
|
Read the convention files listed above for project-specific patterns and rules. All referenced paths have been extracted — no need to read index files to discover more.
|
||||||
50
.gga
Normal file
50
.gga
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Gentleman Guardian Angel Configuration
|
||||||
|
# https://github.com/your-org/gga
|
||||||
|
|
||||||
|
# AI Provider (required)
|
||||||
|
# Options: claude, gemini, codex, opencode, ollama:<model>, lmstudio[:model], github:<model>
|
||||||
|
# Examples:
|
||||||
|
# PROVIDER="claude"
|
||||||
|
# PROVIDER="gemini"
|
||||||
|
# PROVIDER="codex"
|
||||||
|
# PROVIDER="opencode"
|
||||||
|
# PROVIDER="opencode:anthropic/claude-opus-4-5"
|
||||||
|
# PROVIDER="ollama:llama3.2"
|
||||||
|
# PROVIDER="ollama:codellama"
|
||||||
|
# PROVIDER="lmstudio"
|
||||||
|
# PROVIDER="lmstudio:qwen2.5-coder-7b-instruct"
|
||||||
|
# PROVIDER="github:gpt-4o"
|
||||||
|
# PROVIDER="github:deepseek-r1"
|
||||||
|
PROVIDER="claude"
|
||||||
|
|
||||||
|
# File patterns to include in review (comma-separated)
|
||||||
|
# Default: * (all files)
|
||||||
|
# Examples:
|
||||||
|
# FILE_PATTERNS="*.ts,*.tsx"
|
||||||
|
# FILE_PATTERNS="*.py"
|
||||||
|
# FILE_PATTERNS="*.go,*.mod"
|
||||||
|
FILE_PATTERNS="*.ts,*.tsx,*.js,*.jsx"
|
||||||
|
|
||||||
|
# File patterns to exclude from review (comma-separated)
|
||||||
|
# Default: none
|
||||||
|
# Examples:
|
||||||
|
# EXCLUDE_PATTERNS="*.test.ts,*.spec.ts"
|
||||||
|
# EXCLUDE_PATTERNS="*_test.go,*.mock.ts"
|
||||||
|
EXCLUDE_PATTERNS="*.test.ts,*.spec.ts,*.test.tsx,*.spec.tsx,*.d.ts"
|
||||||
|
|
||||||
|
# File containing code review rules
|
||||||
|
# Default: AGENTS.md
|
||||||
|
RULES_FILE="AGENTS.md"
|
||||||
|
|
||||||
|
# Strict mode: fail if AI response is ambiguous
|
||||||
|
# Default: true
|
||||||
|
STRICT_MODE="true"
|
||||||
|
|
||||||
|
# Timeout in seconds for AI provider response
|
||||||
|
# Default: 300 (5 minutes)
|
||||||
|
# Increase for large changesets or slow connections
|
||||||
|
TIMEOUT="300"
|
||||||
|
|
||||||
|
# Base branch for --pr-mode (auto-detects main/master/develop if empty)
|
||||||
|
# Default: auto-detect
|
||||||
|
# PR_BASE_BRANCH="main"
|
||||||
16
AGENTS.md
Normal file
16
AGENTS.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Code Review Rules
|
||||||
|
|
||||||
|
## Flutter / Dart
|
||||||
|
|
||||||
|
- Use functional components with Flutter hooks when possible
|
||||||
|
- Follow Clean Architecture: pantallas, modelos, servicios, estado
|
||||||
|
- Use Provider for state management
|
||||||
|
- Use flutter_test for unit testing
|
||||||
|
- Run flutter analyze before committing
|
||||||
|
- Use conventional commits: feat, fix, chore, docs, etc.
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- No Co-Authored-By in commits
|
||||||
|
- Conventional commit format
|
||||||
|
- Test before push
|
||||||
@@ -12,11 +12,17 @@ class EstadoJuego extends ChangeNotifier {
|
|||||||
final Map<String, String> _votos = {}; // votanteId -> votadoId
|
final Map<String, String> _votos = {}; // votanteId -> votadoId
|
||||||
bool _cargando = false;
|
bool _cargando = false;
|
||||||
|
|
||||||
|
/// Jugador local del host en modo multi-dispositivo
|
||||||
|
Jugador? _hostLocal;
|
||||||
|
|
||||||
BancoPalabras? get banco => _banco;
|
BancoPalabras? get banco => _banco;
|
||||||
Partida? get partida => _partida;
|
Partida? get partida => _partida;
|
||||||
Map<String, String> get votos => Map.unmodifiable(_votos);
|
Map<String, String> get votos => Map.unmodifiable(_votos);
|
||||||
bool get cargando => _cargando;
|
bool get cargando => _cargando;
|
||||||
|
|
||||||
|
/// Jugador local del host (para modo multi-dispositivo)
|
||||||
|
Jugador? get hostLocal => _hostLocal;
|
||||||
|
|
||||||
Future<void> cargarBanco() async {
|
Future<void> cargarBanco() async {
|
||||||
_cargando = true;
|
_cargando = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -25,6 +31,16 @@ class EstadoJuego extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Establece el jugador local del host para modo multi-dispositivo
|
||||||
|
void setHostJugador(String nombre) {
|
||||||
|
_hostLocal = Jugador(
|
||||||
|
id: 'host-local',
|
||||||
|
nombre: nombre,
|
||||||
|
endpointId: null, // El host local no tiene endpointId
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Crea una nueva partida con la configuración dada y lista de jugadores
|
/// Crea una nueva partida con la configuración dada y lista de jugadores
|
||||||
void crearPartida({
|
void crearPartida({
|
||||||
required ConfigPartida config,
|
required ConfigPartida config,
|
||||||
|
|||||||
@@ -255,5 +255,11 @@
|
|||||||
"searchingGames": "Searching for nearby games...",
|
"searchingGames": "Searching for nearby games...",
|
||||||
"noGamesFound": "No games found",
|
"noGamesFound": "No games found",
|
||||||
"noGamesFoundHint": "Make sure the host has the room open and you are nearby",
|
"noGamesFoundHint": "Make sure the host has the room open and you are nearby",
|
||||||
"orScanQR": "Not showing up? Scan the host's QR code"
|
"orScanQR": "Not showing up? Scan the host's QR code",
|
||||||
|
"selectYourProfile": "Your profile",
|
||||||
|
"selectProfile": "Select a profile",
|
||||||
|
"createNewUser": "Create new user",
|
||||||
|
"userNameRequired": "Name cannot be empty",
|
||||||
|
"profileSelected": "Profile selected",
|
||||||
|
"availableProfiles": "Available profiles"
|
||||||
}
|
}
|
||||||
@@ -277,5 +277,11 @@
|
|||||||
"votacionSolicitada": "Votación solicitada",
|
"votacionSolicitada": "Votación solicitada",
|
||||||
"whoDoYouThinkIsTheImpostor": "¿Quién es el impostor?",
|
"whoDoYouThinkIsTheImpostor": "¿Quién es el impostor?",
|
||||||
"selectOnePlayer": "Selecciona a un jugador para votar",
|
"selectOnePlayer": "Selecciona a un jugador para votar",
|
||||||
"votar": "Votar"
|
"votar": "Votar",
|
||||||
|
"selectYourProfile": "Tu perfil",
|
||||||
|
"selectProfile": "Selecciona un perfil",
|
||||||
|
"createNewUser": "Crear nuevo usuario",
|
||||||
|
"userNameRequired": "El nombre no puede estar vacio",
|
||||||
|
"profileSelected": "Perfil seleccionado",
|
||||||
|
"availableProfiles": "Perfiles disponibles"
|
||||||
}
|
}
|
||||||
@@ -1196,6 +1196,42 @@ abstract class AppLocalizations {
|
|||||||
/// In es, this message translates to:
|
/// In es, this message translates to:
|
||||||
/// **'Votar'**
|
/// **'Votar'**
|
||||||
String get votar;
|
String get votar;
|
||||||
|
|
||||||
|
/// No description provided for @selectYourProfile.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Tu perfil'**
|
||||||
|
String get selectYourProfile;
|
||||||
|
|
||||||
|
/// No description provided for @selectProfile.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Selecciona un perfil'**
|
||||||
|
String get selectProfile;
|
||||||
|
|
||||||
|
/// No description provided for @createNewUser.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Crear nuevo usuario'**
|
||||||
|
String get createNewUser;
|
||||||
|
|
||||||
|
/// No description provided for @userNameRequired.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'El nombre no puede estar vacio'**
|
||||||
|
String get userNameRequired;
|
||||||
|
|
||||||
|
/// No description provided for @profileSelected.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Perfil seleccionado'**
|
||||||
|
String get profileSelected;
|
||||||
|
|
||||||
|
/// No description provided for @availableProfiles.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Perfiles disponibles'**
|
||||||
|
String get availableProfiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -579,4 +579,22 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,4 +582,22 @@ class AppLocalizationsCa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,4 +585,22 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,4 +579,22 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Your profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Select a profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Create new user';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'Name cannot be empty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Profile selected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Available profiles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -581,4 +581,22 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -584,4 +584,22 @@ class AppLocalizationsEu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,4 +582,22 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -581,4 +581,22 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,4 +582,22 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,4 +579,22 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,4 +579,22 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,4 +582,22 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,4 +582,22 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -583,4 +583,22 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,4 +582,22 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -581,4 +581,22 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -578,6 +578,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get votar => 'Votar';
|
String get votar => 'Votar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectYourProfile => 'Tu perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectProfile => 'Selecciona un perfil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createNewUser => 'Crear nuevo usuario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userNameRequired => 'El nombre no puede estar vacio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSelected => 'Perfil seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableProfiles => 'Perfiles disponibles';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||||
|
|||||||
20
lib/modelos/usuario.dart
Normal file
20
lib/modelos/usuario.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/// Modelo de usuario para el pool de usuarios en modo multi-dispositivo
|
||||||
|
class Usuario {
|
||||||
|
final String id;
|
||||||
|
final String nombre;
|
||||||
|
final String? avatar;
|
||||||
|
|
||||||
|
Usuario({required this.id, required this.nombre, this.avatar});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'nombre': nombre,
|
||||||
|
if (avatar != null) 'avatar': avatar,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory Usuario.fromJson(Map<String, dynamic> json) => Usuario(
|
||||||
|
id: json['id'] as String,
|
||||||
|
nombre: json['nombre'] as String,
|
||||||
|
avatar: json['avatar'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../estado/estado_juego.dart';
|
import '../estado/estado_juego.dart';
|
||||||
import '../modelos/palabra.dart';
|
import '../modelos/palabra.dart';
|
||||||
import '../modelos/partida.dart';
|
import '../modelos/partida.dart';
|
||||||
|
import '../modelos/usuario.dart';
|
||||||
import '../servicios/servicio_nearby.dart';
|
import '../servicios/servicio_nearby.dart';
|
||||||
import '../servicios/servicio_permisos.dart';
|
import '../servicios/servicio_permisos.dart';
|
||||||
import '../tema/tema_app.dart';
|
import '../tema/tema_app.dart';
|
||||||
@@ -121,8 +122,8 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Pedir nombre del host
|
// 2. Seleccionar o crear usuario del pool
|
||||||
final nombre = await _pedirNombreHost();
|
final nombre = await _seleccionarUsuarioHost();
|
||||||
if (nombre == null || nombre.trim().isEmpty) return;
|
if (nombre == null || nombre.trim().isEmpty) return;
|
||||||
|
|
||||||
// 3. Iniciar host en Nearby
|
// 3. Iniciar host en Nearby
|
||||||
@@ -152,6 +153,10 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
|||||||
onIniciar: () {
|
onIniciar: () {
|
||||||
// Cuando el host toca "Iniciar" con suficientes jugadores
|
// Cuando el host toca "Iniciar" con suficientes jugadores
|
||||||
final estado = context.read<EstadoJuego>();
|
final estado = context.read<EstadoJuego>();
|
||||||
|
|
||||||
|
// Set host local player first (required for host-included game)
|
||||||
|
estado.setHostJugador(nombre.trim());
|
||||||
|
|
||||||
final jugadoresMulti = [
|
final jugadoresMulti = [
|
||||||
nombre.trim(),
|
nombre.trim(),
|
||||||
...nearby.jugadores.map((j) => j.nombre),
|
...nearby.jugadores.map((j) => j.nombre),
|
||||||
@@ -207,6 +212,133 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Muestra diálogo para seleccionar usuario del pool o crear nuevo
|
||||||
|
Future<String?> _seleccionarUsuarioHost() async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final nearby = context.read<ServicioNearby>();
|
||||||
|
final usuarios = nearby.usuarios;
|
||||||
|
|
||||||
|
// Si hay usuarios en el pool, mostrar selección
|
||||||
|
if (usuarios.isNotEmpty) {
|
||||||
|
String? seleccionado;
|
||||||
|
return showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => StatefulBuilder(
|
||||||
|
builder: (ctx, setDialogState) => AlertDialog(
|
||||||
|
title: Text(l10n.selectYourProfile),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: seleccionado,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: const Icon(Icons.person),
|
||||||
|
hintText: l10n.selectProfile,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
// Opción para crear nuevo usuario
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: '_new_',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add, size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(l10n.createNewUser),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Usuarios existentes
|
||||||
|
...usuarios.map((usuario) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: usuario.nombre,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(usuario.avatar ?? '👤'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(usuario.nombre),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onChanged: (valor) {
|
||||||
|
setDialogState(() => seleccionado = valor);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (seleccionado == '_new_') {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_crearNuevoUsuarioHost();
|
||||||
|
} else if (seleccionado != null) {
|
||||||
|
Navigator.pop(ctx, seleccionado);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(l10n.accept),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool vacío, pedir nombre nuevo
|
||||||
|
return _pedirNombreHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un nuevo usuario y lo agrega al pool
|
||||||
|
Future<String?> _crearNuevoUsuarioHost() async {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final nearby = context.read<ServicioNearby>();
|
||||||
|
|
||||||
|
final nombre = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(l10n.createNewUser),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: l10n.yourName,
|
||||||
|
prefixIcon: const Icon(Icons.person),
|
||||||
|
),
|
||||||
|
onSubmitted: (v) => Navigator.pop(ctx, v),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, controller.text),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nombre != null && nombre.trim().isNotEmpty) {
|
||||||
|
final nuevoUsuario = Usuario(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
nombre: nombre.trim(),
|
||||||
|
);
|
||||||
|
nearby.agregarUsuario(nuevoUsuario);
|
||||||
|
return nombre.trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Método original para pedir nombre (usado cuando pool vacío)
|
||||||
Future<String?> _pedirNombreHost() async {
|
Future<String?> _pedirNombreHost() async {
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:farolero/l10n/generated/app_localizations.dart';
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
||||||
|
import '../modelos/usuario.dart';
|
||||||
import '../servicios/servicio_nearby.dart';
|
import '../servicios/servicio_nearby.dart';
|
||||||
import '../tema/tema_app.dart';
|
import '../tema/tema_app.dart';
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class PantallaLobbyHost extends StatefulWidget {
|
|||||||
|
|
||||||
class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
||||||
bool _iniciando = false;
|
bool _iniciando = false;
|
||||||
|
String? _perfilSeleccionado;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -66,6 +68,65 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Selección de perfil
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.selectYourProfile,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _perfilSeleccionado,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: const Icon(Icons.person),
|
||||||
|
hintText: l10n.selectProfile,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
// Opción para crear nuevo usuario
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: '_new_',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add, size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(l10n.createNewUser),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Usuarios existentes
|
||||||
|
...nearby.usuarios.map((usuario) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: usuario.nombre,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(usuario.avatar ?? '👤'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(usuario.nombre),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onChanged: (valor) {
|
||||||
|
if (valor == '_new_') {
|
||||||
|
_crearNuevoUsuario(context);
|
||||||
|
} else {
|
||||||
|
setState(() => _perfilSeleccionado = valor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Lista de jugadores
|
// Lista de jugadores
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -116,7 +177,10 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text('📱', style: TextStyle(fontSize: 48)),
|
const Text(
|
||||||
|
'📱',
|
||||||
|
style: TextStyle(fontSize: 48),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
l10n.waitingForPlayers,
|
l10n.waitingForPlayers,
|
||||||
@@ -141,15 +205,26 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
|||||||
if (totalJugadores < 3)
|
if (totalJugadores < 3)
|
||||||
Text(
|
Text(
|
||||||
l10n.needMorePlayers(3 - totalJugadores),
|
l10n.needMorePlayers(3 - totalJugadores),
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(
|
||||||
color: TemaApp.colorNaranja,
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(color: TemaApp.colorNaranja),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (_perfilSeleccionado == null)
|
||||||
|
Text(
|
||||||
|
l10n.selectProfile,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(color: TemaApp.colorNaranja),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: totalJugadores >= 3 && !_iniciando
|
onPressed:
|
||||||
|
totalJugadores >= 3 &&
|
||||||
|
_perfilSeleccionado != null &&
|
||||||
|
!_iniciando
|
||||||
? () {
|
? () {
|
||||||
setState(() => _iniciando = true);
|
setState(() => _iniciando = true);
|
||||||
widget.onIniciar();
|
widget.onIniciar();
|
||||||
@@ -181,10 +256,7 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
|||||||
Text(esHost ? '👑' : '🎭', style: const TextStyle(fontSize: 20)),
|
Text(esHost ? '👑' : '🎭', style: const TextStyle(fontSize: 20)),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(nombre, style: Theme.of(context).textTheme.titleMedium),
|
||||||
nombre,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (esHost)
|
if (esHost)
|
||||||
Container(
|
Container(
|
||||||
@@ -206,4 +278,46 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _crearNuevoUsuario(BuildContext context) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
final nearby = context.read<ServicioNearby>();
|
||||||
|
|
||||||
|
final nombre = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(l10n.createNewUser),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: l10n.yourName,
|
||||||
|
prefixIcon: const Icon(Icons.person),
|
||||||
|
),
|
||||||
|
onSubmitted: (v) => Navigator.pop(ctx, v),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, controller.text),
|
||||||
|
child: Text(l10n.accept),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nombre != null && nombre.trim().isNotEmpty) {
|
||||||
|
final nuevoUsuario = Usuario(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
nombre: nombre.trim(),
|
||||||
|
);
|
||||||
|
nearby.agregarUsuario(nuevoUsuario);
|
||||||
|
setState(() => _perfilSeleccionado = nombre.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:farolero/l10n/generated/app_localizations.dart';
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
||||||
import '../modelos/jugador.dart';
|
import '../modelos/jugador.dart';
|
||||||
|
import '../modelos/usuario.dart';
|
||||||
import '../servicios/servicio_nearby.dart';
|
import '../servicios/servicio_nearby.dart';
|
||||||
import '../servicios/servicio_permisos.dart';
|
import '../servicios/servicio_permisos.dart';
|
||||||
import '../tema/tema_app.dart';
|
import '../tema/tema_app.dart';
|
||||||
@@ -104,7 +105,10 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
if (nearby.hostEndpointId != null) {
|
if (nearby.hostEndpointId != null) {
|
||||||
nearby.enviarMensaje(
|
nearby.enviarMensaje(
|
||||||
nearby.hostEndpointId!,
|
nearby.hostEndpointId!,
|
||||||
MensajeP2P(tipo: TipoMensaje.ping, datos: {'solicitoVotacion': true}),
|
MensajeP2P(
|
||||||
|
tipo: TipoMensaje.ping,
|
||||||
|
datos: {'solicitoVotacion': true},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -122,7 +126,10 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
if (nearby.hostEndpointId != null) {
|
if (nearby.hostEndpointId != null) {
|
||||||
nearby.enviarMensaje(
|
nearby.enviarMensaje(
|
||||||
nearby.hostEndpointId!,
|
nearby.hostEndpointId!,
|
||||||
MensajeP2P(tipo: TipoMensaje.voto, datos: {'votoporId': votoporId}),
|
MensajeP2P(
|
||||||
|
tipo: TipoMensaje.voto,
|
||||||
|
datos: {'votoporId': votoporId},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -148,7 +155,8 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
final permisosOk = await ServicioPermisos.solicitarPermisosNearby(context);
|
final permisosOk = await ServicioPermisos.solicitarPermisosNearby(context);
|
||||||
if (!permisosOk) {
|
if (!permisosOk) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = 'Se necesitan permisos de Bluetooth y ubicación para buscar partidas.';
|
_error =
|
||||||
|
'Se necesitan permisos de Bluetooth y ubicación para buscar partidas.';
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,7 +172,8 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = 'No se pudo iniciar la búsqueda. Verifica Bluetooth y ubicación.';
|
_error =
|
||||||
|
'No se pudo iniciar la búsqueda. Verifica Bluetooth y ubicación.';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +188,10 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
final nearby = context.read<ServicioNearby>();
|
final nearby = context.read<ServicioNearby>();
|
||||||
// Parar discovery antes de conectar
|
// Parar discovery antes de conectar
|
||||||
await nearby.pararBusqueda();
|
await nearby.pararBusqueda();
|
||||||
final ok = await nearby.conectarAHost(endpointId, _nombreController.text.trim());
|
final ok = await nearby.conectarAHost(
|
||||||
|
endpointId,
|
||||||
|
_nombreController.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
if (!ok && mounted) {
|
if (!ok && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -211,7 +223,8 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_escaneandoQR = false;
|
_escaneandoQR = false;
|
||||||
_conectando = true;
|
_conectando = true;
|
||||||
_salaSeleccionada = datos['host'] as String? ?? datos['sala'] as String? ?? 'Sala';
|
_salaSeleccionada =
|
||||||
|
datos['host'] as String? ?? datos['sala'] as String? ?? 'Sala';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Iniciar búsqueda para que Nearby encuentre al host
|
// Iniciar búsqueda para que Nearby encuentre al host
|
||||||
@@ -252,9 +265,7 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
|
|
||||||
Widget _buildFormularioNombre(BuildContext context, AppLocalizations l10n) {
|
Widget _buildFormularioNombre(BuildContext context, AppLocalizations l10n) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(l10n.joinGameTitle)),
|
||||||
title: Text(l10n.joinGameTitle),
|
|
||||||
),
|
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Form(
|
child: Form(
|
||||||
@@ -310,7 +321,11 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
|
|
||||||
// ==================== PASO 2: DISCOVERY ====================
|
// ==================== PASO 2: DISCOVERY ====================
|
||||||
|
|
||||||
Widget _buildDiscovery(BuildContext context, AppLocalizations l10n, ServicioNearby nearby) {
|
Widget _buildDiscovery(
|
||||||
|
BuildContext context,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ServicioNearby nearby,
|
||||||
|
) {
|
||||||
final hosts = nearby.hostsEncontrados;
|
final hosts = nearby.hostsEncontrados;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -345,7 +360,8 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 20, height: 20,
|
width: 20,
|
||||||
|
height: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
color: TemaApp.colorNaranja,
|
color: TemaApp.colorNaranja,
|
||||||
@@ -378,9 +394,8 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.noGamesFoundHint,
|
l10n.noGamesFoundHint,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium
|
||||||
color: Colors.grey,
|
?.copyWith(color: Colors.grey),
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -406,9 +421,9 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.orScanQR,
|
l10n.orScanQR,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(
|
||||||
color: Colors.grey,
|
context,
|
||||||
),
|
).textTheme.bodyMedium?.copyWith(color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -451,14 +466,18 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Toca para unirte',
|
'Toca para unirte',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(
|
||||||
color: Colors.grey,
|
context,
|
||||||
),
|
).textTheme.bodySmall?.copyWith(color: Colors.grey),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
|
const Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -499,9 +518,9 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.scanHostQR,
|
l10n.scanHostQR,
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(
|
||||||
color: Colors.white,
|
context,
|
||||||
),
|
).textTheme.titleLarge?.copyWith(color: Colors.white),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -514,6 +533,9 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
// ==================== ESPERA ====================
|
// ==================== ESPERA ====================
|
||||||
|
|
||||||
Widget _buildPantallaEspera(BuildContext context, AppLocalizations l10n) {
|
Widget _buildPantallaEspera(BuildContext context, AppLocalizations l10n) {
|
||||||
|
final nearby = context.watch<ServicioNearby>();
|
||||||
|
final usuarios = nearby.usuarios;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(_salaSeleccionada ?? l10n.joinGameTitle),
|
title: Text(_salaSeleccionada ?? l10n.joinGameTitle),
|
||||||
@@ -531,12 +553,12 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(24),
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
// Estado de conexión
|
||||||
const Text('✅', style: TextStyle(fontSize: 64)),
|
const Text('✅', style: TextStyle(fontSize: 64)),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
@@ -556,13 +578,127 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
|
|||||||
l10n.waitingForHost,
|
l10n.waitingForHost,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Pool de usuarios disponibles (tarea 3.4)
|
||||||
|
if (usuarios.isNotEmpty) ...[
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.availableProfiles,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Opción crear nuevo usuario (tarea 3.5)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: TemaApp.colorAcento,
|
||||||
|
),
|
||||||
|
title: Text(l10n.createNewUser),
|
||||||
|
onTap: () => _crearNuevoUsuario(context),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
// Usuarios existentes
|
||||||
|
...usuarios.map(
|
||||||
|
(usuario) => ListTile(
|
||||||
|
leading: Text(
|
||||||
|
usuario.avatar ?? '👤',
|
||||||
|
style: const TextStyle(fontSize: 24),
|
||||||
|
),
|
||||||
|
title: Text(usuario.nombre),
|
||||||
|
onTap: () {
|
||||||
|
// Seleccionar usuario - enviar al host
|
||||||
|
_enviarUsuarioAlHost(usuario);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
] else ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Si no hay usuarios, permitir crear uno
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => _crearNuevoUsuario(context),
|
||||||
|
icon: const Icon(Icons.person_add),
|
||||||
|
label: Text(l10n.createNewUser),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crea un nuevo usuario y lo envía al host
|
||||||
|
Future<void> _crearNuevoUsuario(BuildContext context) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
final nearby = context.read<ServicioNearby>();
|
||||||
|
|
||||||
|
final nombre = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(l10n.createNewUser),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: l10n.yourName,
|
||||||
|
prefixIcon: const Icon(Icons.person),
|
||||||
|
),
|
||||||
|
onSubmitted: (v) => Navigator.pop(ctx, v),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, controller.text),
|
||||||
|
child: Text(l10n.accept),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nombre != null && nombre.trim().isNotEmpty) {
|
||||||
|
final nuevoUsuario = Usuario(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
nombre: nombre.trim(),
|
||||||
|
);
|
||||||
|
// Agregar localmente
|
||||||
|
nearby.agregarUsuario(nuevoUsuario);
|
||||||
|
// Enviar al host
|
||||||
|
_enviarUsuarioAlHost(nuevoUsuario);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envía el usuario seleccionado/creado al host
|
||||||
|
void _enviarUsuarioAlHost(Usuario usuario) {
|
||||||
|
final nearby = context.read<ServicioNearby>();
|
||||||
|
if (nearby.hostEndpointId != null) {
|
||||||
|
nearby.enviarMensaje(
|
||||||
|
nearby.hostEndpointId!,
|
||||||
|
MensajeP2P(
|
||||||
|
tipo: TipoMensaje.usuarioNuevo,
|
||||||
|
datos: {'usuario': usuario.toJson()},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('${usuario.nombre} seleccionado')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== HELPERS ====================
|
// ==================== HELPERS ====================
|
||||||
|
|
||||||
Widget _buildError(String msg) {
|
Widget _buildError(String msg) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:nearby_connections/nearby_connections.dart';
|
import 'package:nearby_connections/nearby_connections.dart';
|
||||||
|
import '../modelos/usuario.dart';
|
||||||
|
|
||||||
/// Tipos de mensajes en el protocolo P2P
|
/// Tipos de mensajes en el protocolo P2P
|
||||||
enum TipoMensaje {
|
enum TipoMensaje {
|
||||||
@@ -14,6 +15,9 @@ enum TipoMensaje {
|
|||||||
listo,
|
listo,
|
||||||
ping,
|
ping,
|
||||||
jugadorDesconectado,
|
jugadorDesconectado,
|
||||||
|
usuarioNuevo,
|
||||||
|
usuarioEliminado,
|
||||||
|
usuariosActualizados,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mensaje del protocolo P2P entre dispositivos
|
/// Mensaje del protocolo P2P entre dispositivos
|
||||||
@@ -23,10 +27,7 @@ class MensajeP2P {
|
|||||||
|
|
||||||
MensajeP2P({required this.tipo, required this.datos});
|
MensajeP2P({required this.tipo, required this.datos});
|
||||||
|
|
||||||
String toJson() => json.encode({
|
String toJson() => json.encode({'tipo': tipo.name, 'datos': datos});
|
||||||
'tipo': tipo.name,
|
|
||||||
'datos': datos,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory MensajeP2P.fromJson(String jsonStr) {
|
factory MensajeP2P.fromJson(String jsonStr) {
|
||||||
final mapa = json.decode(jsonStr) as Map<String, dynamic>;
|
final mapa = json.decode(jsonStr) as Map<String, dynamic>;
|
||||||
@@ -53,7 +54,8 @@ class JugadorConectado {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Callback para mensajes recibidos
|
/// Callback para mensajes recibidos
|
||||||
typedef OnMensajeCallback = void Function(String endpointId, MensajeP2P mensaje);
|
typedef OnMensajeCallback =
|
||||||
|
void Function(String endpointId, MensajeP2P mensaje);
|
||||||
|
|
||||||
/// Servicio para conexiones P2P usando Google Nearby Connections API.
|
/// Servicio para conexiones P2P usando Google Nearby Connections API.
|
||||||
class ServicioNearby extends ChangeNotifier {
|
class ServicioNearby extends ChangeNotifier {
|
||||||
@@ -80,6 +82,9 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
String? _faseActual;
|
String? _faseActual;
|
||||||
Map<String, dynamic>? _datosPartida;
|
Map<String, dynamic>? _datosPartida;
|
||||||
|
|
||||||
|
// Pool de usuarios para modo multi-dispositivo
|
||||||
|
final Map<String, Usuario> _usuariosPool = {};
|
||||||
|
|
||||||
bool get esHost => _esHost;
|
bool get esHost => _esHost;
|
||||||
bool get conectado => _conectado;
|
bool get conectado => _conectado;
|
||||||
bool get buscando => _buscando;
|
bool get buscando => _buscando;
|
||||||
@@ -95,7 +100,11 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
|
|
||||||
List<JugadorConectado> get jugadores => _jugadores.values.toList();
|
List<JugadorConectado> get jugadores => _jugadores.values.toList();
|
||||||
int get numJugadoresConectados => _jugadores.length;
|
int get numJugadoresConectados => _jugadores.length;
|
||||||
Map<String, String> get hostsEncontrados => Map.unmodifiable(_hostsEncontrados);
|
Map<String, String> get hostsEncontrados =>
|
||||||
|
Map.unmodifiable(_hostsEncontrados);
|
||||||
|
|
||||||
|
/// Pool de usuarios disponibles para seleccionar en modo multi-dispositivo
|
||||||
|
List<Usuario> get usuarios => _usuariosPool.values.toList();
|
||||||
|
|
||||||
/// Registra un listener de mensajes
|
/// Registra un listener de mensajes
|
||||||
void onMensaje(OnMensajeCallback callback) {
|
void onMensaje(OnMensajeCallback callback) {
|
||||||
@@ -113,6 +122,45 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== USER POOL ====================
|
||||||
|
|
||||||
|
/// Agrega un usuario al pool de usuarios
|
||||||
|
void agregarUsuario(Usuario usuario) {
|
||||||
|
_usuariosPool[usuario.id] = usuario;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elimina un usuario del pool
|
||||||
|
void eliminarUsuario(String usuarioId) {
|
||||||
|
_usuariosPool.remove(usuarioId);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene un usuario por su ID
|
||||||
|
Usuario? getUsuario(String usuarioId) {
|
||||||
|
return _usuariosPool[usuarioId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sincroniza el pool de usuarios con una lista
|
||||||
|
void sincronizarUsuarios(List<Usuario> usuarios) {
|
||||||
|
_usuariosPool.clear();
|
||||||
|
for (final usuario in usuarios) {
|
||||||
|
_usuariosPool[usuario.id] = usuario;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el jugador local del host (él mismo como participante)
|
||||||
|
/// Retorna un JugadorConectado con endpointId null porque es local
|
||||||
|
JugadorConectado? getJugadorLocal() {
|
||||||
|
if (_miNombre == null) return null;
|
||||||
|
return JugadorConectado(
|
||||||
|
endpointId: _miEndpointId ?? '', // vacío indica que es el host local
|
||||||
|
nombre: _miNombre!,
|
||||||
|
listo: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== HOST ====================
|
// ==================== HOST ====================
|
||||||
|
|
||||||
/// Inicia como host (anunciando el endpoint)
|
/// Inicia como host (anunciando el endpoint)
|
||||||
@@ -211,10 +259,13 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
_hostEndpointId = endpointId;
|
_hostEndpointId = endpointId;
|
||||||
_conectado = true;
|
_conectado = true;
|
||||||
// Enviar mensaje de unirse
|
// Enviar mensaje de unirse
|
||||||
enviarMensaje(endpointId, MensajeP2P(
|
enviarMensaje(
|
||||||
|
endpointId,
|
||||||
|
MensajeP2P(
|
||||||
tipo: TipoMensaje.unirse,
|
tipo: TipoMensaje.unirse,
|
||||||
datos: {'nombre': _miNombre ?? 'Jugador'},
|
datos: {'nombre': _miNombre ?? 'Jugador'},
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
@@ -228,10 +279,12 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
final jugador = _jugadores.remove(endpointId);
|
final jugador = _jugadores.remove(endpointId);
|
||||||
if (jugador != null) {
|
if (jugador != null) {
|
||||||
// Notificar a todos que se desconectó
|
// Notificar a todos que se desconectó
|
||||||
enviarATodos(MensajeP2P(
|
enviarATodos(
|
||||||
|
MensajeP2P(
|
||||||
tipo: TipoMensaje.jugadorDesconectado,
|
tipo: TipoMensaje.jugadorDesconectado,
|
||||||
datos: {'nombre': jugador.nombre, 'endpointId': endpointId},
|
datos: {'nombre': jugador.nombre, 'endpointId': endpointId},
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Cliente perdió conexión con host
|
// Cliente perdió conexión con host
|
||||||
@@ -241,7 +294,11 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEndpointEncontrado(String endpointId, String endpointName, String serviceId) {
|
void _onEndpointEncontrado(
|
||||||
|
String endpointId,
|
||||||
|
String endpointName,
|
||||||
|
String serviceId,
|
||||||
|
) {
|
||||||
debugPrint('Host encontrado: $endpointName ($endpointId)');
|
debugPrint('Host encontrado: $endpointName ($endpointId)');
|
||||||
_hostsEncontrados[endpointId] = endpointName;
|
_hostsEncontrados[endpointId] = endpointName;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -303,17 +360,20 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
endpointId: endpointId,
|
endpointId: endpointId,
|
||||||
nombre: nombre,
|
nombre: nombre,
|
||||||
);
|
);
|
||||||
// Enviar info de sala al nuevo jugador
|
// Enviar info de sala al nuevo jugador (incluye pool de usuarios)
|
||||||
enviarMensaje(endpointId, MensajeP2P(
|
enviarMensaje(
|
||||||
|
endpointId,
|
||||||
|
MensajeP2P(
|
||||||
tipo: TipoMensaje.salaInfo,
|
tipo: TipoMensaje.salaInfo,
|
||||||
datos: {
|
datos: {
|
||||||
'sala': _nombreSala,
|
'sala': _nombreSala,
|
||||||
'jugadores': _jugadores.values.map((j) => {
|
'jugadores': _jugadores.values
|
||||||
'nombre': j.nombre,
|
.map((j) => {'nombre': j.nombre, 'endpointId': j.endpointId})
|
||||||
'endpointId': j.endpointId,
|
.toList(),
|
||||||
}).toList(),
|
'usuarios': _usuariosPool.values.map((u) => u.toJson()).toList(),
|
||||||
},
|
},
|
||||||
));
|
),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -330,15 +390,62 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TipoMensaje.usuarioNuevo:
|
||||||
|
_handleUsuarioNuevo(mensaje);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TipoMensaje.usuariosActualizados:
|
||||||
|
_handleUsuariosActualizados(mensaje);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleUsuarioNuevo(MensajeP2P mensaje) {
|
||||||
|
final usuarioJson = mensaje.datos['usuario'] as Map<String, dynamic>?;
|
||||||
|
if (usuarioJson != null) {
|
||||||
|
final nuevoUsuario = Usuario.fromJson(usuarioJson);
|
||||||
|
_usuariosPool[nuevoUsuario.id] = nuevoUsuario;
|
||||||
|
// Propagar a todos los clientes
|
||||||
|
if (_esHost) {
|
||||||
|
enviarATodos(
|
||||||
|
MensajeP2P(
|
||||||
|
tipo: TipoMensaje.usuarioNuevo,
|
||||||
|
datos: {'usuario': usuarioJson},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUsuariosActualizados(MensajeP2P mensaje) {
|
||||||
|
final usuariosJson = mensaje.datos['usuarios'] as List<dynamic>?;
|
||||||
|
if (usuariosJson != null) {
|
||||||
|
_usuariosPool.clear();
|
||||||
|
for (final u in usuariosJson) {
|
||||||
|
final usuario = Usuario.fromJson(u as Map<String, dynamic>);
|
||||||
|
_usuariosPool[usuario.id] = usuario;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _procesarMensajeCliente(String endpointId, MensajeP2P mensaje) {
|
void _procesarMensajeCliente(String endpointId, MensajeP2P mensaje) {
|
||||||
switch (mensaje.tipo) {
|
switch (mensaje.tipo) {
|
||||||
case TipoMensaje.salaInfo:
|
case TipoMensaje.salaInfo:
|
||||||
_datosPartida = mensaje.datos;
|
_datosPartida = mensaje.datos;
|
||||||
|
// Sincronizar pool de usuarios si viene en el mensaje
|
||||||
|
final usuariosData = mensaje.datos['usuarios'] as List<dynamic>?;
|
||||||
|
if (usuariosData != null) {
|
||||||
|
_usuariosPool.clear();
|
||||||
|
for (final u in usuariosData) {
|
||||||
|
final usuario = Usuario.fromJson(u as Map<String, dynamic>);
|
||||||
|
_usuariosPool[usuario.id] = usuario;
|
||||||
|
}
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -402,7 +509,9 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
}) async {
|
}) async {
|
||||||
for (final entry in _jugadores.entries) {
|
for (final entry in _jugadores.entries) {
|
||||||
final esImpostor = impostores[entry.key] ?? false;
|
final esImpostor = impostores[entry.key] ?? false;
|
||||||
await enviarMensaje(entry.key, MensajeP2P(
|
await enviarMensaje(
|
||||||
|
entry.key,
|
||||||
|
MensajeP2P(
|
||||||
tipo: TipoMensaje.partidaInicio,
|
tipo: TipoMensaje.partidaInicio,
|
||||||
datos: {
|
datos: {
|
||||||
'palabra': esImpostor ? null : palabraSecreta,
|
'palabra': esImpostor ? null : palabraSecreta,
|
||||||
@@ -410,30 +519,32 @@ class ServicioNearby extends ChangeNotifier {
|
|||||||
'categoria': categoria,
|
'categoria': categoria,
|
||||||
'numJugadores': _jugadores.length + 1, // +1 por el host
|
'numJugadores': _jugadores.length + 1, // +1 por el host
|
||||||
},
|
},
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Host envía cambio de fase
|
/// Host envía cambio de fase
|
||||||
Future<void> enviarCambioFase(String fase, [Map<String, dynamic>? extra]) async {
|
Future<void> enviarCambioFase(
|
||||||
|
String fase, [
|
||||||
|
Map<String, dynamic>? extra,
|
||||||
|
]) async {
|
||||||
final datos = {'fase': fase, ...?extra};
|
final datos = {'fase': fase, ...?extra};
|
||||||
await enviarATodos(MensajeP2P(tipo: TipoMensaje.fase, datos: datos));
|
await enviarATodos(MensajeP2P(tipo: TipoMensaje.fase, datos: datos));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Host envía resultado de votación
|
/// Host envía resultado de votación
|
||||||
Future<void> enviarResultadoVotacion(Map<String, dynamic> resultado) async {
|
Future<void> enviarResultadoVotacion(Map<String, dynamic> resultado) async {
|
||||||
await enviarATodos(MensajeP2P(
|
await enviarATodos(
|
||||||
tipo: TipoMensaje.votacionResultado,
|
MensajeP2P(tipo: TipoMensaje.votacionResultado, datos: resultado),
|
||||||
datos: resultado,
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Host envía fin de partida
|
/// Host envía fin de partida
|
||||||
Future<void> enviarFinPartida(Map<String, dynamic> resultado) async {
|
Future<void> enviarFinPartida(Map<String, dynamic> resultado) async {
|
||||||
await enviarATodos(MensajeP2P(
|
await enviarATodos(
|
||||||
tipo: TipoMensaje.partidaFin,
|
MensajeP2P(tipo: TipoMensaje.partidaFin, datos: resultado),
|
||||||
datos: resultado,
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== LIMPIEZA ====================
|
// ==================== LIMPIEZA ====================
|
||||||
|
|||||||
115
test/estado_juego_crear_partida_host_test.dart
Normal file
115
test/estado_juego_crear_partida_host_test.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:farolero/estado/estado_juego.dart';
|
||||||
|
import 'package:farolero/modelos/palabra.dart';
|
||||||
|
import 'package:farolero/modelos/partida.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('EstadoJuego crearPartida with host local', () {
|
||||||
|
late EstadoJuego estado;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
estado = EstadoJuego();
|
||||||
|
await estado.cargarBanco();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
estado.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should include host in jugadores list when setHostJugador called',
|
||||||
|
() {
|
||||||
|
// Set host local player first
|
||||||
|
estado.setHostJugador('HostJuan');
|
||||||
|
|
||||||
|
// Create game with 3 client players + host (host is in the list)
|
||||||
|
estado.crearPartida(
|
||||||
|
config: ConfigPartida(
|
||||||
|
modoMultimovil: true,
|
||||||
|
categoria: 'objetos',
|
||||||
|
numImpostores: 1,
|
||||||
|
pistaImpostor: false,
|
||||||
|
),
|
||||||
|
nombresJugadores: ['HostJuan', 'Cliente1', 'Cliente2', 'Cliente3'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(estado.partida, isNotNull);
|
||||||
|
expect(estado.partida!.jugadores.length, 4);
|
||||||
|
|
||||||
|
// Verify host is in the list
|
||||||
|
final hostJugador = estado.partida!.jugadores
|
||||||
|
.where((j) => j.nombre == 'HostJuan')
|
||||||
|
.firstOrNull;
|
||||||
|
expect(hostJugador, isNotNull);
|
||||||
|
expect(hostJugador!.endpointId, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('should assign word to host local player', () {
|
||||||
|
estado.setHostJugador('HostJuan');
|
||||||
|
|
||||||
|
estado.crearPartida(
|
||||||
|
config: ConfigPartida(
|
||||||
|
modoMultimovil: true,
|
||||||
|
categoria: 'objetos',
|
||||||
|
numImpostores: 1,
|
||||||
|
pistaImpostor: false,
|
||||||
|
),
|
||||||
|
nombresJugadores: ['HostJuan', 'Cliente1', 'Cliente2', 'Cliente3'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final hostJugador = estado.partida!.jugadores
|
||||||
|
.where((j) => j.nombre == 'HostJuan')
|
||||||
|
.firstOrNull;
|
||||||
|
|
||||||
|
// Host should receive a word if not impostor
|
||||||
|
expect(hostJugador, isNotNull);
|
||||||
|
if (!hostJugador!.esImpostor) {
|
||||||
|
expect(hostJugador.palabra, isNotNull);
|
||||||
|
expect(hostJugador.palabra!.isNotEmpty, isTrue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should include host in impostor selection pool', () async {
|
||||||
|
// Run multiple times to increase chance of host being impostor
|
||||||
|
bool hostWasImpostorAtLeastOnce = false;
|
||||||
|
bool hostWasNormalAtLeastOnce = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
final estado2 = EstadoJuego();
|
||||||
|
await estado2.cargarBanco();
|
||||||
|
estado2.setHostJugador('HostJuan');
|
||||||
|
|
||||||
|
estado2.crearPartida(
|
||||||
|
config: ConfigPartida(
|
||||||
|
modoMultimovil: true,
|
||||||
|
categoria: 'objetos',
|
||||||
|
numImpostores: 1,
|
||||||
|
pistaImpostor: false,
|
||||||
|
),
|
||||||
|
nombresJugadores: ['HostJuan', 'Cliente1', 'Cliente2', 'Cliente3'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final hostJugador = estado2.partida!.jugadores
|
||||||
|
.where((j) => j.nombre == 'HostJuan')
|
||||||
|
.firstOrNull;
|
||||||
|
|
||||||
|
if (hostJugador!.esImpostor) {
|
||||||
|
hostWasImpostorAtLeastOnce = true;
|
||||||
|
} else {
|
||||||
|
hostWasNormalAtLeastOnce = true;
|
||||||
|
}
|
||||||
|
estado2.dispose();
|
||||||
|
|
||||||
|
if (hostWasImpostorAtLeastOnce && hostWasNormalAtLeastOnce) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host should have been impostor at least once and normal at least once
|
||||||
|
// (statistically likely with 20 iterations)
|
||||||
|
expect(hostWasImpostorAtLeastOnce, isTrue);
|
||||||
|
expect(hostWasNormalAtLeastOnce, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
39
test/estado_juego_host_local_test.dart
Normal file
39
test/estado_juego_host_local_test.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:farolero/estado/estado_juego.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('EstadoJuego host local', () {
|
||||||
|
late EstadoJuego estado;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
estado = EstadoJuego();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
estado.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should start with null hostLocal', () {
|
||||||
|
expect(estado.hostLocal, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set host jugador correctly', () {
|
||||||
|
estado.setHostJugador('Juan');
|
||||||
|
|
||||||
|
expect(estado.hostLocal, isNotNull);
|
||||||
|
expect(estado.hostLocal!.nombre, 'Juan');
|
||||||
|
expect(estado.hostLocal!.endpointId, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update host jugador name', () {
|
||||||
|
estado.setHostJugador('Juan');
|
||||||
|
expect(estado.hostLocal!.nombre, 'Juan');
|
||||||
|
|
||||||
|
estado.setHostJugador('Maria');
|
||||||
|
expect(estado.hostLocal!.nombre, 'Maria');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
70
test/servicio_nearby_user_pool_test.dart
Normal file
70
test/servicio_nearby_user_pool_test.dart
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:farolero/modelos/usuario.dart';
|
||||||
|
import 'package:farolero/servicios/servicio_nearby.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('ServicioNearby user pool', () {
|
||||||
|
late ServicioNearby servicio;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
servicio = ServicioNearby();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
servicio.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should start with empty user pool', () {
|
||||||
|
expect(servicio.usuarios, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add user to pool', () {
|
||||||
|
final usuario = Usuario(id: 'user-1', nombre: 'Juan');
|
||||||
|
servicio.agregarUsuario(usuario);
|
||||||
|
|
||||||
|
expect(servicio.usuarios.length, 1);
|
||||||
|
expect(servicio.usuarios.first.nombre, 'Juan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove user from pool', () {
|
||||||
|
final usuario = Usuario(id: 'user-1', nombre: 'Juan');
|
||||||
|
servicio.agregarUsuario(usuario);
|
||||||
|
expect(servicio.usuarios.length, 1);
|
||||||
|
|
||||||
|
servicio.eliminarUsuario('user-1');
|
||||||
|
expect(servicio.usuarios, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should synchronize users from list', () {
|
||||||
|
final usuarios = [
|
||||||
|
Usuario(id: 'user-1', nombre: 'Juan'),
|
||||||
|
Usuario(id: 'user-2', nombre: 'Maria'),
|
||||||
|
];
|
||||||
|
servicio.sincronizarUsuarios(usuarios);
|
||||||
|
|
||||||
|
expect(servicio.usuarios.length, 2);
|
||||||
|
expect(servicio.usuarios.map((u) => u.nombre).toList(), contains('Juan'));
|
||||||
|
expect(
|
||||||
|
servicio.usuarios.map((u) => u.nombre).toList(),
|
||||||
|
contains('Maria'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get usuario by id', () {
|
||||||
|
final usuario = Usuario(id: 'user-1', nombre: 'Juan');
|
||||||
|
servicio.agregarUsuario(usuario);
|
||||||
|
|
||||||
|
final found = servicio.getUsuario('user-1');
|
||||||
|
expect(found, isNotNull);
|
||||||
|
expect(found!.nombre, 'Juan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for non-existent user', () {
|
||||||
|
final found = servicio.getUsuario('non-existent');
|
||||||
|
expect(found, isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
37
test/usuario_test.dart
Normal file
37
test/usuario_test.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:farolero/modelos/usuario.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Usuario', () {
|
||||||
|
test('should create Usuario with id and nombre', () {
|
||||||
|
final usuario = Usuario(id: 'test-id', nombre: 'Juan');
|
||||||
|
|
||||||
|
expect(usuario.id, 'test-id');
|
||||||
|
expect(usuario.nombre, 'Juan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should serialize to JSON correctly', () {
|
||||||
|
final usuario = Usuario(id: 'test-id', nombre: 'Juan');
|
||||||
|
final json = usuario.toJson();
|
||||||
|
|
||||||
|
expect(json['id'], 'test-id');
|
||||||
|
expect(json['nombre'], 'Juan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should deserialize from JSON correctly', () {
|
||||||
|
final json = {'id': 'test-id', 'nombre': 'Juan'};
|
||||||
|
final usuario = Usuario.fromJson(json);
|
||||||
|
|
||||||
|
expect(usuario.id, 'test-id');
|
||||||
|
expect(usuario.nombre, 'Juan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle JSON with avatar field', () {
|
||||||
|
final json = {'id': 'test-id', 'nombre': 'Juan', 'avatar': '👤'};
|
||||||
|
final usuario = Usuario.fromJson(json);
|
||||||
|
|
||||||
|
expect(usuario.id, 'test-id');
|
||||||
|
expect(usuario.nombre, 'Juan');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user